二叉樹遍歷方法大全(包含莫里斯遍歷)

前言

二叉樹的遍歷可能大家都比較熟悉了往枣,這篇文章主要介紹了三種二叉樹的遍歷方法——遞歸伐庭、迭代和莫里斯遍歷,他們各自有各自的特點分冈。其中最重要的是莫里斯遍歷圾另,相對于前兩種方法比較少見,只需要固定的空間就可以完成迭代遍歷雕沉。這篇文章將會結合動圖集乔,帶你了解關于樹遍歷的知識。

簡介

我們通常希望通過訪問樹的每個節(jié)點來處理二叉樹,每次執(zhí)行特定的操作扰路,例如打印節(jié)點的內容尤溜、得到樹的所有節(jié)點的總和或者要找到最大的值。以某種順序訪問所有節(jié)點的過程稱為遍歷汗唱,僅遍歷樹中每個節(jié)點一次的遍歷稱為樹節(jié)點的枚舉宫莱。某些應用不需要以任何特定順序訪問節(jié)點,只要每個節(jié)點被精確訪問一次即可哩罪;有些應用授霸,必須按保留某些關系的順序訪問節(jié)點。

線性數(shù)據(jù)結構(如數(shù)組际插、堆棧碘耳、隊列和鏈表)只有一種讀取數(shù)據(jù)的方法,但是像樹這樣的分層數(shù)據(jù)結構可以以不同的方式遍歷框弛。

遍歷種類

根據(jù)我們遍歷樹的順序辛辨,我們把遍歷分成三種,分別是:

  • 中序遍歷
  • 前序遍歷
  • 后序遍歷

這些遍歷方式和樹的結構有關瑟枫。

tree.png

中序遍歷

  1. 先遍歷左子樹
  2. 再遍歷父節(jié)點
  3. 最后遍歷右子樹

圖片中的中序遍歷結果應該是[4,2,5,1,6,3,7]

前序遍歷

  1. 先遍歷父節(jié)點
  2. 再遍歷左子樹
  3. 最后遍歷右子樹

圖片中的前序遍歷結果應該是[1,2,4,5,3,6,7]

后序遍歷

  1. 先遍歷左子樹
  2. 再遍歷右子樹
  3. 最后遍歷父節(jié)點

圖中的后序遍歷結果應該是[4,5,2,6,7,3,1]

代碼實現(xiàn)

首先定義樹的結構

public class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(int x) { val = x; }

}

在代碼實現(xiàn)里面我們要返回按照特定遍歷順序依次遍歷的節(jié)點

中序實現(xiàn)

中序遞歸

按照中序遍歷的定義可以很容易用遞歸來實現(xiàn)

public List<Integer> inorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    List<Integer> res = new ArrayList<>();  // 保存最后的結果
    inorderTraversal(root, res);
    return res;
}

public void inorderTraversal(TreeNode root, List<Integer> res) {
    if (root == null) {
        return;
    }
    inorderTraversal(root.left, res);   // 遍歷左子樹
    res.add(root.val);                  // 遍歷父節(jié)點
    inorderTraversal(root.right, res);  // 遍歷右子樹
}

中序迭代

采用迭代的方法就有點復雜了斗搞,需要借助額外的數(shù)據(jù)結構——棧。

這個方法的思路是:

先從父結點遍歷左子節(jié)點力奋,一直遍歷到不再存在左子節(jié)點,然后從棧頂開始檢查幽七,對剛才遍歷的節(jié)點進行逆向遍歷景殷,找到每一個節(jié)點的右子節(jié)點,如果這些右子節(jié)點有左節(jié)點就繼續(xù)壓入棧中(相當于下次遍歷要從這個右子節(jié)點的左子樹開始)澡屡,繼續(xù)上面的過程猿挚。

整個相當于深度優(yōu)先遍歷,從每個節(jié)點的左節(jié)點遍歷驶鹉,遍歷父節(jié)點绩蜻,最后遍歷右節(jié)點。棧的作用相當于記住了上次遍歷的位置室埋,用來保存下次應該開始遍歷的節(jié)點办绝。

inorder.png
public List<Integer> inorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    Stack<TreeNode> stack = new Stack<>();
    List<Integer> res = new ArrayList<>();  // 遍歷結果
    stack.push(root);

    TreeNode cur = root.left;
    while (!stack.isEmpty() || cur != null) {
        while (cur != null) {   // 先將所有的左節(jié)點的內容壓入棧中
            stack.push(cur);
            cur = cur.left;
        }
        cur = stack.pop();    // 出棧的時候進行遍歷
        res.add(cur.val);
        cur = cur.right;     // 代表開始遍歷右子樹
    }
    return res;
}

中序莫里斯迭代

