您好,登錄后才能下訂單哦!
這篇文章主要介紹了Java中無權無向圖的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
我們知道,前面討論的數據結構都有一個框架,而這個框架是由相應的算法實現的,比如二叉樹搜索樹,左子樹上所有結點的值均小于它的根結點的值,右子樹所有結點的值均大于它的根節點的值,類似這種形狀使得它容易搜索數據和插入數據,樹的邊表示了從一個節點到另一個節點的快捷方式。
而圖通常有個固定的形狀,這是由物理或抽象的問題所決定的。比如圖中節點表示城市,而邊可能表示城市間的班機航線。如下圖是美國加利福利亞簡化的高速公路網:
如果兩個頂點被同一條邊連接,就稱這兩個頂點是鄰接的,如上圖 I 和 G 就是鄰接的,而 I 和 F 就不是。有時候也將和某個指定頂點鄰接的頂點叫做它的鄰居,比如頂點 G 的鄰居是 I、H、F。
路徑是邊的序列,比如從頂點B到頂點J的路徑為 BAEJ,當然還有別的路徑 BCDJ,BACDJ等等。
如果至少有一條路徑可以連接起所有的頂點,那么這個圖稱作連通的;如果假如存在從某個頂點不能到達另外一個頂點,則稱為非聯通的。
如果圖中的邊沒有方向,可以從任意一邊到達另一邊,則稱為無向圖;比如雙向高速公路,A城市到B城市可以開車從A駛向B,也可以開車從B城市駛向A城市。但是如果只能從A城市駛向B城市的圖,那么則稱為有向圖。
圖中的邊被賦予一個權值,權值是一個數字,它能代表兩個頂點間的物理距離,或者從一個頂點到另一個頂點的時間,這種圖被稱為有權圖;反之邊沒有賦值的則稱為無權圖。
本篇博客我們討論的是無權無向圖。
我們知道圖是由頂點和邊組成,那么在計算機中,怎么來模擬頂點和邊?
在大多數情況下,頂點表示某個真實世界的對象,這個對象必須用數據項來描述。比如在一個飛機航線模擬程序中,頂點表示城市,那么它需要存儲城市的名字、海拔高度、地理位置和其它相關信息,因此通常用一個頂點類的對象來表示一個頂點,這里我們僅僅在頂點中存儲了一個字母來標識頂點,同時還有一個標志位,用來判斷該頂點有沒有被訪問過(用于后面的搜索)。
/** * 頂點類 * @author vae */ public class Vertex { public char label; public boolean wasVisited; public Vertex(char label){ this.label = label; wasVisited = false; } }
頂點對象能放在數組中,然后用下標指示,也可以放在鏈表或其它數據結構中,不論使用什么結構,存儲只是為了使用方便,這與邊如何連接點是沒有關系的。
在前面講解各種樹的數據結構時,大多數樹都是每個節點包含它的子節點的引用,比如紅黑樹、二叉樹。也有用數組表示樹,樹組中節點的位置決定了它和其它節點的關系,比如堆就是用數組表示。
然而圖并不像樹,圖沒有固定的結構,圖的每個頂點可以與任意多個頂點相連,為了模擬這種自由形式的組織結構,用如下兩種方式表示圖:鄰接矩陣和鄰接表(如果一條邊連接兩個頂點,那么這兩個頂點就是鄰接的)
鄰接矩陣:
鄰接矩陣是一個二維數組,數據項表示兩點間是否存在邊,如果圖中有 N 個頂點,鄰接矩陣就是 N*N 的數組。上圖用鄰接矩陣表示如下:
1表示有邊,0表示沒有邊,也可以用布爾變量true和false來表示。頂點與自身相連用 0 表示,所以這個矩陣從左上角到右上角的對角線全是 0 。
注意:這個矩陣的上三角是下三角的鏡像,兩個三角包含了相同的信息,這個冗余信息看似低效,但是在大多數計算機中,創造一個三角形數組比較困難,所以只好接受這個冗余,這也要求在程序處理中,當我們增加一條邊時,比如更新鄰接矩陣的兩部分,而不是一部分。
鄰接表:
鄰接表是一個鏈表數組(或者是鏈表的鏈表),每個單獨的鏈表表示了有哪些頂點與當前頂點鄰接。
在圖中實現最基本的操作之一就是搜索從一個指定頂點可以到達哪些頂點,比如從武漢出發的高鐵可以到達哪些城市,一些城市可以直達,一些城市不能直達。現在有一份全國高鐵模擬圖,要從某個城市(頂點)開始,沿著鐵軌(邊)移動到其他城市(頂點),有兩種方法可以用來搜索圖:深度優先搜索(DFS)和廣度優先搜索(BFS)。它們最終都會到達所有連通的頂點,深度優先搜索通過棧來實現,而廣度優先搜索通過隊列來實現,不同的實現機制導致不同的搜索方式。
深度優先搜索算法有如下規則:
規則1:如果可能,訪問一個鄰接的未訪問頂點,標記它,并將它放入棧中。
規則2:當不能執行規則 1 時,如果棧不為空,就從棧中彈出一個頂點。
規則3:如果不能執行規則 1 和規則 2 時,就完成了整個搜索過程。
對于上圖,應用深度優先搜索如下:假設選取 A 頂點為起始點,并且按照字母優先順序進行訪問,那么應用規則 1 ,接下來訪問頂點 B,然后標記它,并將它放入棧中;再次應用規則 1,接下來訪問頂點 F,再次應用規則 1,訪問頂點 H。我們這時候發現,沒有 H 頂點的鄰接點了,這時候應用規則 2,從棧中彈出 H,這時候回到了頂點 F,但是我們發現 F 也除了 H 也沒有與之鄰接且未訪問的頂點了,那么再彈出 F,這時候回到頂點 B,同理規則 1 應用不了,應用規則 2,彈出 B,這時候棧中只有頂點 A了,然后 A 還有未訪問的鄰接點,所有接下來訪問頂點 C,但是 C又是這條線的終點,所以從棧中彈出它,再次回到 A,接著訪問 D,G,I,最后也回到了 A,然后訪問 E,但是最后又回到了頂點 A,這時候我們發現 A沒有未訪問的鄰接點了,所以也把它彈出棧。現在棧中已無頂點,于是應用規則 3,完成了整個搜索過程。
深度優先搜索在于能夠找到與某一頂點鄰接且沒有訪問過的頂點。這里以鄰接矩陣為例,找到頂點所在的行,從第一列開始向后尋找值為1的列;列號是鄰接頂點的號碼,檢查這個頂點是否未訪問過,如果是這樣,那么這就是要訪問的下一個頂點,如果該行沒有頂點既等于1(鄰接)且又是未訪問的,那么與指定點相鄰接的頂點就全部訪問過了(后面會用算法實現)。
深度優先搜索要盡可能的遠離起始點,而廣度優先搜索則要盡可能的靠近起始點,它首先訪問起始頂點的所有鄰接點,然后再訪問較遠的區域,這種搜索不能用棧實現,而是用隊列實現。
規則1:訪問下一個未訪問的鄰接點(如果存在),這個頂點必須是當前頂點的鄰接點,標記它,并把它插入到隊列中。
規則2:如果已經沒有未訪問的鄰接點而不能執行規則 1 時,那么從隊列列頭取出一個頂點(如果存在),并使其成為當前頂點。
規則3:如果因為隊列為空而不能執行規則 2,則搜索結束。
對于上面的圖,應用廣度優先搜索:以A為起始點,首先訪問所有與 A 相鄰的頂點,并在訪問的同時將其插入隊列中,現在已經訪問了 A,B,C,D和E。這時隊列(從頭到尾)包含 BCDE,已經沒有未訪問的且與頂點 A 鄰接的頂點了,所以從隊列中取出B,尋找與B鄰接的頂點,這時找到F,所以把F插入到隊列中。已經沒有未訪問且與B鄰接的頂點了,所以從隊列列頭取出C,它沒有未訪問的鄰接點。因此取出 D 并訪問 G,D也沒有未訪問的鄰接點了,所以取出E,現在隊列中有 FG,在取出 F,訪問 H,然后取出 G,訪問 I,現在隊列中有 HI,當取出他們時,發現沒有其它為訪問的頂點了,這時隊列為空,搜索結束。
實現深度優先搜索的棧StackX.class
package com.ys.graph; public class StackX { private final int SIZE = 20; private int[] st; private int top; public StackX(){ st = new int[SIZE]; top = -1; } public void push(int j){ st[++top] = j; } public int pop(){ return st[top--]; } public int peek(){ return st[top]; } public boolean isEmpty(){ return (top == -1); } }
實現廣度優先搜索的隊列Queue.class
package com.ys.graph; public class Queue { private final int SIZE = 20; private int[] queArray; private int front; private int rear; public Queue(){ queArray = new int[SIZE]; front = 0; rear = -1; } public void insert(int j) { if(rear == SIZE-1) { rear = -1; } queArray[++rear] = j; } public int remove() { int temp = queArray[front++]; if(front == SIZE) { front = 0; } return temp; } public boolean isEmpty() { return (rear+1 == front || front+SIZE-1 == rear); } }
圖代碼 Graph.class
package com.ys.graph; public class Graph { private final int MAX_VERTS = 20;//表示頂點的個數 private Vertex vertexList[];//用來存儲頂點的數組 private int adjMat[][];//用鄰接矩陣來存儲 邊,數組元素0表示沒有邊界,1表示有邊界 private int nVerts;//頂點個數 private StackX theStack;//用棧實現深度優先搜索 private Queue queue;//用隊列實現廣度優先搜索 /** * 頂點類 * @author vae */ class Vertex { public char label; public boolean wasVisited; public Vertex(char label){ this.label = label; wasVisited = false; } } public Graph(){ vertexList = new Vertex[MAX_VERTS]; adjMat = new int[MAX_VERTS][MAX_VERTS]; nVerts = 0;//初始化頂點個數為0 //初始化鄰接矩陣所有元素都為0,即所有頂點都沒有邊 for(int i = 0; i < MAX_VERTS; i++) { for(int j = 0; j < MAX_VERTS; j++) { adjMat[i][j] = 0; } } theStack = new StackX(); queue = new Queue(); } //將頂點添加到數組中,是否訪問標志置為wasVisited=false(未訪問) public void addVertex(char lab) { vertexList[nVerts++] = new Vertex(lab); } //注意用鄰接矩陣表示邊,是對稱的,兩部分都要賦值 public void addEdge(int start, int end) { adjMat[start][end] = 1; adjMat[end][start] = 1; } //打印某個頂點表示的值 public void displayVertex(int v) { System.out.print(vertexList[v].label); } /**深度優先搜索算法: * 1、用peek()方法檢查棧頂的頂點 * 2、用getAdjUnvisitedVertex()方法找到當前棧頂點鄰接且未被訪問的頂點 * 3、第二步方法返回值不等于-1則找到下一個未訪問的鄰接頂點,訪問這個頂點,并入棧 * 如果第二步方法返回值等于 -1,則沒有找到,出棧 */ public void depthFirstSearch() { //從第一個頂點開始訪問 vertexList[0].wasVisited = true; //訪問之后標記為true displayVertex(0);//打印訪問的第一個頂點 theStack.push(0);//將第一個頂點放入棧中 while(!theStack.isEmpty()) { //找到棧當前頂點鄰接且未被訪問的頂點 int v = getAdjUnvisitedVertex(theStack.peek()); if(v == -1) { //如果當前頂點值為-1,則表示沒有鄰接且未被訪問頂點,那么出棧頂點 theStack.pop(); }else { //否則訪問下一個鄰接頂點 vertexList[v].wasVisited = true; displayVertex(v); theStack.push(v); } } //棧訪問完畢,重置所有標記位wasVisited=false for(int i = 0; i < nVerts; i++) { vertexList[i].wasVisited = false; } } //找到與某一頂點鄰接且未被訪問的頂點 public int getAdjUnvisitedVertex(int v) { for(int i = 0; i < nVerts; i++) { //v頂點與i頂點相鄰(鄰接矩陣值為1)且未被訪問 wasVisited==false if(adjMat[v][i] == 1 && vertexList[i].wasVisited == false) { return i; } } return -1; } /** * 廣度優先搜索算法: * 1、用remove()方法檢查棧頂的頂點 * 2、試圖找到這個頂點還未訪問的鄰節點 * 3、 如果沒有找到,該頂點出列 * 4、 如果找到這樣的頂點,訪問這個頂點,并把它放入隊列中 */ public void breadthFirstSearch(){ vertexList[0].wasVisited = true; displayVertex(0); queue.insert(0); int v2; while(!queue.isEmpty()) { int v1 = queue.remove(); while((v2 = getAdjUnvisitedVertex(v1)) != -1) { vertexList[v2].wasVisited = true; displayVertex(v2); queue.insert(v2); } } //搜索完畢,初始化,以便于下次搜索 for(int i = 0; i < nVerts; i++) { vertexList[i].wasVisited = false; } } public static void main(String[] args) { Graph graph = new Graph(); graph.addVertex('A'); graph.addVertex('B'); graph.addVertex('C'); graph.addVertex('D'); graph.addVertex('E'); graph.addEdge(0, 1);//AB graph.addEdge(1, 2);//BC graph.addEdge(0, 3);//AD graph.addEdge(3, 4);//DE System.out.println("深度優先搜索算法 :"); graph.depthFirstSearch();//ABCDE System.out.println(); System.out.println("----------------------"); System.out.println("廣度優先搜索算法 :"); graph.breadthFirstSearch();//ABDCE } }
測試結果:
對于圖的操作,還有一個最常用的就是找到最小生成樹,最小生成樹就是用最少的邊連接所有頂點。對于給定的一組頂點,可能有很多種最小生成樹,但是最小生成樹的邊的數量 E 總是比頂點 V 的數量小1,即:
V = E + 1
這里不用關心邊的長度,不是找最短的路徑(會在帶權圖中講解),而是找最少數量的邊,可以基于深度優先搜索和廣度優先搜索來實現。
比如基于深度優先搜索,我們記錄走過的邊,就可以創建一個最小生成樹。因為DFS 訪問所有頂點,但只訪問一次,它絕對不會兩次訪問同一個頂點,但她看到某條邊將到達一個已訪問的頂點,它就不會走這條邊,它從來不遍歷那些不可能的邊,因此,DFS 算法走過整個圖的路徑必定是最小生成樹。
//基于深度優先搜索找到最小生成樹 public void mst(){ vertexList[0].wasVisited = true; theStack.push(0); while(!theStack.isEmpty()){ int currentVertex = theStack.peek(); int v = getAdjUnvisitedVertex(currentVertex); if(v == -1){ theStack.pop(); }else{ vertexList[v].wasVisited = true; theStack.push(v); displayVertex(currentVertex); displayVertex(v); System.out.print(" "); } } //搜索完畢,初始化,以便于下次搜索 for(int i = 0; i < nVerts; i++) { vertexList[i].wasVisited = false; } }
感謝你能夠認真閱讀完這篇文章,希望小編分享的“Java中無權無向圖的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。