原題連結:
整體思路
三道題的解決思路可統一,模板也極其相似,比九章提供的更漂亮。
- 將二叉樹分為“左”(包括一路向左,經過的所有實際左+根)、“右”(包括實際的右)兩種節點
- 使用同樣的順序將“左”節點入棧
- 在合適的時機轉向(轉向後,“右”節點即成為“左”節點)、訪問節點、或出棧
比如{1,2,3},當cur位於節點1時,1、2屬於“左”節點,3屬於“右”節點。DFS的非遞迴實現本質上是在協調入棧、出棧和訪問,三種操作的順序。上述統一使得我們不再需要關注入棧順序,僅需要關注出棧和訪問(第3點),隨著更詳細的分析,你將更加體會到這種簡化帶來的好處。
將對節點的訪問定義為results.add(node.val);
,分析如下:
先序&&中序:
先序和中序的情況是極其相似的。
- 先序的實際順序:根左右
- 中序的實際順序:左根右
使用上述思路,先序和中序的遍歷順序可統一為:“左”“右”。
給我們的直觀感覺是程式碼也會比較相似。實際情況正是如此,先序與中序的區別只在於對“左”節點的訪問上。
先序的實現
不需要入棧,每次遍歷到“左”節點,立即輸出即可。
需要注意的是,遍歷到最左下的節點時,實際上輸出的已經不再是實際的根節點,而是實際的左節點。這符合先序的定義。
while (cur != null) {
results.add(cur.val);
stack.push(cur);
cur = cur.left;
}
複製程式碼
而後,因為我們已經訪問過所有“左”節點,現在只需要將這些沒用的節點出棧,然後轉向到“右”節點。於是“右”節點也變成了“左”節點,後續處理同上。
if (!stack.empty()) {
cur = stack.pop();
// 轉向
cur = cur.right;
}
複製程式碼
完整程式碼如下:
private List<Integer> dfsPreOrder(TreeNode root) {
ArrayList<Integer> results = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.empty()) {
while (cur != null) {
results.add(cur.val);
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
// 轉向
cur = cur.right;
}
return results;
}
複製程式碼
中序的實現
基於對先序的分析,先序與中序的區別只在於對“左”節點的處理上
,我們調整一行程式碼即可完成中序遍歷。
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
results.add(cur.val); // 僅調整該行程式碼
// 轉向
cur = cur.right;
複製程式碼
注意,我們在出棧之後才訪問這個節點。因為先序先訪問實際根,後訪問實際左,而中序恰好相反。相同的是,訪問完根+左子樹(先序)或左子樹+根(中序)後,都需要轉向到“右”節點,使“右”節點稱為新的“左”節點。
完整程式碼如下:
private List<Integer> dfsInOrder(TreeNode root) {
List<Integer> results = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode cur = root;
while (cur != null || !stack.empty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
results.add(cur.val);
cur = cur.right;
}
return results;
}
複製程式碼
後序
後序的情況略有不同,但仍然十分簡潔。
- 後序的實際順序:左右根
入棧順序不變,我們只需要考慮第3點的變化(合適時機轉向
)。出棧的物件一定都是“左”節點(“右”節點會在轉向後稱為“左”節點,然後入棧),也就是實際的左或根;實際的左可以當做左右子樹都為null的根,所以我們只需要分析實際的根。
對於實際的根,需要保證先後訪問了左子樹、右子樹之後,才能訪問根。實際的右節點、左節點、根節點都會成為“左”節點入棧,所以我們只需要在出棧之前,將該節點視作實際的根節點,並檢查其右子樹是否已被訪問即可。如果不存在右子樹,或右子樹已被訪問了,那麼可以訪問根節點,出棧,並不需要轉向;如果還沒有訪問,就轉向,使其“右”節點成為“左”節點,等著它先被訪問之後,再來訪問根節點。
所以,我們需要增加一個標誌,記錄右子樹的訪問情況。由於訪問根節點前,一定先緊挨著訪問了其右子樹,所以我們只需要一個標誌位。
完整程式碼如下:
private List<Integer> dfsPostOrder(TreeNode root) {
List<Integer> results = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode last = null;
while(cur != null || !stack.empty()){
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.peek();
if (cur.right == null || cur.right == last) {
results.add(cur.val);
stack.pop();
// 記錄上一個訪問的節點
// 用於判斷“訪問根節點之前,右子樹是否已訪問過”
last = cur;
// 表示不需要轉向,繼續彈棧
cur = null;
} else {
cur = cur.right;
}
}
return results;
}
複製程式碼
總結
思路簡潔萬歲!模板大法萬歲!
消滅人類暴政,世界屬於三體!
本文連結:【刷題】二叉樹非遞迴遍歷
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。