莫里斯遍歷不需要遞歸或者臨時的棧空間就可以完成遍歷姚淆,空間復雜度是常數(shù)孕蝉。但是為了解決從子節(jié)點找到父節(jié)點的問題,需要臨時修改樹的結構腌逢,在遍歷完成之后復原成原來的樹結構降淮。

整個遍歷的過程中只需要兩個指針——當前指針cur和臨時前驅指針prev,具體的過程如下

  1. 如果左子節(jié)點是空搏讶,錄入當前節(jié)點佳鳖,當前指針cur指向右子節(jié)點
  2. 如果左子節(jié)點不是空霍殴,遍歷左子節(jié)點的最右側右子節(jié)點,找到最右側葉節(jié)點系吩,在尋找的過程中可能出現(xiàn)兩種情況:
    • 如果遍歷到的葉節(jié)點的右子節(jié)點是空来庭,把葉節(jié)點的右子節(jié)點指向cur節(jié)點,cur移向左子節(jié)點
    • 如果遍歷到的葉節(jié)點的右子節(jié)點是cur節(jié)點淑玫,表示原來的葉節(jié)點到cur節(jié)點連接已經存在巾腕,現(xiàn)在遍歷結束了,需要復原絮蒿,置節(jié)點的右子節(jié)點為空尊搬,在錄入了cur節(jié)點之后,cur移到自己的右子節(jié)點
  3. 重復上面兩步直到當前節(jié)點為空

其中最不好理解的是第二步土涝,遍歷左子樹的右節(jié)點的過程中佛寿,只有當左子樹沒有建立到父節(jié)點的連接的時候,才能最后遍歷到盡頭但壮,達到盡頭之后需要和父節(jié)點連接起來冀泻,當cur遍歷到這個葉節(jié)點的時候才能回到正確的父節(jié)點的位置。

當當前節(jié)點cur遍歷完了左子樹回到父節(jié)點的時候蜡饵,多余的連接還是存在的弹渔,需要移除這個連接,而方法就是和建立連接一樣遍歷左子樹找到最右側節(jié)點溯祸,這個時候判斷的依據(jù)就不能是右節(jié)點為空肢专,必須是左子節(jié)點的最右節(jié)點等于當前節(jié)點,相當于找到循環(huán)的起點焦辅,然后在這個地方切斷聯(lián)系博杖。

morris.gif

代碼實現(xiàn):

public List<Integer> inorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    TreeNode cur = root;    // 記錄當前節(jié)點位置
    List<Integer> res = new ArrayList<>();
    while (cur != null) {
        if (cur.left == null) {   // 左節(jié)點為空,移到右子節(jié)點
            res.add(cur.val);
            cur = cur.right;
        }  else {
            TreeNode prev = cur.left;
            while (prev.right != null && prev.right != cur) { // 遍歷到左子樹的最右側節(jié)點
                prev = prev.right;
            }
            if (prev.right == null) {        // 建立返回父節(jié)點連接
                prev.right = cur;
                cur = cur.left;
            } else {                        // 左子樹建立了連接筷登,說明遍歷完了剃根,可以拆除連接
                res.add(cur.val);           // 中序遍歷錄入當前節(jié)點
                prev.right = null;
                cur = cur.right;
            }
        }
    }
    return res;
}

時間復雜度分析:莫里斯遍歷的空間復雜度是常數(shù)狈醉,這個比較好理解惠险,但是時間復雜度為什么是O(n)呢莺匠?明明在代碼里面有個嵌套的循環(huán)可能會提高時間復雜度:

while (prev.right != null && prev.right != cur) { // 遍歷到左子樹的最右側節(jié)點
    prev = prev.right;
}

可是如果在圖中模擬一下這個循環(huán)就會發(fā)現(xiàn)其實在尋找前驅節(jié)點的過程中,所有的節(jié)點其實最多只被遍歷了兩遍宵呛!比如對于節(jié)點1码秉,在尋找前驅節(jié)點的時候遍歷了25;當cur5回到1之后,又遍歷了一遍25赡译,至此25在所有尋找前驅節(jié)點的過程中各遍歷了兩邊狂男,而在尋找2..7的前驅節(jié)點的時候舞吭,都沒有遍歷到25(除去了從25本身開始查找前驅節(jié)點時對自己的遍歷),所以25總體在前驅搜索的過程中只有兩次,加上他們自身的遍歷纵朋,也就只有3次聂薪。綜合來說肘交,樹的每個節(jié)點的遍歷次數(shù)最多都是3次复罐,所以時間復雜度是O(n)級別的乱投。

traverse.png

前序實現(xiàn)

前序遞歸

和中序的遞歸差不多施掏,只有一行代碼的區(qū)別:

