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

原題連接:java

總體思路

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

  1. 將二叉樹分爲「左」(包括一路向左,通過的全部實際左+根)、「右」(包括實際的右)兩種節點
  2. 使用一樣的順序將「左」節點入棧
  3. 在合適的時機轉向(轉向後,「右」節點即成爲「左」節點)、訪問節點、或出棧

好比{1,2,3},當cur位於節點1時,一、2屬於「左」節點,3屬於「右」節點。DFS的非遞歸實現本質上是在協調入棧、出棧和訪問,三種操做的順序。上述統一使得咱們再也不須要關注入棧順序,僅須要關注出棧和訪問(第3點),隨着更詳細的分析,你將更加體會到這種簡化帶來的好處。git

將對節點的訪問定義爲results.add(node.val);,分析以下:github

先序&&中序:

先序和中序的狀況是極其類似的。post

  • 先序的實際順序:根左右
  • 中序的實際順序:左根右

使用上述思路,先序和中序的遍歷順序可統一爲:「左」「右」。spa

給咱們的直觀感受是代碼也會比較類似。實際狀況正是如此,先序與中序的區別只在於對「左」節點的訪問上。code

先序的實現

不須要入棧,每次遍歷到「左」節點,當即輸出便可。對象

須要注意的是,遍歷到最左下的節點時,實際上輸出的已經再也不是實際的根節點,而是實際的左節點。這符合先序的定義。遞歸

while (cur != null) {
	results.add(cur.val);
	stack.push(cur);
	cur = cur.left;
}
複製代碼

然後,由於咱們已經訪問過全部「左」節點,如今只須要將這些沒用的節點出棧,而後轉向到「右」節點。因而「右」節點也變成了「左」節點,後續處理同上。get

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

相關文章
相關標籤/搜索