跳到主要內容

演算法

同一個問題,不同的解題思路

主題 · BFS 與 DFS

BFS 與 DFS

走遍一張圖的兩種方式:BFS(Breadth-First Search,廣度優先搜尋)用佇列一圈一圈往外擴,DFS(Depth-First Search,深度優先搜尋)用堆疊一路走到底再回頭。換一個資料結構,走法就完全不同。

走法
圖
起點
1/40
ABCDEFGHIJ
正在走在佇列裡等走過了(數字是順序)見過了,跳過
佇列:前 → 後
  1. A
走訪順序
(還沒有)

把起點 A 放進佇列。

走過的點
0 / 10
佇列最多放幾個
4
取出+檢查邊的次數
38
亮起來的是這一步執行的程式碼
function bfs(adj: number[][], start: number): number[] {
const seen = new Set([start]);
const queue = [start];
const order: number[] = [];
for (let head = 0; head < queue.length; head++) {
const u = queue[head];
order.push(u);
for (const v of adj[u]) {
if (seen.has(v)) continue;
seen.add(v);
queue.push(v);
}
}
return order;
}
function dfs(adj: number[][], start: number): number[] {
const visited = new Set<number>();
const stack = [start];
const order: number[] = [];
while (stack.length > 0) {
const u = stack.pop()!;
if (visited.has(u)) continue;
visited.add(u);
order.push(u);
for (let i = adj[u].length - 1; i >= 0; i--) {
if (!visited.has(adj[u][i])) stack.push(adj[u][i]);
}
}
return order;
}

模型假設與範圍

  • Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。

什麼時候用

  • BFS:要「最少幾步」到達(邊沒有權重),例如社群網路的幾度分隔、迷宮最短路、網頁爬蟲一層一層往外抓。
  • DFS:要走到底再回頭的問題,例如找所有連通的區塊、偵測環、拓撲排序、迷宮生成、回溯搜尋。
  • 只想知道「走不走得到」時兩個都可以,選記憶體比較省的那個(見上方比較)。

和其他主題的關係

時間與空間複雜度(Big O)

操作平均最差
BFS
每個點進出佇列一次,每條邊從兩端各看一次
O(V + E)O(V + E)
DFSO(V + E)O(V + E)
用相鄰矩陣存圖時
找鄰居要掃整列,不管實際有幾條邊
O(V²)O(V²)

空間:O(V),見過的標記,加上佇列或堆疊

Big O 實測:n 變大時步數怎麼長

數的是:取出次數+檢查邊的次數(n 個點、每點約 4 條邊的隨機圖)

Big On = 500n = 5,000n = 50,000成長倍數:實測(理論)
BFSO(n)2,49825,000249,998×100 (×100)
DFSO(n)2,99830,001299,998×100 (×100)

邊數和點數成正比時,O(V + E) 就是 O(n):點變成一百倍,工作量也正好一百倍。

和其他做法比

BFS 佇列最多DFS 堆疊最多遞迴版 DFS 的深度
完全二元樹,1,023 個點(寬而淺)512109
一條直線,1,000 個點(窄而深)11999
隨機稀疏圖,1,000 個點4171,002999

同一張圖、從同一個點出發,兩種走法做的事一樣多(每個點、每條邊各一次),差別在記憶體:BFS 要同時記住一整圈的點,在又寬又淺的樹上最多;遞迴版 DFS 的呼叫深度等於它走的那條路有多長,在一條長直線上會深到可能把呼叫堆疊撐爆。

真實世界裡的它

  • 垃圾回收的 mark 階段:從根物件出發走遍所有還被參照的物件。
  • LinkedIn 的「二度人脈」、Facebook 的共同好友,是在社群圖上做 BFS。
  • 小畫家的油漆桶(flood fill)、找出圖片裡相連的區塊。
  • 編譯器與套件管理員用 DFS 找出循環依賴。

取捨與陷阱

  • 忘了標記見過的點:只要圖裡有環,就會永遠繞圈子。
  • BFS 要在放進佇列時就標記,不是取出時;否則同一個點會被排進去很多次。
  • 遞迴寫的 DFS 在很深的圖上會 stack overflow;Python 預設遞迴上限只有 1,000 層。改用明確的堆疊就沒有這個問題。
  • 圖不連通時,從一個起點走不到全部:要對每個還沒走過的點各起一次頭。

LeetCode 練習