public List<Integer> preorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    List<Integer> res = new ArrayList<>();  // 保存最后的結果
    preorderTraversal(root, res);
    return res;
}

public void preorderTraversal(TreeNode root, List<Integer> res) {
    if (root == null) {
        return;
    }
    res.add(root.val);                  // 遍歷父節(jié)點   注意這行代碼提前了
    preorderTraversal(root.left, res);   // 遍歷左子樹
    preorderTraversal(root.right, res);  // 遍歷右子樹
}

前序迭代

直接根據(jù)中序迭代的方法七芭,將記錄遍歷元素的時機改為了在入棧的時候就記錄,也就是將父節(jié)點計入數(shù)組的時間提前了预明。

public List<Integer> preorderTraversal3(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    Stack<TreeNode> stack = new Stack<>();
    List<Integer> res = new ArrayList<>();  // 遍歷結果
    stack.push(root);

    TreeNode cur = root.left;
    res.add(root.val);      // 記錄根節(jié)點元素
    while (!stack.isEmpty() || cur != null) {
        while (cur != null) {   // 先將所有的左節(jié)點的內容壓入棧中
            stack.push(cur);
            res.add(cur.val);  // 這里在入棧的時候就要記錄遍歷的元素
            cur = cur.left;
        }
        cur = stack.pop();
        cur = cur.right;     // 代表開始遍歷右子樹
    }
    return res;
}

前序莫里斯

和中序莫里斯遍歷的代碼也基本一樣究西,只不過當左子節(jié)點存在的時候,添加節(jié)點元素的位置從拆除多余連接的時候變成了建立連接的時候卓练,也就是在移動cur指針之前就得記錄節(jié)點隘蝎,保證當前指向的節(jié)點是最先記錄的,左右子樹的節(jié)點要靠后顽悼,并且不能重復記錄元素曼振。

public List<Integer> preorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    TreeNode cur = root;    // 記錄當前節(jié)點位置
    List<Integer> res = new ArrayList<>();
    while (cur != null) {
        if (cur.left == null) {   // 左節(jié)點為空,移到右子節(jié)點
            res.add(cur.val);
            cur = cur.right;
        }  else {
            TreeNode prev = cur.left;
            while (prev.right != null && prev.right != cur) { // 遍歷到左子樹的最右側節(jié)點
                prev = prev.right;
            }
            if (prev.right == null) {        // 建立返回父節(jié)點連接
                prev.right = cur;
                res.add(cur.val);           // 注意添加元素到數(shù)組的代碼位置移到了這里
                cur = cur.left;
            } else {                        // 左子樹建立了連接蔚龙,說明遍歷完了冰评,可以拆除連接
                prev.right = null;
                cur = cur.right;
            }
        }
    }
    return res;
}

后序實現(xiàn)

后序遞歸

后序遞歸和前中遞歸的實現(xiàn)差不多,只需要把錄入元素的時機放在遍歷左右子樹之后就行了

public List<Integer> postorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    List<Integer> res = new ArrayList<>();  // 保存最后的結果
    postorderTraversal(root, res);
    return res;
}

public void postorderTraversal(TreeNode root, List<Integer> res) {
    if (root == null) {
        return;
    }
    postorderTraversal(root.left, res);   // 遍歷左子樹
    postorderTraversal(root.right, res);  // 遍歷右子樹
    res.add(root.val);                  // 遍歷父節(jié)點
}

還有另外一種遞歸可能會對我們后序迭代算法略有啟發(fā):我們可以通過將遍歷父節(jié)點操作放在最前面木羹,然后交換遍歷左右子樹的順序甲雅,得到反轉的后序遍歷結果,最后反轉一下就能得到正確的遍歷結果汇跨。

public List<Integer> postorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    List<Integer> res = new ArrayList<>();  // 保存最后的結果
    postorderTraversal(root, res);
    Collections.reverse(res);             // 反轉數(shù)組
    return res;
}

public void postorderTraversal(TreeNode root, List<Integer> res) {
    if (root == null) {
        return;
    }
    res.add(root.val);                  // 遍歷父節(jié)點
    postorderTraversal(root.right, res);  // 遍歷右子樹
    postorderTraversal(root.left, res);   // 遍歷左子樹
}

后序迭代

后序迭代就比較巧妙了务荆,利用上面講到的修改前序遍歷的遍歷左右子樹的順序妆距,移植到迭代過程中的棧的操作穷遂,把原來的所有的right改成left,原來的left改成right

