探索二叉樹
樹
是一種經常用到的數據結構,用來模擬具有樹狀結構性質的數據集合。
樹裏的每一個節點有一個根植和一個包含所有子節點的列表。從圖的觀點來看,樹也可視爲一個擁有N 個節點
和N-1 條邊
的一個有向無環圖。
二叉樹
是一種更爲典型的樹樹狀結構。如它名字所描述的那樣,二叉樹是每個節點最多有兩個子樹
的樹結構,通常子樹被稱作“左子樹”和“右子樹”。
前序遍歷
前序遍歷首先訪問根節點,然後遍歷左子樹,最後遍歷右子樹。
請看下面的例子:
中序遍歷
中序遍歷是先遍歷左子樹,然後訪問根節點,然後遍歷右子樹。
後序遍歷
後序遍歷是先遍歷左子樹,然後遍歷右子樹,最後訪問樹的根節點。
值得注意的是,當你刪除樹中的節點時,刪除過程將按照後序遍歷的順序進行。 也就是說,當你刪除一個節點時,你將首先刪除它的左節點和它的右邊的節點,然後再刪除節點本身。
如果你想對這棵樹進行後序遍歷,使用棧來處理表達式會變得更加容易。 每遇到一個操作符,就可以從棧中彈出棧頂的兩個元素,計算並將結果返回到棧中。
層序遍歷 - 介紹
層序遍歷就是逐層遍歷樹結構。
廣度優先搜索
是一種廣泛運用在樹或圖這類數據結構中,遍歷或搜索的算法。 該算法從一個根節點開始,首先訪問節點本身。 然後遍歷它的相鄰節點,其次遍歷它的二級鄰節點、三級鄰節點,以此類推。
當我們在樹中進行廣度優先搜索時,我們訪問的節點的順序是按照層序遍歷順序的。
運用遞歸解決樹的問題
遞歸是解決樹的相關問題最有效和最常用的方法之一。
樹可以以遞歸的方式定義爲一個節點(根節點),它包括一個值和一個指向其他節點指針的列表。 遞歸是樹的特性之一。 因此,許多樹問題可以通過遞歸的方式來解決。 對於每個遞歸層級,我們只能關注單個節點內的問題,並通過遞歸調用函數來解決其子節點問題。
通常,我們可以通過 “自頂向下” 或 “自底向上” 的遞歸來解決樹問題。
“自頂向下” 的解決方案
“自頂向下” 意味着在每個遞歸層級,我們將首先訪問節點來計算一些值,並在遞歸調用函數時將這些值傳遞到子節點。 所以 “自頂向下” 的解決方案可以被認爲是一種前序遍歷。 具體來說,遞歸函數 top_down(root, params)
的原理是這樣的:
1. return specific value for null node
2. update the answer if needed // anwer <-- params
3. left_ans = top_down(root.left, left_params) // left_params <-- root.val, params
4. right_ans = top_down(root.right, right_params) // right_params <-- root.val, params
5. return the answer if needed // answer <-- left_ans, right_ans
例如,思考這樣一個問題:給定一個二叉樹,請尋找它的最大深度。
我們知道根節點的深度是1
。 對於每個節點,如果我們知道某節點的深度,那我們將知道它子節點的深度。 因此,在調用遞歸函數的時候,將節點的深度傳遞爲一個參數,那麼所有的節點都知道它們自身的深度。 而對於葉節點,我們可以通過更新深度從而獲取最終答案。 這裏是遞歸函數 maximum_depth(root, depth)
的僞代碼:
1. return if root is null
2. if root is a leaf node:
3. answer = max(answer, depth) // update the answer if needed
4. maximum_depth(root.left, depth + 1) // call the function recursively for left child
5. maximum_depth(root.right, depth + 1) // call the function recursively for right child
以下的例子可以幫助你理解它是如何工作的:
int answer; // don't forget to initialize answer before call maximum_depth
void maximum_depth(TreeNode* root, int depth) {
if (!root) {
return;
}
if (!root->left && !root->right) {
answer = max(answer, depth);
}
maximum_depth(root->left, depth + 1);
maximum_depth(root->right, depth + 1);
}
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-idIouR7Y-1589513500589)(C:\Users\SHERRY~1\AppData\Local\Temp\1589338059349.png)]
“自底向上” 的解決方案
“自底向上” 是另一種遞歸方法。 在每個遞歸層次上,我們首先對所有子節點遞歸地調用函數,然後根據返回值和根節點本身的值得到答案。 這個過程可以看作是後序遍歷的一種。 通常, “自底向上” 的遞歸函數 bottom_up(root)
爲如下所示:
1. return specific value for null node
2. left_ans = bottom_up(root.left) // call function recursively for left child
3. right_ans = bottom_up(root.right) // call function recursively for right child
4. return answers // answer <-- left_ans, right_ans, root.val
讓我們繼續討論前面關於樹的最大深度的問題,但是使用不同的思維方式:對於樹的單個節點,以節點自身爲根的子樹的最大深度x
是多少?
如果我們知道一個根節點,以其左子節點爲根的最大深度爲l
和以其右子節點爲根的最大深度爲r
,我們是否可以回答前面的問題? 當然可以,我們可以選擇它們之間的最大值,再加上1來獲得根節點所在的子樹的最大深度。 那就是 x = max(l,r)+ 1
。
這意味着對於每一個節點來說,我們都可以在解決它子節點的問題之後得到答案。 因此,我們可以使用“自底向上“的方法。下面是遞歸函數 maximum_depth(root)
的僞代碼:
1. return 0 if root is null // return 0 for null node
2. left_depth = maximum_depth(root.left)
3. right_depth = maximum_depth(root.right)
4. return max(left_depth, right_depth) + 1 // return depth of the subtree rooted at root
以下的例子可以幫助你理解它是如何工作的:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-q0lLSyrc-1589513500589)(C:\Users\SHERRY~1\AppData\Local\Temp\1589513301516.png)]
int maximum_depth(TreeNode* root) {
if (!root) {
return 0; // return 0 for null node
}
int left_depth = maximum_depth(root->left);
int right_depth = maximum_depth(root->right);
return max(left_depth, right_depth) + 1; // return depth of the subtree rooted at root
}
總結
瞭解遞歸併利用遞歸解決問題並不容易。
當遇到樹問題時,請先思考一下兩個問題:
- 你能確定一些參數,從該節點自身解決出發尋找答案嗎?
- 你可以使用這些參數和節點本身的值來決定什麼應該是傳遞給它子節點的參數嗎?
如果答案都是肯定的,那麼請嘗試使用 “自頂向下
” 的遞歸來解決此問題。
並利用遞歸解決問題並不容易。
當遇到樹問題時,請先思考一下兩個問題:
- 你能確定一些參數,從該節點自身解決出發尋找答案嗎?
- 你可以使用這些參數和節點本身的值來決定什麼應該是傳遞給它子節點的參數嗎?
如果答案都是肯定的,那麼請嘗試使用 “自頂向下
” 的遞歸來解決此問題。
或者你可以這樣思考:對於樹中的任意一個節點,如果你知道它子節點的答案,你能計算出該節點的答案嗎? 如果答案是肯定的,那麼 “自底向上
” 的遞歸可能是一個不錯的解決方法。