本文根據清華大學鄧俊輝老師課程《數據結構》總結,課程地址 。
遍歷介紹
按照事先約定的某種規則或次序,對節點各訪問一次而且僅一次。與向量和列表等線性結構一樣,二叉樹的這類訪問也統稱爲遍歷(traversal)。
二叉樹本身並不具有天然的全局次序, 故爲實現遍歷,需通過在各節點與其孩子之間約定某種局部次序, 間接地定義某種全局次序。
按慣例左兄弟優先於右兄弟, 若記做節點 V
,及其左、右孩子 L
和 R
,則下圖所示,局
部訪問的次序可有 V L R
、 L V R
和 L 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(); //彈出下一批的節點
}
}
根據上面的代碼,舉個例子。
上圖所示的二叉樹遍歷,流程描述如下:
- 從節點
a
出發,沿左分支不斷深入,直至沒有左分支的節點,沿途節點遇到後立即訪問。首先a
的右節點c
直接進棧,然後訪問左節點b
; b
的右節點直接進棧,此時其爲空節點,所以空節點進棧,訪問b
的左節點,也爲空,直接進行下一步;- 彈出棧頂空節點,再彈出
c
,將c
的右節點f
直接進棧,並訪問左節點d
; - 將
d
的右節點e
直接進棧,並訪問左節點 ; d
的左節點爲空。接下來彈出棧頂的e
,並將e
的右節點(空節點)直接進棧,訪問e
的左節點;e
的左節點爲空。接下來彈出棧頂的f
,並將f
的右節點(空節點)直接進棧,訪問f
的左節點g
;- 將
g
的右節點(空節點)直接進棧, 訪問g
的左節點; g
的左節點爲空。彈出g
的右節點(空節點),再彈出f
的右節點(空節點);- 棧爲空,遍歷結束。(其實上述描述的每一次循環都會做一次棧是否爲空的檢查)
中序遍歷
通過中序遍歷操作後,返回結果的順序如下圖所示。
同樣需注意下圖是最終返回的結果展示順序,實現方法及流程並非如此。
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; //轉向右子樹
}
}
根據上面的代碼,舉個例子。
上圖所示的二叉樹遍歷,流程描述如下:
- 從節點
b
出發,b
進棧S
。沿左分支不斷深入,遇到節點則入棧; - 直至所有左分支節點處理完畢。(此時
S
中從上往下爲a、b
); - 彈出棧
S
頂節點a
並訪問之; - 轉向
a
右子樹。到此處截止,爲一個循環體操作。接下來對a
右子樹,對其重複循環體類似操作; - 但這裏
a
右子樹爲空,所以繼續彈出b
。轉向b
右子樹,對其進行重複循環體類似操作; - 所以
f、d、c
依次入棧,c
在棧頂。彈出c
,轉向c
右子樹,重複循環體; c
右子樹爲空。彈出d
,轉向d
右子樹,重複循環體。e
入棧,彈出e
,轉向c
右子樹,重複循環體,c
右子樹爲空;- 彈出
f
,轉向f
右子樹,重複循環體。 g
入棧,g
出棧,轉向g
右子樹,爲空;- 此時,沒有新的節點入棧,棧中也沒有其他節點,終止遍歷操作。
後序遍歷
通過後序遍歷操作後,返回結果的順序如下圖所示。
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); //彈出棧頂(即前一節點之後繼),並訪問之
}
}
根據上面的代碼,舉個例子。
- 找到最高左側可見葉節點
k
,若有右子樹優先入棧(此處爲j
),但優先往左子樹方向走(i
入棧); i
的右子樹h
入棧,i
無左子樹,所以繼續對右子樹h
進行操作;h
的右子樹g
入棧,方向到左子樹(b
入棧);b
的右子樹a
入棧,b
無左子樹。繼續對a
進行操作,a
無子節點;- 到此爲止,第一次入棧操作結束,此時棧中頂而下依次爲
abghijk
; - 接下來彈出棧頂元素
a
,訪問之; b
是a
的父節點,不用進行 入棧操作。彈出棧頂元素b
,訪問之;接下來是
g
,非b
的父節點,執行入棧操作,按照1~5步驟說的方法,依次將fedc
入棧;接下來判斷是否需要執行入棧,並不斷從棧中彈出節點,並訪問之;
- 最後,棧爲空,遍歷結束。