咱們以前介紹了線性關係的線性表,層次關係的樹形結構,下面咱們來介紹結點之間關係任意的結構,圖。
1、相關概念node
1,圖是由頂點的有窮非空集合和頂點之間邊的集合組成,一般表示爲G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。算法
2,各類圖定義數組
若兩頂點之間的邊沒有方向,則稱這條邊爲無向邊Edge,用無序偶對(v1,v2)來表示。若是圖中任意兩個頂點之間的邊都是無向邊,則稱該圖爲無向圖。數據結構
若從頂點v1到v2的邊有方向,則稱這條邊爲有向邊,也稱爲弧(Arc),表示爲有序偶<v1,v2>,稱v1爲弧尾,v2爲弧頭。若圖中任意兩個頂點之間的邊都是有向邊,則稱該圖爲有向圖。this
注意無向邊用(),有向邊用<>spa
圖按照邊或弧的多少分爲稀疏圖和稠密圖,但劃分邊界比較模糊。任意兩個頂點之間都存在邊叫徹底圖,有向的叫有向徹底圖。若無重複邊或頂點回到自身的邊的叫作簡單圖。指針
圖上邊或弧帶權則稱爲網。code
3,圖的頂點與邊之間的關係視頻
圖中頂點之間有鄰接點Adjacent的概念,v和v‘相鄰接,邊(v,v')依附於頂點v和v’,或者說邊(v,v')與頂點v和v’相關聯。頂點的度Degree是與v相關聯的邊的數目,記做TD(v)。blog
有向圖中有入度ID(v)和出度OD(V)的概念.
圖中的頂點間存在路徑則說明是連通的,若是路徑最終回到起始點則成爲環,不重複的路徑稱爲簡單路徑。頂點不重複出現的迴路,叫作簡單迴路或簡單環。
若任意兩點之間都是連通的,則成爲連通圖,有向則是強連通圖。圖中有子圖,若子圖極大連通則稱該子圖爲連通份量,有向的則稱爲強連通份量。
無向圖是連通圖且n個頂點有n-1條邊則叫作生成樹。有向圖中一頂點入度爲0,其餘頂點入度爲1的叫作有向樹,一個有向圖能夠分解爲若干有向樹構成的生成森林。
2、圖的抽象數據類型
3、圖的存儲結構
圖結構比較複雜,任意兩個頂點之間均可能存在聯繫,所以沒法以數據元素在內存中的物理位置來表示元素間的關係;而多重鏈表的方式又有操做的不便,所以對於圖來講,它實現物理存儲是個難題,下面咱們來看前輩們已經提供的五種不一樣的存儲結構
1,鄰接矩陣
圖的鄰接矩陣存儲方式是用兩個數組來表示圖。一個一維數組存儲圖中頂點信息,一個二維數組(稱爲鄰接矩陣)存儲圖中的邊或弧的信息。若無向圖中存在這條邊,或有向圖中存在這條弧,則矩陣中的該位置置爲1,不然置0.以下
鄰接矩陣是如何實現圖的建立的呢? 代碼以下
/* 頂點的包裝類 */ public class Vertex<T>{ private T data; public Vertex(T data){ this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
/* 鄰接矩陣實現圖的建立 */ public class MGraph<T> { private Vertex<T>[] vertexs; private int[][] edges; private int numVertex; //頂點的實際數量 private int maxNumVertex; //頂點的最大數量 private int INFINITY = 65535; public MGraph(int maxNumVertex){ this.maxNumVertex = maxNumVertex; this.vertexs = (Vertex<T>[]) new Vertex[maxNumVertex]; this.edges = new int[maxNumVertex][maxNumVertex]; for (int i = 0; i < numVertex; i++){ for (int j = 0; j < numVertex; j++){ edges[i][j] = INFINITY; } } for (int i = 0; i < numVertex; i++) { edges[i][i] = 0; } } public int getNumVertex(){ return numVertex; } public int getMaxNumVertex(){ return maxNumVertex; } public boolean isFull(){ return numVertex == maxNumVertex; } public void addVertex(T data){ if(isFull()){ throw new RuntimeException("圖滿了"); } Vertex<T> v = new Vertex<T>(data); vertexs[numVertex++] = v; } /** * 刪除圖中與data相等的頂點 * @param data 要刪除的頂點的data值 * @return 返回刪除了幾個頂點 */ public int removeVertex(T data){ int flag = 0; for (int i = 0; i < numVertex; i++){ if (vertexs[i].getData().equals(data)){ for (int j = i; j < numVertex - 1; j++){ vertexs[j] = vertexs[j + 1]; } //刪除矩陣的第 i 行 for (int row = i; row < numVertex - 1; row++){ for (int col = 0; col < numVertex; col++){ edges[col][row] = edges[col][row + 1]; } } //刪除矩陣的第 i 列 for (int row = 0; row < numVertex; row++){ for (int col = i; col < numVertex - 1; col++){ edges[col][row] = edges[col + 1][row]; } } numVertex--; flag++; } } return flag; } private int getIndexOfData(T data){ int i = 0; while (!vertexs[i].getData().equals(data)){ i++; } return i; } /** * 若爲無向圖,data的順序隨意;若爲有向圖,則添加的邊是data1指向data2 * @param data1 弧尾 * @param data2 弧頭 * @param weight 權值 */ public void addEdge(T data1, T data2, int weight){ int index1 = getIndexOfData(data1); int index2 = getIndexOfData(data2); edges[index1][index2] = weight; } public void removeEdge(T data1, T data2){ int index1 = getIndexOfData(data1); int index2 = getIndexOfData(data2); edges[index1][index2] = INFINITY; } public void printMatrix(){ for (int row = 0; row < numVertex; row++){ for (int col = 0; col < numVertex; col++){ System.out.print(edges[row][col] + " "); } System.out.println(); } } }
鄰接矩陣能夠解決圖的物理存儲,但咱們也發現,對於邊相對頂點來講較少的圖,這種結構是存在對存儲空間的極大浪費的。如何解決呢?看下面
2,鄰接表
咱們在前面提到過,順序存儲結構存在預先分配內存可能形成空間浪費的問題,因而引出了鏈式存儲結構。咱們用相似於前面樹結構中孩子表示法的方式,數組與鏈表相結合的存儲方法稱爲鄰接表。
處理方法:頂點用一維數組存儲;每一個頂點的全部鄰接點構成一個線性表,用單鏈表存儲。有向圖稱爲頂點v的邊表;無向圖稱爲頂點v做爲弧尾的出邊表。
注:如果有向圖,鄰接表結構是相似的。因爲有向圖有方向,咱們是以頂點爲弧尾來存儲邊表的,這樣很容易獲得每一個頂點的出度。但也有是爲了便於肯定頂點的入度,咱們能夠創建一個有向圖的逆鄰接表,即對每一個頂點v1都創建一個連接爲v1爲弧頭的表。
如果帶權值的網圖,能夠在邊表結點的定義中再增長一個weight的數據域,存儲權值信息便可。
下面是鄰接表存儲圖結構的代碼實現
public class VertexL<T> { private T data; private EdgeL firstEdge; public VertexL(T data) { this.data = data; } public void setFirstEdge(EdgeL e){ this.firstEdge = e; } public EdgeL getFirstEdge(){ return firstEdge; } public T getData(){ return data; } }
public class EdgeL { private int adjvex; //存儲鄰接點對應的下標 private EdgeL nextEdge; //存儲 public EdgeL(int adjvex){ this.adjvex = adjvex; } public EdgeL(int adjvex, EdgeL e){ this.adjvex = adjvex; this.nextEdge = e; } public EdgeL getNextEdge(){ return nextEdge; } public void setNextEdge(EdgeL e){ this.nextEdge = e; } public int getAdjvex(){ return adjvex; } }
/* 無向圖(無權值)的鄰接表存儲。 */ public class GraphAdjList <T>{ private VertexL<T>[] vertexs; private int numVertex; private int maxNumVertex; public GraphAdjList(int maxNumVertex){ this.maxNumVertex = maxNumVertex; this.vertexs =(VertexL<T>[]) new VertexL[maxNumVertex]; numVertex = 0; } public boolean isFull(){ return numVertex == maxNumVertex; } private int getNumVertex(){ return numVertex; } /** * 添加頂點 * @param data */ public void addVertex(T data){ if (isFull()){ throw new IndexOutOfBoundsException(); } VertexL<T> v = new VertexL<T>(data); vertexs[numVertex++] = v; } /** * 添加邊 * @param data1 * @param data2 */ public void addEdge(T data1, T data2){ int indexOfData1 = getIndex(data1); int indexOfData2 = getIndex(data2); if (vertexs[indexOfData1].getFirstEdge() == null){ vertexs[indexOfData1].setFirstEdge(new EdgeL(indexOfData2)); }else { vertexs[indexOfData1].getFirstEdge().setNextEdge(new EdgeL(indexOfData2, vertexs[indexOfData1].getFirstEdge().getNextEdge())); } if (vertexs[indexOfData2].getFirstEdge() == null){ vertexs[indexOfData2].setFirstEdge(new EdgeL(indexOfData1)); }else { vertexs[indexOfData2].getFirstEdge().setNextEdge(new EdgeL(indexOfData1, vertexs[indexOfData1].getFirstEdge().getNextEdge())); } } private int getIndex(T data){ int i = 0; for (; i < numVertex; i++){ if (data.equals(vertexs[i].getData())){ break; } } if (!data.equals(vertexs[i].getData()) && i == numVertex){ throw new NullPointerException(); } return i; } /** * 刪除邊 * @param data1 * @param data2 */ public void removeEdge(T data1, T data2){ int indexOfData1 = getIndex(data1); int indexOfData2 = getIndex(data2); VertexL v = vertexs[indexOfData1]; EdgeL e = v.getFirstEdge(); if (e.getAdjvex() == indexOfData2){ if (v.getFirstEdge().getNextEdge() == null) { v.setFirstEdge(null); }else { v.setFirstEdge(e.getNextEdge()); } }else { while (e.getNextEdge().getAdjvex() != indexOfData2){ e = e.getNextEdge(); } if (e.getNextEdge().getNextEdge() != null) { e.setNextEdge(e.getNextEdge().getNextEdge()); }else { e.setNextEdge(null); } } } /** * 刪除頂點 * @param data */ public void removeVertex(T data){ int index = getIndex(data); for (int i = 0; i < numVertex; i++){ if (i == index){ continue; } removeEdge(vertexs[i].getData(), data); } for (int i = index; i < numVertex - 1; i++){ vertexs[i] = vertexs[i + 1]; } }
3,十字鏈表
對於有向圖來講,鄰接表只關心了出度問題,想了解入度就必須遍歷整個圖才能知道;反之,逆鄰接表解決了入度問題,卻不瞭解出度的狀況。那麼能不能把鄰接表和逆鄰接表結合一下呢?
這就是下面要講的存儲方式:十字鏈表。
咱們既然要結合鄰接表和逆鄰接表,就要先把頂點域融合一下以下
firstin表示入邊表表頭指針,指向該頂點入邊表的第一個結點; firstout表示出邊表表頭指針,指向該頂點出邊表的第一個結點。
下面咱們來把邊表結點結構也融合一下
其中tailvex是指弧起點在頂點表的下標 ; headvex是指弧終點在頂點表中的下標。
headlink是入邊表指針域,指向同一個弧頭的弧;taillink是出邊表指針域,指向同一個弧尾的弧。 重新的邊表結點的域能夠看出來,每個邊表結點既承擔了做爲入邊表的職責,也承擔了做爲出邊表結點的職責。
例以下面這個例子
圖中虛線箭頭的含義就是此圖的逆鄰接表的表示。咱們能夠簡單的理解爲,好比第一行的邊結點,就是表示從0指向3的有向弧,因此它必定是由0直接指向,而且由3虛線指向的。
再如圖中惟一連續指向的從V0指向邊10,再指向邊20,能夠發現弧頭爲0的都在同一列,弧尾同的都在同一行;因爲V0有兩個入度,因此虛線連續指向兩個邊結點。
十字鏈表的好處是由於結合了鄰接表和逆鄰接表,既容易找到入度,也容易找到出度。除告終構複雜一點,它建立圖算法的時間複雜度是和鄰接表相同的。所以在有向圖中,十字鏈表是很是好的數據結構。
代碼實現以下:
/* 十字鏈表實現的圖結構的弧定義 */ public class EdgeOL { private int tail; private int head; public EdgeOL(int tail, int head) { this.tail = tail; this.head = head; } } /* 十字鏈表實現的圖結構的頂點定義 */ public class VertexOL<T> { private T data; private EdgeOL firstIn; private EdgeOL firstOut; public VertexOL(T data) { this.data = data; } }
/* 圖結構的十字鏈表實現 */ public class GraphOrthogonalList<T> { private VertexOL<T>[] vertexs; private int numVertex; private int maxNumVertex; public GraphOrthogonalList(int maxNumVertex){ this.maxNumVertex = maxNumVertex; vertexs = (VertexOL<T>[])new VertexOL[maxNumVertex]; } public boolean isFull(){ return numVertex == maxNumVertex; } /** * 添加新頂點 * @param data 新頂點的數據域 */ public void addVertex(T data){ if (isFull()){ return; } VertexOL<T> v = new VertexOL<>(data); vertexs[numVertex++] = v; } public void addEdge(int tail, int head){ EdgeOL e = new EdgeOL(tail, head); //頭插法,造成十字鏈表 e.setTailLink(vertexs[tail].getFirstOut()); vertexs[tail].setFirstOut(e); e.setHeadLink(vertexs[head].getFirstIn()); vertexs[head].setFirstIn(e); } /** * 刪除一個邊結點 * @param tail * @param head */ public void removeEdge(int tail, int head){ removeFromTailList(tail, head); removeFromHeadList(tail, head); } /** * 從鄰接表中刪除一個邊結點 * @param tail * @param head */ private void removeFromTailList(int tail, int head){ EdgeOL e = vertexs[tail].getFirstOut(); //從tailLink中刪除它 if (e != null && e.getHeadVex() == head){ //若是e是第一個但不是最後一個結點,刪除它 if (e.getTailLink() != null){ vertexs[tail].setFirstOut(e.getTailLink()); }else { //若是e是第一個也是最後一個結點,刪除它 vertexs[tail].setFirstOut(null); } }else if (e != null){ //若是e不是第一個結點,那麼遍歷鏈表找到要刪除的邊結點的上一個結點!! while (e.getTailLink() != null && e.getTailLink().getHeadVex() != head){ e = e.getTailLink(); } if (e.getHeadVex() != head){ //throw new NullPointerException(); //這裏不能拋異常,由於後面要遍歷刪除邊,拋異常會使程序終止 return; }else { e.setTailLink(e.getTailLink().getTailLink()); } } } /** * 從逆鄰接表中刪除一個邊結點 * @param tail * @param head */ private void removeFromHeadList(int tail, int head){ //從headLink中刪除它 EdgeOL e = vertexs[head].getFirstOut(); if (e != null && e.getTailVex() == tail){ //若是e1是第一個但不是最後一個結點,刪除它 if (e.getHeadLink() != null){ vertexs[head].setFirstIn(e.getHeadLink()); }else { //若是e1是第一個也是最後一個結點,刪除它 vertexs[head].setFirstIn(null); } }else if (e != null){ //若是e1不是第一個結點,那麼遍歷鏈表找到要刪除的邊結點的上一個結點!! while (e.getHeadLink() != null && e.getHeadLink().getTailVex() != tail){ e = e.getHeadLink(); } if (e.getTailVex() != tail){ //throw new NullPointerException(); return; }else { e.setHeadLink(e.getHeadLink().getHeadLink()); } } } /** * 刪除index角標的頂點 * @param index */ public void removeVertex(int index){ if (index >= numVertex){ throw new NullPointerException(); } //刪除與該頂點有關的全部邊 for (int i = numVertex - 1; i > 0; i--){ removeEdge(index, i); removeEdge(i, index); } //刪除該結點 for (int i = index; i < numVertex - 1; i++){ vertexs[i] = vertexs[i + 1]; } numVertex--; } }
4,鄰接多重表
上面的三種結構看似已經解決了全部問題,但在編寫代碼的時候才能體會到,插入頂點,插入邊時很是方便,但刪除時很麻煩。如何解決呢? 有時又要對已訪問的邊作標記,又怎麼作呢?
下面咱們來看面向無向圖的鄰接多重表。
咱們把鄰接表中的邊表結點的結構進行改造以下
其中ivex和jvex是與某條邊依附的兩個頂點在頂點表中的下標。ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一條邊。這就是鄰接多重表。
注意:ilink指向的結點的jvex和它自己的ivex值相同
下面舉例
若要刪除左圖(v0 , v2)這條邊,僅需讓6 , 9這兩個連接改成^便可,刪除方便了不少。
上面這種方法是《大話數據結構》中的畫法,它 「貌似」 限制了ivex和jvex的順序,使得在代碼實現上難以思考。我找到了下面這個視頻,是一個很好的鄰接多重表的解釋,(咖喱英語警告)。它沒有限制ivex必須指向相同的jvex,沒有限制ivex和jvex的順序,也沒有限制數組中的頂點結點只能指向一個邊結點,更靈活,更好理解,代碼也更容易實現。
https://www.youtube.com/watch?v=f2z1n6atBsc
下面是視頻中畫法的代碼實現。
/* 鄰接多重表的頂點定義 */ public class VertexAM<T> { private T data; private EdgeAM firstEdge; public VertexAM(T data){ this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } public EdgeAM getFirstEdge() { return firstEdge; } public void setFirstEdge(EdgeAM firstEdge) { this.firstEdge = firstEdge; } }
/* 鄰接多重表的邊結點定義 */ public class EdgeAM { private int ivex; private int jvex; private EdgeAM ilink; private EdgeAM jlink; public EdgeAM(int ivex, int jvex){ this.ivex = ivex; this.jvex = jvex; } public int getIvex() { return ivex; } public void setIvex(int ivex) { this.ivex = ivex; } public int getJvex() { return jvex; } public void setJvex(int jvex) { this.jvex = jvex; } public EdgeAM getIlink() { return ilink; } public void setIlink(EdgeAM ilink) { this.ilink = ilink; } public EdgeAM getJlink() { return jlink; } public void setJlink(EdgeAM jlink) { this.jlink = jlink; } }
/* 鄰接多重表的代碼實現 */ public class GraphAM <T>{ private VertexAM<T>[] vertexs; private int numVertex; private int maxNumVertex; public GraphAM(int maxNumVertex){ this.maxNumVertex = maxNumVertex; this.vertexs = (VertexAM<T>[])new VertexAM[maxNumVertex]; numVertex = 0; } public boolean isFull(){ return numVertex == maxNumVertex; } public void addVertex(T data){ if (isFull()){ return; } vertexs[numVertex++] = new VertexAM<>(data); } /** * 將新結點連在iLink鏈表的鏈尾和jLink鏈表的鏈尾 * @param ivex * @param jvex */ public void addEdge(int ivex, int jvex){ EdgeAM e = new EdgeAM(ivex, jvex); if (vertexs[ivex].getFirstEdge() == null) { vertexs[ivex].setFirstEdge(e); } else if (vertexs[jvex].getFirstEdge() == null){ vertexs[jvex].setFirstEdge(e); } else { EdgeAM ptr = vertexs[ivex].getFirstEdge(); while (ptr.getIlink() != null){ ptr = ptr.getIlink(); } ptr.setIlink(e); ptr = vertexs[jvex].getFirstEdge(); while (ptr.getJlink() != null){ ptr = ptr.getJlink(); } ptr.setJlink(e); } } /** * 刪除邊,若是邊結點直連頂點結點,則直接刪除;若不直連,先找到邊結點的上一個邊結點,而後將上一個邊結點的link域置空。 * @param ivex * @param jvex */ public void removeEdge(int ivex, int jvex) { if (vertexs[ivex].getFirstEdge() != null && vertexs[ivex].getFirstEdge().getJvex() == jvex) { vertexs[ivex].setFirstEdge(null); } else if (vertexs[jvex].getFirstEdge() != null && vertexs[jvex].getFirstEdge().getIvex() == ivex) { vertexs[jvex].setFirstEdge(null); } else { removeFromLink(ivex, jvex); removeFromLink(jvex, ivex); } } private void removeFromLink(int ivex, int jvex){ EdgeAM ptr = vertexs[ivex].getFirstEdge(); if (ptr == null){ return; } while (ptr.getIlink() != null && ptr.getIlink().getJvex() != jvex) { ptr = ptr.getIlink(); } if (ptr.getIlink() == null){ return; }else { ptr.setIlink(null); } } /** * 先刪除與本頂點相連的全部邊,而後再刪除頂點 * @param index */ public void removeVertex(int index){ for (int i = 0; i < numVertex; i++){ removeEdge(i, index); removeEdge(index, i); } for (int i = index; i < numVertex - 1; i++){ vertexs[i] = vertexs[i + 1]; } numVertex--; } }
5,邊集數組
邊集數組由兩個一維數組構成,一個存儲頂點的信息;另外一個存儲邊的信息,這個邊數組每一個數據元素由一條邊的起點下標(begin)、終點下標(end)和權(weight)組成。
邊集數組的效率並不高,它適合對邊依次進行處理的操做,而不適合對頂點進行相關的操做。邊集數組的應用將在後面的克魯斯卡爾(Kruskal)算法中有介紹。
4、圖的遍歷
圖的遍歷是和樹的遍歷相似,咱們但願從圖中某一頂點出發仿遍圖中其他頂點,且使每個頂點僅被訪問一次,這一過程就叫作圖的遍歷(Traversing Graph)
1,深度優先遍歷(Depth_First_Search DFS)
咱們以上圖爲例,假設咱們從A出發,只要沒碰到訪問過的結點,就一直往右手邊走,先到B,再到C,再到D,E,F,此時再往右走就碰到A了,因此咱們往左邊走,到G,往右爲D,D訪問過了,因此往左走到H,這時H的左右D和E都已經訪問過了,到了死衚衕。
可是此時圖中還有I結點沒有訪問過,因此咱們從H沿原路後退通過G,F,E,D,C,此時從C往左手邊走訪問了I。這就是深度優先遍歷的思路:從圖中某個頂點v出發,只要它存在沒有被訪問過的鄰接點,就進入該鄰接點,而後以該點進行深度優先遍歷。
仔細觀察你們會感覺到,深度優先遍歷其實很像棧/遞歸。因此想到用遞歸來實現深度優先遍歷。注意實現過程當中沒必要拘泥於上面解釋圖片時的一直往右走這個說法,由於圖的存儲只有畫出圖片對人的觀察來講纔有左和右的意義。代碼以下
/* 對於使用鄰接矩陣存儲的圖的深度優先遍歷 */
//標識結點是否被訪問過 private boolean[] visited; //深度優先遍歷操做入口 public void DFSTraverse(){ this.visited = new boolean[getNumVertex()]; for (boolean bool : visited){ bool = false; } //若該頂點沒被訪問過,則從該頂點爲起點深度優先遍歷,若爲連通圖,則只會執行一次DFS for (int i = 0; i < getNumVertex(); i++){ DFS(i); } } //深度優先遍歷算法 private void DFS(int i) { visited[i] = true; System.out.println(vertexs[i].getData()); for (int j = 0; j < numVertex; j++){ if ( !visited[j] && (edges[i][j] != 0 || edges[i][j] != INFINITY)){ DFS(j); } } }
//標識結點是否被訪問過 private boolean[] visited; //深度優先遍歷操做入口 public void DFSTraverse(){ this.visited = new boolean[getNumVertex()]; for (boolean bool : visited){ bool = false; } //若該頂點沒被訪問過,則從該頂點爲起點深度優先遍歷,若爲連通圖,則只會執行一次DFS for (int i = 0; i < getNumVertex(); i++){ if (!visited[i]) { DFS(i); } } } //深度優先遍歷算法 private void DFS(int i) { visited[i] = true; System.out.println(vertexs[i].getData()); if (!(vertexs[i].getFirstEdge() == null)) { EdgeL e = vertexs[i].getFirstEdge(); //取到鄰接表的第一個元素 while (e != null) { //鄰接表不爲空 if (!visited[e.getAdjvex()]) { //若是該元素沒有被訪問過,則以該元素爲起點再次進行深度優先遍歷;若是訪問過,則取到鄰接表的下一個結點(能夠理解爲往右走走不通,變成往左走) DFS(e.getAdjvex()); } e = e.getNextEdge(); } } }
上面是兩種存儲方式的深度優先遍歷的遞歸寫法,咱們能夠看出鄰接矩陣的DFS的時間複雜度是O(n2)而鄰接表的DFS是O(n+e)因此當點多邊少的稀疏圖時,鄰接表結構在深度優先遍歷上的時間效率大大提升。
衆所周知,遞歸算法在數據量過大時容易引發棧溢出等問題,因此下面咱們來看一下DFS的非遞歸寫法
以下是鄰接矩陣,DFS非遞歸寫法(ArrayStack是我在前面關於棧的博客中實現的一個簡單鏈棧demo)
//標識結點是否被訪問過 private boolean[] visited_2; /** * 利用棧來實現,若是該頂點被訪問,則壓棧; * 當走到死衚衕,查詢棧頂元素是否有其餘未被訪問的鄰接點,若是有,則訪問它,並壓棧,若是沒有,則將棧頂元素彈棧,直到棧爲空。 */ public void DFSTraverse_2(){ this.visited = new boolean[numVertex]; for (int i = 0; i < numVertex; i++){ visited[i] = false; } ArrayStack<Integer> s = new ArrayStack<Integer>(); int i = 0; visit(i); s.push(i); while (!s.isEmpty()){ int j = 0; int top = s.getTop(); for (; j < numVertex; j++){ if ( !visited[j] && (edges[top][j] != 0 || edges[top][j] != INFINITY)){ visit(j); visited[j] = true; s.push(j); break; } } if (j == numVertex){ s.pop(); } } } private void visit(int i){ System.out.println(vertexs[i].getData()); visited[i] = true; }
如下是鄰接表DFS非遞歸寫法。
//標識結點是否被訪問過 private boolean[] visited_2; //深度優先遍歷操做入口 public void DFSTraverse_2(){ this.visited = new boolean[getNumVertex()]; for (int i = 0; i < numVertex; i++){ visited[i] = false; } for (int i = 0; i < numVertex; i++) { ArrayStack<Integer> s = new ArrayStack<>(); visit(i); s.push(i); while (!s.isEmpty()) { int topIndex = s.getTopData(); EdgeL p = vertexs[topIndex].getFirstEdge(); //遍歷鄰接表,直到找到鄰接表中沒有被訪問過的結點 while (p != null) { if (!visited[p.getAdjvex()]) { visit(p.getAdjvex()); s.push(p.getAdjvex()); } else if(p.getNextEdge() != null && visited[p.getNextEdge().getAdjvex()] == false){ p = p.getNextEdge(); } } if (p == null) { s.pop(); } } } } public void visit(int i){ System.out.println(vertexs[i].getData()); visited[i] = true; }
2,廣度優先遍歷(Breadth_First_Search BFS)
若是說圖的深度優先遍歷相似於樹的前序遍歷,那麼廣度優先遍歷就相似於樹的層序遍歷。
咱們把上左圖調整一下位置,造成有層間關係的相似樹的結構。而後以隊列的形式,當一個元素被遍歷,則將它出隊的同時,將它的未被遍歷的鄰接結點入隊,直到隊列中的所有元素都被遍歷。
代碼以下
/* 鄰接矩陣存儲的圖的廣度優先遍歷 */ public void BFSTraverse(){ ArrayDeque<Integer> queue = new ArrayDeque<>(); this.visited = new boolean[getNumVertex()]; for (int i = 0; i < numVertex; i++){ visited[i] = false; } for(int i = 0; i < numVertex; i++){ if (!visited[i]) { queue.add(i); } while (!queue.isEmpty()){ int row = queue.remove(); visit(row); for (int j = 0; j < numVertex; j++){ //若是存在這條邊,且這條邊的鄰接點沒有被訪問過,且鄰接點不在隊列中,則將該鄰接點入隊 if ((edges[row][j] != 0 && edges[row][j] != INFINITY) && !visited[j] && !queue.contains(j)){ queue.add(j); } } } } } private void visit(int i){ System.out.println(vertexs[i].getData()); visited[i] = true; } }
/* 鄰接表存儲的圖的廣度優先遍歷 */ public void BFSTraverse(){ ArrayDeque<Integer> queue = new ArrayDeque<>(); this.visited = new boolean[getNumVertex()]; for (int i = 0; i < getNumVertex(); i++){ visited[i] = false; } for (int i = 0; i < getNumVertex(); i++){ if (!visited[i]) { queue.add(i); } while (!queue.isEmpty()){ int node = queue.remove(); visit(node); EdgeL e = vertexs[node].getFirstEdge(); if (e != null && !queue.contains(e.getAdjvex()) && !visited[e.getAdjvex()]) { queue.add(e.getAdjvex()); while (e != null && e.getNextEdge() != null && !queue.contains(e.getAdjvex()) && !visited[e.getAdjvex()]) { queue.add(e.getAdjvex()); e = e.getNextEdge(); } } } } } public void visit(int i){ System.out.println(vertexs[i].getData()); visited[i] = true; }
對比發現,DFS和BFS在時間複雜度上是同樣的,僅僅是訪問次序不一樣。深度優先更適合目標明確,以找到目標爲目的的狀況;廣度優先更適合在不斷擴大遍歷範圍時找到相對最優解的狀況。
下面應該是最小生成樹這一部分,可是研究了一天,發現大話數據結構這本書的圖這部分寫的實在是爛,難如下嚥,因此後面將再起一篇博客,來記錄後續部分,將會以Sedgewick版《算法》的風格來敘述。若是有人看到博客對後續有期待,請等我幾天。