圖演算法 - 只需“五步” ,獲取兩節點間的所有路徑(非遞迴方式)
在實現 “圖” 資料結構時,會遇到 “獲取兩點之間是所有路徑” 這個演算法問題,網上的資料大多都是利用遞迴演算法來實現(見文末的參考文章)。
我們知道在 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
進行到這裡,我們停下來觀察一番,發現主棧裡的內容已經是一條完整的從 v3
到 v6
的路徑了:
獲取一條從 v3 到 v6 的路徑
我們輸出當前棧為陣列:['v3', 'v1', 'v0', 'v2', 'v5', 'v6']
,該陣列就表示 v3 -> v1 -> v0 -> v2 -> v5 -> v6
這條路徑。
進行至此,我們終於獲取了一條從 v3
到 v6
的路徑。
應該為自己的努力鼓個掌,已經看到勝利的曙光;接下來加個簡單的迴圈就能獲取所有的路徑。
Step 5: 獲取所有路徑
重複 Step 2 - Step 4 步驟,採取策略如下:
- 只要輔棧棧頂是非空列表,我們就建棧
- 只要輔棧棧頂是空列表,我們就削棧
- 只要主棧棧頂是目標節點,我們輸出路徑,同時削棧
重複以上過程,直到主棧為空為止。
隨著 建棧(build stack) 和 削棧(cutdown stack) 過程的進行,主棧和輔棧不斷變化著,在這個變化的過程中我們就能不斷地獲取從 v3
到 v6
的路徑,最終就可以獲取所有的路徑。
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、總結
最近在複習 “圖” 這資料結構,在過程中逐步嘗試書寫程式碼去實現箇中演算法。能夠體會得到知識點只有經過自己思考和總結後,才能為之後的融會貫通打下基礎。
在本文的學習總結中,有兩點體會印象較為深刻:
- 能用能遞迴解決的問題,一般都可以用 迴圈 + 棧(Stack) 的方式來解決。
- 當不知道演算法如何實現的時候,比較適合歸納總結的學習方法,即先逐步從簡單場景開始演示,等摸索到其中規律之後再著手去實現。
圖相關的演算法還有很多,有很多經典演算法,後續有空會將一些經典的演算法實現並整理出來,互有裨益。
參考文章
- 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) 知識和資訊,歡迎掃碼關注交流。
個人微信公