1. 程式人生 > >基本圖演算法之圖的搜尋

基本圖演算法之圖的搜尋

介紹完圖的儲存結構以及如何從外部讀取txt檔案來建立一個圖的內容,我們開始介紹關於圖的入門的一個演算法——圖的搜尋,也就是我們常說的圖的遍歷,與(二叉)樹的遍歷方式(先序遍歷、中序遍歷、後序遍歷、層次遍歷)類似,首先圖的遍歷有兩種方式 廣度優先遍歷(BFS)、深度優先遍歷(DFS)。  關於基本的[DFS](https://en.wikipedia.org/wiki/Depth-first_search)和[BFS](https://en.wikipedia.org/wiki/Breadth-first_search)的思路不再進行介紹,這裡主要記載我在編寫時遇到的問題。  在我剛剛看完關於圖的兩種搜尋(遍歷)演算法時,說這兩種方法的時間複雜度均為O(v+e),也就相信了兩種演算法在實現後跑相同的圖集的時候,耗時應該是相差不多的,這種差距當然在跑一些只有一萬頂點、一萬邊的圖中不會看出來,因為耗時都很短,但是當我使用大的資料集進行測試的時候發現兩種演算法之間的時間使用還有不小的差距的,下面通過我前期寫和程式碼來分析,為什麼會有這樣的情況發生。  我們先聊下DFS,DFS的實現方式我們分為兩種,遞迴式和非遞迴式,遞迴則是比較簡單,也很容易實現,如下:
 void Graph::DFS_Sin(int v){
    Edge* e;
    head[v].statu=true;
    e=head[v].next;
    while(e!=NULL){
        if (!head[e->vName].statu)
        {
            DFS_Sin(e->vName);
        }
        e=e->nextEdge;
    }
}
 理論上使用遞迴的方式速度要慢於非遞迴的方式的,但是當我寫完成之後發現非如此,非但不比遞迴的方式要慢,還慢的出奇。。。。。很明顯是出現了錯誤,下面貼出程式碼:
void
Graph::DFS(){ stack <int> st; for(int i=0;i<vex_num;++i){ if (head[i].statu==false) { st.push(i); head[i].statu=true; Edge* temp; while(!st.empty()){ temp=head[st.top()].next;/*這種寫法的時間消耗必定要長; 主要是就因為我們從棧中出來之後都是從當前頂點撒向的第一條邊開始掃描的, 也就是說每條邊我們都要訪問多次,不僅僅是一次, 所以在實際跑程式的時候消耗的時間要長的多久*/
while(temp!=NULL && head[temp->vName].statu==true) temp=temp->nextEdge; if (temp==NULL) { st.pop(); }else{ head[temp->vName].statu=true; st.push(temp->vName); } }//while;
}//if; } }
 以上標出了問題出現的地方,也就是說,如果使用遞迴的形勢進行深度搜索時,我們每個邊只搜尋了一次,但是使用我那樣的錯誤方法寫時,對每條邊的搜尋次數將大大大的增加,所以對於處理大的圖的時候,這種寫法總是不能在可接受時間內完成圖的完整遍歷。那我們應該如何寫出DFS的非遞迴方式,其實也是在
  procedure DFS-iterative(G,v):
      let S be a stack
      S.push(v)
      while S is not empty
            v = S.pop()
            if v is not labeled as discovered:
                label v as discovered
                for all edges from v to w in G.adjacentEdges(v) do
                    S.push(w)
 以上演算法是如何解決我的那個問題的呢?我是將頂點入棧的同時進行訪問標記,而正確的寫法則採用了出棧的時候才進行標記。同時,與遞迴的方法相比,此種非遞迴的方式是先訪問與頂點相連的最後一條邊,而遞迴的方式則是先訪問第一條邊,下面引用原文,出自wikipedia: >These two variations(注:指的是遞迴方式和非遞迴方式) of DFS visit the neighbors of each vertex in the opposite order from each other: the first neighbor of v visited by the recursive variation is the first one in the list of adjacent edges, while in the iterative variation the first visited neighbor is the last one in the list of adjacent edges. The recursive implementation will visit the nodes from the example graph in the following order: A, B, D, F, E, C, G. The non-recursive implementation will visit the nodes as: A, E, F, B, D, C, G. The non-recursive implementation is similar to breadth-first search but differs from it in two ways: it uses a stack instead of a queue, and it delays checking whether a vertex has been discovered until the vertex is popped from the stack rather than making this check before pushing the vertex. Note that this non-recursive implementation of DFS may use O(|E|) space in the worst case, for example on a complete graph.  BFS的方法也較為簡單,也沒有遇到什麼疑問,所以不在佔用篇幅來貼寫程式碼,後期會將全部程式碼上傳至[github](https://github.com/tianzhuwei)。  知道兩種方法的程式碼實現之後來分析,為什麼兩者(DFS,BFS)兩種方式的時間差距會這麼大(測試的圖為無向圖),如下圖1:

按照正常來說,時間不可能相差太多,下面進行下程式碼的排查!!!
 寫到這裡不知道是不是自己程式碼存在的錯誤呢?因為資料集比較大,不可以一一測試,所以接下來找同學一起來測試一下相同的資料,看看時間消耗如何,測試結果後續更新!今天暫且到這裡^_^

更新

特意測試儀了一下BFS中使用自己定義的佇列與使用STL中queue 的速度使用的圖為有向圖和無向圖,下面上圖:

會發現自己定義的佇列確實要比使用STL中的queue要快,之後再將DFS中棧進行自定義進行比較,看是否有同樣的影響。

下面又測試了一下DFS使用遞迴方式和非遞迴方式的時間比較,上圖:

很淡疼的在處理有向圖資料時非遞迴的方式居然比遞迴的方式還要慢,問題出在哪裡??
稍後會再列出使用自己定義的棧和呼叫STL中的棧實現DFS時的時間比較圖:

最後得出的結論應該不是我的程式碼存在錯誤,DFS的搜尋速度確實要快於BFS,但是佔用的空間也要大,基本上符合“用空間換取時間的操作”下面再上一張最後DFS和BFS比較:

這裡找到一個比較好STL容器與自定義陣列的帖子,上一個關鍵截圖

額額這個行文的思路很亂,因為完全是遇到什麼問題再雲解決,以後注意!