【刷題】二叉樹非遞迴遍歷

monkeysayhi發表於2017-10-16

原題連結:

整體思路

三道題的解決思路可統一,模板也極其相似,比九章提供的更漂亮。

  1. 將二叉樹分為“左”(包括一路向左,經過的所有實際左+根)、“右”(包括實際的右)兩種節點
  2. 使用同樣的順序將“左”節點入棧
  3. 在合適的時機轉向(轉向後,“右”節點即成為“左”節點)、訪問節點、或出棧

比如{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 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章