算法分析與設計:圖的搜索算法

一、圖的兩種基本遍歷

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距離短(經過定點少)的頂點更優先被遍歷,就好像根據當前頂點的寬廣度(鄰接的頂點數)向外擴散一樣。

爲使算法對非連通圖也有效,我們可以在每次搜索後檢查頂點情況,將未訪問的頂點作爲下一個源頂點繼續搜索。
爲實現廣度優先搜索,我們可以使用隊列來保存需要搜索的頂點。
爲避免對頂點的重複遍歷,另設一個數組,標記頂點是否被遍歷過。

算法描述如下:

  1. 給定一個圖和起始頂點,標記起始頂點併入隊列
  2. 從隊列中彈出一個頂點,並查找其所有的鄰接頂點;被查找到的頂點若已被標記,則捨棄;否則標記併入隊列
  3. 重複步驟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,需要一個數組標記頂點是否被遍歷過。

算法描述如下:

  1. 給定一個圖和起始頂點,標記起始頂點併入棧(入遞歸函數)
  2. 找到棧頂頂點的下一個鄰接點:若未訪問,標記併入棧(遞歸);否則捨棄
  3. 若頂點沒有下一個鄰接點,彈出頂點,回溯至上一頂點
  4. 重複步驟2,直到棧爲空

● 算法實現

  1. 遞歸方式
//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);
    }
}
  1. 棧方式
    在棧方式中,需要另設一個數組,標記各個棧內元素當前搜索的位置
//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網絡,將活動的優先次序以線性方式列出來的過程。
專業課程學習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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章