BFS 與 DFS
走遍一張圖的兩種方式:BFS(Breadth-First Search,廣度優先搜尋)用佇列一圈一圈往外擴,DFS(Depth-First Search,深度優先搜尋)用堆疊一路走到底再回頭。換一個資料結構,走法就完全不同。
走法
圖
起點
1/40
正在走在佇列裡等走過了(數字是順序)見過了,跳過
佇列:前 → 後
- 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) |
| DFS | O(V + E) | O(V + E) |
| 用相鄰矩陣存圖時 找鄰居要掃整列,不管實際有幾條邊 | O(V²) | O(V²) |
空間:O(V),見過的標記,加上佇列或堆疊
Big O 實測:n 變大時步數怎麼長
數的是:取出次數+檢查邊的次數(n 個點、每點約 4 條邊的隨機圖)
| Big O | n = 500 | n = 5,000 | n = 50,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|
| BFS | O(n) | 2,498 | 25,000 | 249,998 | ×100 (×100) |
| DFS | O(n) | 2,998 | 30,001 | 299,998 | ×100 (×100) |
邊數和點數成正比時,O(V + E) 就是 O(n):點變成一百倍,工作量也正好一百倍。
和其他做法比
| BFS 佇列最多 | DFS 堆疊最多 | 遞迴版 DFS 的深度 | |
|---|---|---|---|
| 完全二元樹,1,023 個點(寬而淺) | 512 | 10 | 9 |
| 一條直線,1,000 個點(窄而深) | 1 | 1 | 999 |
| 隨機稀疏圖,1,000 個點 | 417 | 1,002 | 999 |
同一張圖、從同一個點出發,兩種走法做的事一樣多(每個點、每條邊各一次),差別在記憶體:BFS 要同時記住一整圈的點,在又寬又淺的樹上最多;遞迴版 DFS 的呼叫深度等於它走的那條路有多長,在一條長直線上會深到可能把呼叫堆疊撐爆。
真實世界裡的它
- 垃圾回收的 mark 階段:從根物件出發走遍所有還被參照的物件。
- LinkedIn 的「二度人脈」、Facebook 的共同好友,是在社群圖上做 BFS。
- 小畫家的油漆桶(flood fill)、找出圖片裡相連的區塊。
- 編譯器與套件管理員用 DFS 找出循環依賴。
取捨與陷阱
- 忘了標記見過的點:只要圖裡有環,就會永遠繞圈子。
- BFS 要在放進佇列時就標記,不是取出時;否則同一個點會被排進去很多次。
- 遞迴寫的 DFS 在很深的圖上會 stack overflow;Python 預設遞迴上限只有 1,000 層。改用明確的堆疊就沒有這個問題。
- 圖不連通時,從一個起點走不到全部:要對每個還沒走過的點各起一次頭。