[算法] 二叉樹的 先序遍歷、中序遍歷、後序遍歷

本文根據清華大學鄧俊輝老師課程《數據結構》總結,課程地址

遍歷介紹

按照事先約定的某種規則或次序,對節點各訪問一次而且僅一次。與向量和列表等線性結構一樣,二叉樹的這類訪問也統稱爲遍歷(traversal)。

二叉樹本身並不具有天然的全局次序, 故爲實現遍歷,需通過在各節點與其孩子之間約定某種局部次序, 間接地定義某種全局次序。

按慣例左兄弟優先於右兄弟, 若記做節點 V ,及其左、右孩子 LR ,則下圖所示,局
部訪問的次序可有 V L RL V RL R V 三種選擇。根據節點 V 在其中的訪問次序,三種策略也相應地分別稱作 先序遍歷中序遍歷後序遍歷

這裏寫圖片描述

可以根據節點 V 次序位置進行記憶,先序遍歷中 V 位於前端,中序遍歷中 V 位於中間,後序遍歷中 V 位於後端。

下面說一下各個遍歷的迭代式實現。

先序遍歷

通過先序遍歷操作後,返回結果的順序如下圖所示。 注意下圖是最終返回的結果展示順序,實現方法及流程並非如此。

先序遍歷返回結果

C++ 實現代碼如下:

//從當前節點出發,沿左分支不斷深入,直至沒有左分支的節點,沿途節點遇到後立即訪問
template <typename T, typename VST> //元素類型、操作器
static void visitAlongLeftBranch(BinNodePosi(T) x, VST& visit, Stack<BinNodePosi(T)>& S) {
    while (x) {
        visit(x->data); //訪問當前節點
        S.push(x->rChild); //右孩子入棧暫存(可優化:通過判斷,避免空的右孩子入棧)
        x = x->lChild; //沿左分支深入一層
    }
}

template <typename T, typename VST> //元素類型、操作器
void travPre_I2(BinNodePosi(T) x, VST& visit) { //二叉樹先序遍歷算法(迭代版)
    Stack<BinNodePosi(T)> S; //輔助棧
    while (true) {
        visitAlongLeftBranch(x, visit, S); //從當前節點出發,逐批訪問
        if (S.empty()) break; //直到棧空
        x = S.pop(); //彈出下一批的節點
    }
}

根據上面的代碼,舉個例子。

先序遍歷實現流程

