1. 程式人生 > >圖演算法 - 只需“五步” ,獲取兩節點間的所有路徑(非遞迴方式)

圖演算法 - 只需“五步” ,獲取兩節點間的所有路徑(非遞迴方式)

在實現 “圖” 資料結構時,會遇到 “獲取兩點之間是所有路徑” 這個演算法問題,網上的資料大多都是利用遞迴演算法來實現(見文末的參考文章)。

我們知道在 JS 中用遞迴演算法很容易會讓呼叫棧溢位,為了能在生產環境中使用,必須要用非遞迴方式的去實現。

經過一番探索,實現的思路主要來自文章 《求兩點間所有路徑的遍歷演算法》 ,只是該文中並沒有給出具體的實現細節,需要自己去實現;最終本文的實現結合類似《演算法 - 排程場演算法(Shunting Yard Algorithm)》 中所提及的雙棧來完成。

1、演算法過程

以計算下圖為例, 節點 3 到 節點 6 所有路徑所有可能的路徑為 8 條:

allpath

我們具體講一下如何獲取這 8 條路徑的過程。

首先準備兩個棧,分別稱為 主棧 和 輔棧:

  • 主棧:每個元素是單個節點(Vertex),用於存放當前路徑上的節點;
  • 輔棧:每個元素用於存放主棧對應元素的 相鄰節點列表(Vertex Array);該棧是用來輔助 主棧 的,其長度和 主棧 一致;

Step 1: 建棧

v3(節點3)放到主棧,同時將 v3 節點的鄰接節點列表 [v1, v7] 放到輔棧中:

首次建棧

主棧和輔棧壓入讓棧長度增長,我個人稱之為 建棧(build stack)

Step 2: 繼續建棧

建棧後,我們檢視輔棧,其棧頂是節點列表 [v1, v7]

檢視棧頂

我們取出節點列表的第一個元素 v1,將其壓入到主棧;同時將剩下的節點列表 [v7] 重新壓回到輔棧:

壓棧

同時查詢 v1 的鄰接節點列表是 [v3, v0],由於 v3 節點已經在主棧裡,需要從這個列表中剔除(這一步很重要),將剔除後的節點列表 [v0] 壓入 輔棧 中:

繼續建棧

這一步也讓主棧和輔棧長度增長了,所以也是 建棧(build stack) 過程

Step 3: 削棧

繼續 Step 2 的建棧過程,直到我們的主棧棧頂 v7,此時輔棧的棧頂是空列表 []

當主棧是 v7 的時候,輔棧棧頂是空佇列

由於輔棧的棧頂是空列表 [],所以沒法繼續建棧了 —— 這表明這條路徑走到盡頭了都還沒找到目標節點 v6。

走到 此路不通 的境地,我們就需要開始回退,看看來時的路上的其他岔路。

我們將主棧棧頂的 v7 彈出,同時也將輔棧的空列表 [] 彈出:

削棧

這一操作將導致 主棧 和 輔棧 長度減少,該過程我個人稱之為 削棧(cutdown stack)。

Step 4:獲取第一條路徑

重複上述的 Step 2、Step 3,採取策略:

  • 只要輔棧棧頂是非空列表,我們就建棧
  • 只要輔棧棧頂是空列表,我們就削棧

直到主棧的頂部節點是目標節點 v6

主棧棧頂元素是目標元素v6

進行到這裡,我們停下來觀察一番,發現主棧裡的內容已經是一條完整的從 v3v6 的路徑了:

獲取一條從 v3 到 v6 的路徑

我們輸出當前棧為陣列:['v3', 'v1', 'v0', 'v2', 'v5', 'v6'],該陣列就表示 v3 -> v1 -> v0 -> v2 -> v5 -> v6 這條路徑。

進行至此,我們終於獲取了一條從 v3v6 的路徑。

應該為自己的努力鼓個掌,已經看到勝利的曙光;接下來加個簡單的迴圈就能獲取所有的路徑。

Step 5: 獲取所有路徑

重複 Step 2 - Step 4 步驟,採取策略如下:

  • 只要輔棧棧頂是非空列表,我們就建棧
  • 只要輔棧棧頂是空列表,我們就削棧
  • 只要主棧棧頂是目標節點,我們輸出路徑,同時削棧

重複以上過程,直到主棧為空為止。

隨著 建棧(build stack) 和 削棧(cutdown stack) 過程的進行,主棧和輔棧不斷變化著,在這個變化的過程中我們就能不斷地獲取從 v3v6 的路徑,最終就可以獲取所有的路徑。

2、程式碼實現

2.1、虛擬碼

依據上述過程的描述,很方面將文字轉換成虛擬碼:

BEGIN

  初始化主棧
  初始化輔棧

  首次建棧

  WHILE 主棧不為空 THEN

    獲取輔棧棧頂,為鄰接節點列表

    IF 鄰接節點列表不為空 THEN
      獲取鄰接節點列表首個元素
      將該元素壓入主棧,剩下列表壓入輔棧
      建棧
    ELSE
      削棧
      CONTINUE
    END IF

    IF 主棧棧頂元素 === 目標節點 THEN
      獲取一條路徑,儲存起來
      削棧
    END IF

  END WHILE

END

以上是我們拿無向圖來做範例,實際上該演算法也適合有向圖。

2.2、實現效果

該雙棧演算法的 JS 實現已經寫到程式碼庫 ss-graph 中 ,我們直接拿它來做校驗,實際執行效果如下:

可前往 https://runkit.com/boycgit/ss-graph 自行修改資料體驗:

執行實際程式碼,驗證演算法

3、總結

最近在複習 “圖” 這資料結構,在過程中逐步嘗試書寫程式碼去實現箇中演算法。能夠體會得到知識點只有經過自己思考和總結後,才能為之後的融會貫通打下基礎。

在本文的學習總結中,有兩點體會印象較為深刻:

  1. 能用能遞迴解決的問題,一般都可以用 迴圈 + 棧(Stack) 的方式來解決。
  2. 當不知道演算法如何實現的時候,比較適合歸納總結的學習方法,即先逐步從簡單場景開始演示,等摸索到其中規律之後再著手去實現。

圖相關的演算法還有很多,有很多經典演算法,後續有空會將一些經典的演算法實現並整理出來,互有裨益。

參考文章

  • Find if there is a path between two vertices in a directed graph:geeksforgeeks 相關面試題,遞迴實現
  • Print all paths from a given source to a destination:遞迴實現,查詢所有路徑
  • 求兩點間所有路徑的遍歷演算法:較為通俗易懂;,一個儲存路徑的棧、一個儲存已標記結點的數

以下是我的公眾號,會時常更新 JS(Node.js) 知識和資訊,歡迎掃碼關注交流。

個人微信公