1. 程式人生 > >資料結構——圖(5)——深入分析DFS演算法

資料結構——圖(5)——深入分析DFS演算法

對DFS的過程分析

在前面的文章中我們提到了這樣的一幅圖:
在這裡插入圖片描述

我們知道,在DFS中,我們採用的是遞迴的方式進行實現的,並且給每一個遍歷過的點都做上了標記,目的是為了防止程式進入死迴圈。(為什麼樹可以不需要呢?因為樹沒有環)
利用之前專欄提到的遞迴模式,我們可以寫出下面的虛擬碼:


dfs from v1 to v2:
    base case: if at v2, found!  //基礎事件,假設v1就是v2
    mark v1 as visited. //否則,標記V1
    for all edges from v1 to its neighbors: //遍歷V1節點的所有鄰居
    //如果它的鄰居未曾被訪問,那麼對該節點遞迴呼叫dfs
         if neighbor n is unvisited, recursively call dfs(n, v2)

如果我們用true或者false來表示節點是否被訪問,那麼假設從H出發訪問C節點,初始狀態應該是這樣的:

在這裡插入圖片描述
而呼叫棧此時應該是這樣的:
在這裡插入圖片描述
根據按字母表順序優先訪問的規則此時,訪問的下一個節點應該是節點E,此時對應的內容為:
在這裡插入圖片描述
在這裡插入圖片描述
如此繼續,直到呼叫到頂點G的時候,已經沒有未標記的路可以走了,此時開始回溯:
在這裡插入圖片描述
在這裡插入圖片描述
回溯的過程就是將函式調用出棧的過程:
在這裡插入圖片描述
回到節點E,繼續呼叫dfs演算法:
在這裡插入圖片描述
此時找到H->C的路徑。

DFS的棧實現(非遞迴演算法的實現)

虛擬碼如下

dfs from v1 to v2:
    create a stack, s //建立一個空的棧s
    s.push(v1) //將起始節點入棧
    while s is not empty://當棧不為空時
        v = s.pop() //將棧中的元素彈出,並賦值給節點型別的變數V
        if v has not been visited://如果v沒有被訪問
             mark v as visited//標記V
             //將所有v節點的鄰居入棧
             push all neighbors of v onto the stack 

我們就用上面的步驟繼續分析一下DFS的具體資料在棧中的情況(從H到C)

  1. 一開始我們建立一個空的棧,將節點h入棧,此時棧中只有元素H:
    在這裡插入圖片描述
  2. 此時的棧不為空了, 在while迴圈中執行賦值語句後,v = h,此時h節點尚未被標記,於是標記點h,並且將h的所有鄰居節點入棧,(為什麼G節點不是?因為這是有向圖);
    在這裡插入圖片描述
  3. 同樣這裡因為F是棧頂,所以我們先彈出的是節點F,同樣執行上述操作,F的鄰居是C,所以把C入棧,即如圖所示:

在這裡插入圖片描述
4. 在下一次迴圈中 我們發現
在這裡插入圖片描述
棧中彈出的值恰好就是我們要尋找的值,於是停止搜尋。也就是說這樣的效率比遞迴要高:
在這裡插入圖片描述

DFS的遞迴和迭代解決方案都是正確的,但由於遞迴與使用堆疊的細微差別,它們以不同的順序遍歷節點。

對於h到c的例子,迭代解決方案碰巧更快,但對於不同的圖,遞迴解決方案可能更快。

總結:

在給定節點處呼叫DFS可以查詢從該節點開始所有可到達的節點。(由此判斷圖是否為連通的)
如果我們有一個鄰接列表,在n個節點,m條邊的圖中執行DFS,需要時間O(m + n),空間O(n)