LeetCode 課前預習 - 掌握 BFS 與 DFS 指南
BFS vs DFS
二元樹走訪或稱二元樹遍歷,簡單來說就是走訪樹中各節點,轉化為線性關係。
主要分成兩種策略方式
深度優先搜尋(Depth-first Search,DFS)
:從根節點出發,選擇某一子樹並以垂直方向由上到下處理,將其子節點訪問完後,再選擇另一子樹走訪。廣度優先搜尋(Breadth-first Search,BFS)
:從根節點出發,以水平方向由左到右走訪,將同階層的兄弟節點訪問完之後,再走訪下一階層的節點。
深度優先搜尋 DFS
假設根結點為N、左子樹為L、右子樹為R
- 前序走訪(Pre-order Traversal):
NLR
,根節點
→左子樹
→右子樹
- 中序走訪(In-order Traversal):
LNR
,左子樹
→根節點
→右子樹
- 後序走訪(Post-order Traversal):
LRN
,左子樹
→右子樹
→根節點
簡單來說
- 都是先左子樹,再右子樹
preorder
: root 在前面inorder
: root 在中間(裡面)postorder
: root 在後面
preorder的實作
前序走訪
: 順序是根節點、左子節點、右子節點。
Order: [1, 2, 4, 8, 9, 5, 3, 6, 10, 11, 7]
recursive
: 先處理根節點 -> 假設左右子樹已經處理好 :(再印左)->(後印右)
iterative
: 使用 stack 把要先處理的節點晚點放入,先處理左子樹,再處理右子樹。那就先塞右子樹,再塞左子樹。
1 | def preorder_traversal(root): |
1 | def preorder_traversal_iterative(root): |
inorder的實作
中序走訪
: 順序是左子節點、根節點、右子節點。
Order: [8, 4, 9, 2, 5, 1, 10, 6, 11, 3, 7]
recursive
: 假設左右子樹已經處理好 :先印左子樹 -> 印root ->後印右子樹
iterative
:
- 使用 stack 深入進去左子樹的底端
- stack pop 出來處理(最底層的左子樹)
- 轉向右子樹
1 | def inorder_traversal(root): |
1 | def inorder_traversal_iterative(root): |
postorder的實作
後序走訪
: 順序是左子節點、右子節點、根節點。
Order: [8, 9, 4, 5, 2, 10, 11, 6, 7, 3, 1]
recursive
: 假設左右子樹已經處理好 :先印左子樹 -> 後印右子樹 -> 印root
iterative
:
- 很像preorder前序走訪,只是 順序是根節點、右子節點、左子節點。
- 因此我們可以使用preorder的方式,但是要先append右子樹,再append左子樹
- 最後再reverse整個結果列表
1 | def postorder_traversal(root): |
1 | def postorder_traversal_iterative(root): |
Morris Traversal
很多時候題目會要求 O(1) 空間複雜度,這時候我們可以使用 Morris Traversal 來達成,但是此方法直接修改原始樹,但可以保證空間複雜度為 O(1),因此使用的時候要注意是否可以改動到原始樹。
1 | 1 |
程式碼 preorder-morris-traversal
1 | def flatten2(self, root: Optional[TreeNode]) -> TreeNode: |
廣度優先搜尋 BFS
層序走訪
: 順序是由根節點一層一層往下,由左往右。
Order: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
recursive
: next_level 是拿來存放下一層的節點,values 當前層的解答,最後把 values 加入 result 中。
iterative
: 跟recursive不會有太大的差異,反而比較好理解,就是把下一層要處理的節點放入 queue (先進先出) 中,然後處理當前節點,只要發現當前的節點仍有下一層,那就放到queue等等處理。
1 | def bfs_recursive(root): |
1 | def bfs_iterative(root): |
總結
DFS複雜:使用Stack先進後出
使用recursive
會比較容易
- 大問題:要走訪整個樹
- 小問題:
- 假設左右子樹已經走訪 recursive 左右子樹,最後結合root即可
使用iterative
會比較複雜
preorder
因為需要先完整的走到底 (左子樹),因此可以先印root,然後先放 right 再放 left,這樣 pop 就會處理 按後進先出原則,先處理left再處理right。postorder
比較特別,記住他是 preorder (但是先處理右子樹,再處理左子樹)的相反,因此可以使用跟 preorder 一樣的方式,只是stack先放left再放right,最後再reverse整個結果列表。inorder
比較特殊,因為 root 放在中間,這意味著要先把左子樹走完,為了先走到底,我們要用 while loop 一直往左子樹底部走直到觸底,然後再處理root,最後轉向右子樹。
BFS單純:使用Queue先進先出
使用 recursive
或是 iterative
所需要的概念都相同
- 那就是都是先把下一層要處理的節點放入 queue 中,然後處理當前節點,只要發現當前的節點仍有下一層,那就放到queue等等處理。