第五章:圖(圖的遍歷操作)
1.圖的遍歷
圖的遍歷:從圖中某一頂點除法,按照某種搜索方法沿着圖中的邊對圖中的所有頂點訪問依次且僅訪問一次
其實樹的層次遍歷和圖的廣度優先搜索類似,可以把這個二叉樹看成一個圖
2.廣度優先搜索(BFS)
**廣度優先搜索 **
- 首先訪問起始頂點v
- 接着由v出發依次訪問v的各個 未被訪問過 的鄰接頂點w1,w1…wi
- 然後依次訪問w1,w2…wi的所有 未被訪問過 的鄰接頂點
- 在從這些訪問過的頂點出發,訪問它們所有 未被訪問過 的鄰接頂點
- 以此類推
如上圖,它的 廣度優先搜索遍歷
,我們如果按照 樹的層次遍歷
這種方式進行遍歷它的過程如下:
首先 1,然後訪問它的所有鄰接頂點即 2,3,然後分別訪問 2 和 3 的鄰接頂點,2 的 4 和5 ,3 的 6 ,接着我們從頂點 4 出發依舊訪問它的所有鄰接頂點即 7 和 5,此時我們就會法生錯誤,因爲頂點 5 我們已經訪問過了,再訪問就不符合 **廣度優先搜索 ** 的要求了, **所以我們遍歷的時候不能完全按照樹的層次遍歷的方式進行遍歷圖 ** ,那麼怎麼實現圖的廣度優先搜索呢?
我們知道樹的層次遍歷我們藉助了一種特殊的數據結構:隊列
那麼圖我們不僅僅要依靠隊列還要依靠一個輔助標記數組即: 隊列+輔助標記數組
我們依舊使用上面的圖,首先我們需要初始化,即將輔助標記數組中的值全部置爲0,即0表示未被訪問,1表示已被訪問。重新進行遍歷:從 1 出發,首先 1 入隊,接着修改 v[0]=1
,接着出隊隊首元素 1 並訪問,接着我們需要將 1 的鄰接頂點 2 和 3 一次入隊,接着修改 v[1]=1,v[2]=1
,然後出隊隊首元素 2 並進行訪問,接着我們需要將頂點 2 的鄰接頂點分別 1,4,5進行入隊,這裏我們其實只入隊了 4 和 5,接着修改 v[3]=1,v[4]=1
,這裏有一個判斷過程就是利用這個輔助標記數組,接着出隊隊首隊首元素 3 並訪問,然後將頂點 3 鄰接的頂點並且未入隊的頂點進行入隊操作,即頂點 6 入隊,同時修改a[5]=1
,接着出隊隊首元素 4 並訪問,然後入隊 頂點 4 的鄰接的頂點並且未入隊的頂點 7 ,同時修改 v[6]=1
,接着出隊隊首元素 5 並訪問,接着出隊隊首元素 7 並訪問,接着出隊隊首元素 7 並訪問,遍歷完成。此時數組中所有頂點的值都是1.
代碼實現
bool visited[MAX_TREE_SIZE]
void BFSTraverse(Graph G){
for(int i=0; i<G.vexnum;i++){
visited[i]=FALSE;
}
InitQueue(Q);
//for循環的作用,我們上面講的是一個連通圖,所有頂點都可以通過一個頂點依次進行訪問
//但是如果一個圖不是連通的,我們需要遍歷所有頂點
for(int i=0;i<G.vexnum;i++){
if(!visited(i)){
BFS(G,i);
}
}
}
//廣度優先搜索
void BFS(Graph G,int V){
visit(v); //訪問
visited=TRUE; //TRUE 入隊了,FALSE未入隊
EnQueue(Q,v); //將結點入隊
while(!isEmpty(Q)){ //判斷隊列是否是空
DeQueue(Q,v); //出隊隊首元素,並賦值到v中
//FirstNeighbor 求圖G中頂點x的第一個鄰接頂點,存在返回頂點號,不存在返-1
//判斷 w是否大於0,如果是-1則說明沒有鄰接頂點了
//NextNeighbor 求圖G中頂點v的的下一個鄰接頂點並賦值給w
for(int w=FirstNeighbor(G,v);w>0;w=NextNeighbor(G,v,w)){
if(!visited[w]){//判斷是否入隊過
visit[w];
visited[w]=TRUE;
EnQueue(Q,w);
}
}
}
}
3.應用
3.1無權圖單源最短路徑問題
定義從頂點u到頂點v的最短路徑d(u,v)爲從u到v的任何路徑中最少的邊數
,若從u到v沒有通路,則d(u,v)=∞(表是不可達到)
//和廣度優先搜索相似,增加了保存最短路徑的一個數組
void BFS_MIN_Distance(Graph G,int u){//傳入圖 和 初始頂點
for(int i=0;i<G.vexnum;++i){ //
d[i]=MAX; //保存最短路徑的值,我們初始化爲最大值
}
visited[u]=TRUE;//標識爲該頂點已經入隊
d[u]=0;//初始頂點路徑值改爲0
EnQueue(Q,u);//入隊
while(!isEmpty(Q)){//判斷隊列時候爲空
DeQueue(Q,u);//出隊隊首元素
//FirstNeighbor 求圖G中頂點x的第一個鄰接頂點,存在返回頂點號,不存在返-1
//判斷 w是否大於0,如果是-1則說明沒有鄰接頂點了
//NextNeighbor 求圖G中頂點v的的下一個鄰接頂點並賦值給w
for(int w=FirstNeighbor(G,u);w>0;w=NextNeighbor(G,u,w)){
if(!visit[w]){//判斷該頂點是否已經被訪問過
visited[w]=TRUE;//設置被訪問過
//d[u]表示到初始頂點的最短路徑,w是它的鄰接點,所以+1等目前w到初始頂點的最短路徑
d[w]=d[u]+1;
EnQueue(Q,w);//入隊
}
}
}
}
3.2廣度優先生成樹
廣度優先生成樹:在廣度遍歷過程中,我們可以得到一顆遍歷樹,稱爲廣度優先生成樹(生成森林)
如果是一個連通圖我們會得到是一顆生成樹,而如果是非連通圖我們得到的是生成森林
連通圖
:任意兩個結點都是連通的
鄰接矩陣法的廣度優先生成樹是唯一的,鄰接表法的不唯一
因爲一個圖的鄰接矩陣表示是唯一的,所以我們在進行遍歷的過程也是唯一的,但是鄰接表表示法中我們輸入的次序不唯一生成的邊表就不唯一,對應遍歷的過程(遍歷邊的次序)就不唯一了
4.深度優先搜索(DFS)
廣度優先搜索和樹層次遍歷比較類似,而深度優先搜索和樹先序遍歷比較類似,如果把這樣的一個樹看成一個圖,它的先序遍歷順序就是圖的深度優先搜索遍歷順序
我們可以發現廣度優先搜索是按照圖的寬度範圍進行遍歷,而深度優先搜索是按照一條路徑的深度的走向進行搜索遍歷的
深度優先搜索(DFS)
- 首先訪問起始頂點v
- 接着由v出發訪問v的
任意
一個鄰接且未被訪問
的鄰接頂點Wi - 然後再訪問與Wi鄰接且
未被訪問
的任意頂點Yi - 若Wi沒有鄰接且未被訪問的頂點時,退回到它的上一層頂點v
- 重複上述過程,直到所有頂點被訪問爲止
我們通過上面的算法思想遍歷一遍上圖:首先訪問 1 ,接着可以任意訪問頂點 2 或 3,我們訪問 2,接着我們可以 訪問任意頂點 4 或 5,我們訪問 4 ,然後我們可以訪問任意的頂點 7 或者 5 (7和5也是4的鄰接頂點),假設我們訪問5,然後 5 沒有鄰接且未被訪問的頂點我們退回到 4 ,接着訪問頂點 7,接着退回到頂點 1 ,然後訪問頂點 3 ,接着訪問 6,遍歷完成:1 2 4 5 7 3 6
我們從上面的遍歷過程可以發現整個過程可以使用遞歸
來實現,當然遞歸也可以轉換成棧
來實現,同時我們也需要一個輔助標記數組。即:遞歸(棧)+輔助標記數組
代碼實現
bool visited[MAX_TREE_SIZE]//輔助標記數組
void DFSTraverse(Graph G){
for(int i=0;i<G.vexnum;i++){
visited[i]=FALSE;//初始化所有結點都未必訪問
}
for(int i=0;i<G.vexnum;i++){
if(!visited[i]){//如果結點未被訪問
DFS(G,i);//G:圖,i:起始頂點的編號
}
}
}
void DFS(Graph G,int v){
visit(v); //訪問
visited[v]=TRUE;//置爲訪問過
for(int w=FirstNeighbor(G,v);w>0;w=NextNeighbor(G,v,w)){
if(!visit[w]){//判斷該頂點是否已經被訪問過
DFS(G,w);//遞歸
}
}
}
如上圖,我們通過上面的代碼來遍歷一遍:從A出發,訪問A並把A對應的輔助標記數組值設置爲TREUE
,接着我們找A的第一個鄰接頂點比如是C,然後我們判斷C沒有被訪問過,接着繼續調用DFS
函數,訪問C,同理繼續找C的第一個鄰接頂點D,判斷D沒有被訪問過,我們訪問頂點D,然後D沒有鄰接頂點此函數結束,我們退回到訪問C的頂點的DFS
的for循環中,找到C的第二個鄰接點E,判斷E沒有被方問過,我們訪問E,接着E沒有鄰接頂點,繼續退回到C,C沒有未被訪問過的鄰接頂點,我們退回到A的DFS
的for循環中,發現A的第二個鄰接頂點E被訪問過了,所以A也沒有未被訪問過的鄰接頂點了,我們退回到了DFSTraverse
函數的第二個for循環中,循環判斷到B發現B爲被訪問,然後調用DFS
函數,訪問D,設爲TRUE
,然後發現B不存在未被訪問的鄰接頂點,所以退回到DFSTraverse
函數的第二個for循環中,發現沒有未被訪問過的頂點了,所以遍歷結束:ACDEB
從此過程可以看出第一個函數的作用就是如果我們的初始頂點無法完成遍歷圖中的所有頂點我們就可以通過循環遍歷每一個頂點。
鄰接矩陣法的DFS(BFS)序列唯一,鄰接表法的不唯一
5.深度優先生成樹
深度優先生成樹:在深度遍歷過程中我們可以得到一顆遍歷樹,稱爲深度優先生成樹(生成森林)
鄰接矩陣法的深度度優先生成樹是唯一的,鄰接表法的不唯一
6.遍歷與連通性問題
如何通過遍歷來判斷該圖的連通性?
上面是一個無向圖:無論我們使用BFS還是DFS都能通過任何一個頂點訪問到其他的頂點,所以他是一個連通圖
所以我們有以下結論:在無向圖中,在任意結點除法進行一次遍歷(調用一次BFS或者DFS),若能訪問全部結點,說明該圖是連通的。
上面是一個非連通的無向圖,我們在進行遍歷(BFS或DFS)的時候爲了遍歷到每一個頂點,我們需要一個for循環對每一個頂點進行調用BFS或者DFS。
我們由此可以得到如下結論:在無向圖中,調用遍歷函數(BFS或DFS)的次數爲連通分量的個數
如上面是一個有向圖:我們從頂點B出發DFS可以遍歷到任何頂點,但是能訪問到所有頂點代表這個圖是一個強連通圖嗎?答案當然是不是的,能遍歷到所有頂點只能說明從某個頂點到另一個頂點有一條有向突擊。
如果上圖我們從頂點A開始遍歷,我們則需要調用兩次DFS,所以在有向圖中,調用遍歷函數(BFS或DFS)的次數爲不是強連通分量的個數
無向圖叫連圖,有向圖叫強連通
7.理木客
數據結構相關知識,公衆號理木客同步更新中,歡迎關注