public List<Integer> postorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    Stack<TreeNode> stack = new Stack<>();
    List<Integer> res = new ArrayList<>();  // 遍歷結果
    stack.push(root);

    TreeNode cur = root.right;
    res.add(root.val);  // 添加根節(jié)點娱据,反轉后變成最后元素
    while (!stack.isEmpty() || cur != null) {
        while (cur != null) {   // 先將所有的右節(jié)點的內容壓入棧中
            stack.push(cur);
            res.add(cur.val);   // 添加當前遍歷的節(jié)點
            cur = cur.right;
        }
        cur = stack.pop();
        cur = cur.left;     // 代表開始遍歷左子樹
    }
    Collections.reverse(res); // 反轉最后的結果
    return res;
}

后序莫里斯迭代

修改后序莫里斯迭代的思路其實和上面修改后序迭代的思路一樣

  1. 把前序莫里斯遍歷的代碼粘貼過來
  2. 把原來所有的right改成left蚪黑,把原來所有的left改成right
  3. 返回結果之前反轉一下數(shù)組

這種后序迭代遍歷的核心思路都是通過交換前序遍歷中遍歷左右子樹的順序盅惜,達到完全逆轉后序遍歷的結果,最后反轉得到正確的結果忌穿。

public List<Integer> postorderTraversal(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }

    TreeNode cur = root;    // 記錄當前節(jié)點位置
    List<Integer> res = new ArrayList<>();
    while (cur != null) {
        if (cur.right == null) {   // 右節(jié)點為空抒寂,移到左子節(jié)點
            res.add(cur.val);
            cur = cur.left;
        }  else {
            TreeNode prev = cur.right;
            while (prev.left != null && prev.left != cur) { // 遍歷右子樹的最左側節(jié)點
                prev = prev.left;
            }
            if (prev.left == null) {        // 建立返回父節(jié)點連接
                prev.left = cur;
                res.add(cur.val);           // 添加元素到數(shù)組
                cur = cur.right;
            } else {                        // 右子樹建立了連接,說明遍歷完了掠剑,可以拆除連接
                prev.left = null;
                cur = cur.left;
            }
        }
    }
    Collections.reverse(res);   // 最后要反轉數(shù)組得到最后的結果
    return res;
}

總結

總的來說二叉樹的遍歷是非常重要也是非城撸基礎的知識,大部分人都能夠輕松的寫出遞歸的做法朴译,遞歸的代碼是最簡潔明了容易理解的井佑。

迭代的解決方法比較少見,需要額外的數(shù)據(jù)結構眠寿,循環(huán)的邏輯也不是那么容易理解躬翁,但是在面試或者OJ系統(tǒng)里面可能會出現(xiàn)的比較多。

關于莫里斯遍歷盯拱,可能大多數(shù)人都沒有聽說過這種巧妙的遍歷方法盒发,需要修改樹的結構以降低空間開銷,同時在遍歷結束之后還要復原樹的結構狡逢。這種遍歷相對于上面的迭代更加難以理解宁舰,但是它只需要兩個變量就可以完成遍歷的特點令人影響深刻。

參考

Morris Traversal方法遍歷二叉樹(非遞歸奢浑,不用棧明吩,O(1)空間)
What is Morris traversal?
二叉樹的前序、中序殷费、后序遍歷—迭代方法

更多精彩內容請看我的個人博客

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末印荔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子详羡,更是在濱河造成了極大的恐慌仍律,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件实柠,死亡現(xiàn)場離奇詭異水泉,居然都是意外死亡,警方通過查閱死者的電腦和手機窒盐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門草则,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蟹漓,你說我怎么就攤上這事炕横。” “怎么了葡粒?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵份殿,是天一觀的道長膜钓。 經常有香客問我,道長卿嘲,這世上最難降的妖魔是什么颂斜? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮拾枣,結果婚禮上沃疮,老公的妹妹穿的比我還像新娘。我一直安慰自己梅肤,他們只是感情好忿磅,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凭语,像睡著了一般葱她。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上似扔,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天吨些,我揣著相機與錄音,去河邊找鬼炒辉。 笑死豪墅,一個胖子當著我的面吹牛,可吹牛的內容都是我干的黔寇。 我是一名探鬼主播偶器,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缝裤!你這毒婦竟也來了屏轰?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤憋飞,失蹤者是張志新(化名)和其女友劉穎霎苗,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榛做,經...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡唁盏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了检眯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厘擂。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖锰瘸,靈堂內的尸體忽然破棺而出刽严,到底是詐尸還是另有隱情,我是刑警寧澤获茬,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布港庄,位于F島的核電站,受9級特大地震影響恕曲,放射性物質發(fā)生泄漏鹏氧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一佩谣、第九天 我趴在偏房一處隱蔽的房頂上張望把还。 院中可真熱鬧,春花似錦茸俭、人聲如沸吊履。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艇炎。三九已至,卻和暖如春腾窝,著一層夾襖步出監(jiān)牢的瞬間缀踪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工虹脯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驴娃,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓循集,卻偏偏與公主長得像唇敞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咒彤,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內容