上圖所示的二叉樹遍歷,流程描述如下:

  1. 從節點 a 出發,沿左分支不斷深入,直至沒有左分支的節點,沿途節點遇到後立即訪問。首先 a 的右節點 c 直接進棧,然後訪問左節點 b
  2. b 的右節點直接進棧,此時其爲空節點,所以空節點進棧,訪問 b 的左節點,也爲空,直接進行下一步;
  3. 彈出棧頂空節點,再彈出 c,將 c 的右節點 f 直接進棧,並訪問左節點 d
  4. d 的右節點 e 直接進棧,並訪問左節點 ;
  5. d 的左節點爲空。接下來彈出棧頂的 e ,並將 e 的右節點(空節點)直接進棧,訪問 e 的左節點;
  6. e 的左節點爲空。接下來彈出棧頂的 f ,並將 f 的右節點(空節點)直接進棧,訪問 f 的左節點 g
  7. g 的右節點(空節點)直接進棧, 訪問 g 的左節點;
  8. g 的左節點爲空。彈出g 的右節點(空節點),再彈出 f 的右節點(空節點);
  9. 棧爲空,遍歷結束。(其實上述描述的每一次循環都會做一次棧是否爲空的檢查

中序遍歷

通過中序遍歷操作後,返回結果的順序如下圖所示。
同樣需注意下圖是最終返回的結果展示順序,實現方法及流程並非如此。

中序遍歷返回結果

C++ 實現代碼如下:

template <typename T> //從當前節點出發,沿左分支不斷深入,直至沒有左分支的節點
static void goAlongLeftBranch(BinNodePosi(T) x, Stack<BinNodePosi(T)>& S) {
    while (x) { S.push(x); x = x->lChild; } //當前節點入棧後隨即向左側分支深入,迭代直到無左孩子
}

template <typename T, typename VST> //元素類型、操作器
void travIn_I1(BinNodePosi(T) x, VST& visit) { //二叉樹中序遍歷算法(迭代版)
    Stack<BinNodePosi(T)> S; //輔助棧
    while (true) {
        goAlongLeftBranch(x, S); //從當前節點出發,逐批入棧
        if (S.empty()) break; //直至所有節點處理完畢
        x = S.pop(); visit(x->data); //彈出棧頂節點並訪問之
        x = x->rChild; //轉向右子樹
    }
}

根據上面的代碼,舉個例子。

中序遍歷實現流程

上圖所示的二叉樹遍歷,流程描述如下:

  1. 從節點 b 出發, b 進棧 S。沿左分支不斷深入,遇到節點則入棧;
  2. 直至所有左分支節點處理完畢。(此時 S 中從上往下爲 a、b);
  3. 彈出棧 S 頂節點 a 並訪問之;
  4. 轉向 a 右子樹。到此處截止,爲一個循環體操作。接下來對 a 右子樹,對其重複循環體類似操作;
  5. 但這裏a 右子樹爲空,所以繼續彈出 b 。轉向 b 右子樹,對其進行重複循環體類似操作;
  6. 所以 f、d、c 依次入棧,c 在棧頂。彈出 c ,轉向 c 右子樹,重複循環體;
  7. c 右子樹爲空。彈出 d ,轉向 d 右子樹,重複循環體。
  8. e 入棧,彈出 e ,轉向 c 右子樹,重複循環體,c 右子樹爲空;
  9. 彈出 f,轉向 f 右子樹,重複循環體。
  10. g 入棧, g 出棧,轉向 g 右子樹,爲空;
  11. 此時,沒有新的節點入棧,棧中也沒有其他節點,終止遍歷操作。

後序遍歷

通過後序遍歷操作後,返回結果的順序如下圖所示。

後序遍歷返回結果

C++ 實現代碼如下:

template <typename T> //在以S棧頂節點爲根的子樹中,找到最高左側可見葉節點
static void gotoHLVFL(Stack<BinNodePosi(T)>& S) { //沿途所遇節點依次入棧
    while (BinNodePosi(T) x = S.top()) //自頂而下,反覆檢查當前節點(即棧頂)
        if (HasLChild(*x)) { //儘可能向左
            if (HasRChild(*x)) S.push(x->rChild); //若有右孩子,優先入棧
            S.push(x->lChild); //然後才轉至左孩子
        } else //實不得已
            S.push(x->rChild); //才向右
    S.pop(); //返回之前,彈出棧頂的空節點
}

template <typename T, typename VST>
void travPost_I(BinNodePosi(T) x, VST& visit) { //二叉樹的後序遍歷(迭代版)
    Stack<BinNodePosi(T)> S; //輔助棧
    if (x) S.push(x); //根節點入棧
    while (!S.empty()) {
        if (S.top() != x->parent) //若棧頂非當前節點之父(則必爲其右兄),此時需
            gotoHLVFL(S); //在以其右兄爲根之子樹中,找到HLVFL(相當於遞歸深入其中)
        x = S.pop(); visit(x->data); //彈出棧頂(即前一節點之後繼),並訪問之
    }
}

根據上面的代碼,舉個例子。

後序遍歷實力流程1

  1. 找到最高左側可見葉節點 k,若有右子樹優先入棧(此處爲 j),但優先往左子樹方向走(i 入棧);
  2. i 的右子樹 h 入棧,i 無左子樹,所以繼續對右子樹 h 進行操作;
  3. h 的右子樹 g 入棧,方向到左子樹(b 入棧);
  4. b 的右子樹 a 入棧,b 無左子樹。繼續對 a 進行操作,a 無子節點;
  5. 到此爲止,第一次入棧操作結束,此時棧中頂而下依次爲 abghijk
  6. 接下來彈出棧頂元素 a ,訪問之;
  7. ba 的父節點,不用進行 入棧操作。彈出棧頂元素 b ,訪問之;
  8. 接下來是 g ,非 b 的父節點,執行入棧操作,按照1~5步驟說的方法,依次將 fedc 入棧;

    後序遍歷實力流程2

  9. 接下來判斷是否需要執行入棧,並不斷從棧中彈出節點,並訪問之;

  10. 最後,棧爲空,遍歷結束。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章