一、圖的兩種基本遍歷
1. 鄰接矩陣與鄰接表
圖的儲存方式通常有兩種:鄰接矩陣和鄰接表。
● 鄰接矩陣
最簡單的圖的表示方式。它通過一個二維數組模擬矩陣,來儲存圖的信息。
對於無權圖,矩陣元素a[i][j]標識頂點 i 到頂點 j 的鄰接信息。若爲1則鄰接;爲0則不鄰接。
對於帶權圖,矩陣元素a[i][j]標識頂點 i 到頂點 j 的權值,並通常用 ∞ (INT_MAX) 標誌不鄰接的頂點權值。
在稀疏圖中,鄰接矩陣會造成矩陣空間的大量浪費,這時可以採用壓縮策略(如十字鏈表法)來儲存矩陣。
//鄰接矩陣定義
struct Graph{
int **arcs; //矩陣
int *vexs; //頂點向量
int vernum; //頂點數
int arcnum; //弧數
};
● 鄰接表
爲方便地找到鄰接點,我們將每個頂點的邊用一個單鏈表儲存,並連接在源頂點的頭結點上。所有的頭結點保存在一個一維數組中,這樣就可以得到若干個存有圖內信息的單鏈表。如此得到的整個數據結構叫做鄰接表。
//鄰接表的定義
struct arcNode{ //弧結點的定義
int adjVex; //鄰接的另一個頂點的編號或下標
arcNode *nextArc; //源頂點的下一條弧
};
struct vexNode{ //頂點結點的定義
int number; //頂點編號
arcNode *firstArc; //該頂點的第一條弧
};
struct Graph{
vexNode *vexs; //頂點數組
int vexnum;
int arcnum;
};
● 兩者的比較
優點 | 缺點 | |
---|---|---|
鄰接矩陣 | 適合查找兩點之間是否相連,並得到兩點之間權值 | 尋找某一個點的鄰接頂點需要搜索整行或整列 |
鄰接表 | 容易查找某一個頂點的所有鄰接點 | 判斷頂點間是否相鄰需要搜索一個單鏈表 |
鄰接表易於根據一個頂點,找到它所有鄰接的頂點,因此它很適合用於圖的搜索算法中。下面的算法均使用鄰接表儲存圖。
2. 廣度優先搜索遍歷
● 基本思想
廣度優先搜索簡稱BFS,是從圖中一個源頂點v0出發,找到源頂點所有的鄰接點,然後再對所有找到鄰接頂點,找到它們各自的鄰接頂點。到v0距離短(經過定點少)的頂點更優先被遍歷,就好像根據當前頂點的寬廣度(鄰接的頂點數)向外擴散一樣。
爲使算法對非連通圖也有效,我們可以在每次搜索後檢查頂點情況,將未訪問的頂點作爲下一個源頂點繼續搜索。
爲實現廣度優先搜索,我們可以使用隊列來保存需要搜索的頂點。
爲避免對頂點的重複遍歷,另設一個數組,標記頂點是否被遍歷過。
算法描述如下:
- 給定一個圖和起始頂點,標記起始頂點併入隊列
- 從隊列中彈出一個頂點,並查找其所有的鄰接頂點;被查找到的頂點若已被標記,則捨棄;否則標記併入隊列
- 重複步驟2,直到隊列爲空
● 算法實現
//BFS
void traverseBFS(Graph G, int start, bool *isVisited) //利用隊列搜索
{
queue<int> q;
q.push(start);
isVisited[start] = true;
visit(start); //遍歷時期望做的操作
while(!q.empty()){
int v = q.front();
q.pop();
arcNode *p = G.vexs[v].firstArc;
while(p != nullptr){
if(!isVisited[p->adjVex]){
visit(p->adjVex);
isVisited[p->adjVex] = true;
q.push(p->adjVex);
}
p = p -> nextArc;
}
}
}
void BFS(Graph G) //BFS函數,可以遍歷非連通圖
{
bool isVisited[G.vexnum] = {false};
for(int i = 0;i < G.vexNum;i++){ //檢查頂點
if(!isVisited[i])
traverseBFS(G,i,isVisited);
}
}
● 時間複雜度
設頂點數爲V,弧數爲E。BFS中for循環需要執行V次,而traverseBFS中最內層的語句需要執行E次(搜索整個鄰接表)。
因此廣度優先搜索的時間複雜度爲O(V+E)。
3. 深度優先搜索遍歷
● 基本思想
深度優先搜索簡稱DFS,是從圖中一個源頂點V0出發,找到它的一個鄰接點,若鄰接點未被訪問過,則進入該鄰接點,並繼續找它的鄰接點;若一個點的鄰接點全部被訪問過,則回溯到上一頂點,找它的下一鄰接點。這一算法的過程就好像往圖的深處搜索一樣。
爲實現深度優先搜索,可以使用棧來保存頂點,也可以採用遞歸的方式。
類似於BFS,需要一個數組標記頂點是否被遍歷過。
算法描述如下:
- 給定一個圖和起始頂點,標記起始頂點併入棧(入遞歸函數)
- 找到棧頂頂點的下一個鄰接點:若未訪問,標記併入棧(遞歸);否則捨棄
- 若頂點沒有下一個鄰接點,彈出頂點,回溯至上一頂點
- 重複步驟2,直到棧爲空
● 算法實現
- 遞歸方式
//DFS Rec
void traverseDFSRec(Graph G,int v,bool *isVisited) //遞歸函數
{
isVisited[v] = true;
visit(v);
arcNode *p = G.vexs[v].firstArc;
while(p != nullptr){
if(!isVisited[p->adjVex])
traverseDFSRec(G,p->adjVex,isVisited);
p = p->nextArc;
}
}
void DFS(Graph G) //DFS函數
{
bool isVisited[G.vexnum] = {false};
for(int i = 0;i < G.vexnum;i++){ //檢查頂點
if(!isVisited[i])
traverseDFS(G,i,isVisited);
}
}
- 棧方式
在棧方式中,需要另設一個數組,標記各個棧內元素當前搜索的位置
//DFS Stack
void traverseDFS(Graph G,int start, bool *isVisited) //棧方式深度優先搜索
{
stack<int> s;
arcNode *iterators[G.vexnum];
for(int i = 0;i < G.vexnum;i++){ //初始化迭代器數組
iterators[i] = G.vexs[i].firstArc;
}
s.push(start);
isVisited[start] = true;
visit(start);
while(!s.empty()){
int v = s.top();
if(iterators[v] != nullptr){ //迭代器不爲空,棧頂元素可找到下一鄰接點
int adj = iterators[v]->adjVex;
if(!isVisited[adj]){ //鄰接點未訪問
s.push(adj);
isVisited[adj] = true;
visit(adj);
}
iterators[v] = iterators[v]->nextArc;
}
else{ //迭代器爲空,彈出棧頂元素
s.pop();
}
}
}
void DFS(Graph G) //DFS函數
{
bool isVisited[G.vexnum] = {false};
for(int i = 0;i < G.vexnum;i++){ //檢查頂點
if(!isVisited[i])
traverseDFSRec(G,i,isVisited);
}
}
● 時間複雜度
設頂點數爲V,弧數爲E。DFS中for循環需要執行V次。對於遞歸方式和棧方式,需要while循環內語句共執行E次。
因此深度優先搜索的時間複雜度爲O(V+E)。
二、典型問題
有向無環圖的拓撲排序
● 問題描述
不存在回邊的有向圖稱爲有向無環圖,簡稱DAG圖。
DAG圖有特殊的一類AOV網,其定義爲:頂點表示活動,弧表示活動間的優先關係的有向無環圖。
AOV網常用於工程的計劃和管理等方面。要判斷工程能否有效運行,就是要求解拓撲排序。
所謂拓撲排序,就是分析AOV網絡,將活動的優先次序以線性方式列出來的過程。
● 基本思想
拓撲排序有兩種處理的辦法:一是每次在圖中查找入度爲0的頂點;二是利用深度優先得到一個反向的拓撲順序。
● 算法實現
//方法一:查找入度爲0的頂點
int* topLogicalSort(Graph G)
{
bool isVisited[G.vexnum] = {false};
int inDeg[G.vexnum] = {0};
//獲取入度
for(int i = 0;i < G.vexnum;i++){
arcNode* p = G.vexs[i].firstArc;
while(p != nullptr){
inDeg[p->adjVex]++;
p = p->nextArc;
}
}
int* ans = new int[G.vexnum];
int index = 0;
//開始排序
while(index < G.vexnum){
int cur = -1;
for(int i = 0;i < G.vexnum;i++){ //找到入度爲0的頂點
if(!isVisited[i] && inDeg[i] == 0){
cur = i;
break;
}
}
if(cur == -1) //排序不可推進,說明存在環
return nullptr;
ans[index] = cur; //寫入答案
++index;
isVisited[cur] = true;
arcNode* p = G.vexs[cur].firstArc;
while(p != nullptr){ //將寫入的點刪除,更新入度
inDeg[p->adjVex]--;
p = p->nextArc;
}
}
return ans;
}
//方法二:深度優先
int* topLogicalSortDFS(Graph G)
{
bool isVisited[G.vexnum] = {false};
arcNode *iterators[G.vexnum];
for(int i = 0;i < G.vexnum;i++){ //初始化迭代器數組
iterators[i] = G.vexs[i].firstArc;
}
int *ans = new int[G.vexnum];
stack<int> s;
int index = G.vexnum - 1;
for(int i = 0;i < G.vexnum;i++){
if(!isVisited[i]){
isVisited[i] = true;
s.push(i);
while(!s.empty()){
int v = s.top();
if(iterators[v] != nullptr){
int adj = iterators[v]->adjVex;
if(!isVisited[adj]){
s.push(adj);
isVisited[adj] = true;
}
iterators[v] = iterators[v]->nextArc;
}
else{ //結束點
s.pop();
ans[index--] = v;
}
}
}
}
return ans;
}