數據結構(三):二叉樹遍歷

遍歷方式

二叉樹的常見遍歷方式如下幾種:

  • 前序遍歷: 訪問根節點,前序遍歷方式訪問左子樹,前序遍歷方式訪問右子樹;
  • 中序遍歷: 中序遍歷方式訪問左子樹,訪問根節點,中序遍歷方式訪問右子樹;
  • 后序遍歷: 后序遍歷方式訪問左子樹,后序遍歷方式訪問右子樹,訪問根節點;
  • 層次遍歷: 按照層次遞增的順序,依次訪問樹的每層節點。

示例演示

除去層次遍歷不談,根據其他三種遍歷方式的描述可發現,其描述的內容也就是遞歸進行遍歷的過程。

二叉樹示例:
Binary Tree
前序遍歷

前序遍歷的方式,也就是對每一棵子樹,按照根節點、左子樹、右子樹的順序進行訪問,也就是根-左-右的訪問順序。因為每一棵非空子樹,又可拆分為根節點、左子樹和右子樹,所以可以按照根-左-右的方式,遞歸訪問每棵子樹。

遞歸方式:

#recursive pre-order traversal
def recursive_pre_order_traversal(root):
    if not root:
        return
    print(root.value,end=' ')
    recursive_pre_order_traversal(root.lchild)
    recursive_pre_order_traversal(root.rchild)
pre-order

觀察二叉樹的前序遞歸遍歷方式,首先訪問 “5” 節點,然后是 “3” 節點,然后是 “0” 節點,然后是 “0” 節點右子樹 A (A 子樹遞歸訪問),然后是 “3” 節點的右子樹 B (即進行回溯,B 子樹同 A 子樹,遞歸訪問)。

通過以上過程可以發現:

  • A、B 子樹的節點訪問方式,與以 “5” 為根節點的二叉樹訪問方式相同,即:5 \to 3 \to 0 \to A \to B。
  • 由 A 子樹到 B 子樹的訪問過程即為回溯,示例代碼中回溯的實現,使用了遞歸的形式,遞歸的函數會形成調用棧,每個函數的調用會保存一個棧幀,通過棧幀中數據的保存,來完成回溯。

所以這里前序遞歸遍歷規則 “根-左-右” 的體現包括兩個環節:

  • 遞歸:前序訪問的遞歸順序即為根-左-右。
    步驟:
    1.訪問二叉樹 T 的根節點 root
    2.若 root 左子樹非空,則二叉樹 T 指向 root 左子樹,執行步驟 1;
    3.若 root 左子樹為空,則二叉樹 T 指向 root 右子樹,執行步驟 1;

  • 回溯:一個子樹的遞歸結束后,返回上一層(非父節點那一層)繼續遞歸,即為回溯。
    例如以 0 為根節點,右子樹為 A 的二叉樹,完成前序遍歷后,也就相當于以 3 為根節點,{0, A} 為左子樹,B 為右子樹的二叉樹,剛剛完成根-左-右前序遍歷中的,“左”這一步,下一個訪問的二叉樹即為 B。

根據以上過程,可以推出前序遍歷的非遞歸形式。

非遞歸方式:

#non_recursive pre-order traversal
def non_recursive_pre_order_traversal(root):
    stack = []
    while root or len(stack) > 0:
        if root:  
            print(root.value,end=' ')
            stack.append(root)
            root = root.lchild
        else:  
            root = stack.pop()
            root = root.rchild

以上非遞歸代碼中,通過循環的形式完成了類似于遞歸的調用,通過棧對象完成回溯。代碼循環執行過程為:若二叉樹根節點存在,則訪問根節點,然后以相同方式訪問根節點的左子樹;若根節點不存在,則返回上一層訪問右子樹。

tips

tips:

1.前序和中序的回溯操作,都是訪問上一層的右子樹,因為無論是根-左-右,或者左-根-右,右子樹訪問結束后,都表示根節點和左子樹已經訪問過。

2.上一層不一定是父節點那一層,若二叉樹 C_1 是根節點 B 的左子樹,則左子樹訪問結束,上一層即為父節點一層,也就是根節點 B 這一層,下一步訪問根節點 B 的右子樹 C_2;若二叉樹 C_2 訪問結束,則表示根節點 B 和左子樹 C_1 已經訪問結束,上一層為根節點 A 這一層,下一步訪問即為二叉樹 D

代碼中使用棧來保存上一層的節點,即棧中最后一個元素即為上一層的根節點,通過出棧操作來完成回溯。

中序遍歷

中序遍歷二叉樹順序為左子樹-根節點-右子樹形式。如果二叉樹為二叉搜索樹這樣的節點有序結構,則中序遍歷輸出為有序的節點列表。

遞歸方式:

#recursive in-order traversal
def recursive_in_order_traversal(root):
    if not root:
        return
    recursive_in_order_traversal(root.lchild)
    print(root.value,end=' ')
    recursive_in_order_traversal(root.rchild)

在非遞歸形式中,所謂的訪問其實就是入棧和輸出根節點值的操作,因為要借用棧對象來實現回溯,所以訪問每棵二叉樹時,入棧根節點的操作是不變的。區別只在于中序遍歷時,輸出根節點的操作放在了訪問右子樹前,而非前序遍歷的入棧操作時輸出。

非遞歸方式:

#non_recursive in-order traversal
def non_recursive_in_order_traversal(root):
    stack = []
    while root or len(stack) > 0:
        if root:
            stack.append(root)
            root = root.lchild
        else:
            root = stack.pop()
            print(root.value,end=' ')
            root = root.rchild

觀察前序遍歷和中序遍歷的非遞歸方式可以發現,兩者除了輸出操作 print 語句位置不同外,其他內容完全一樣。

后序遍歷

后序遍歷二叉樹順序為左子樹-右子樹-根節點形式。

遞歸方式:

#recursive post-order traversal
def recursive_post_order_traversal(root):
    if not root:
        return
    recursive_post_order_traversal(root.lchild)
    recursive_post_order_traversal(root.rchild)
    print(root.value,end=' ')

因為要完成回溯,所以根節點入棧的操作不變。后序遍歷的順序為:左-右-根,也就是右子樹訪問結束后才會執行根節點的輸出操作,即右子樹遍歷結束后返回上一層繼續遍歷,后序遍歷中的上一層就是父節點一層。與前序和中序遍歷不同之處在于,后序遍歷在根節點的輸出上需要多做一些工作。

BT

分析上圖 BT 中的兩顆二叉樹:
【1】樹 bt1 中,二叉樹 C 遍歷結束后,輸出上一層的根節點 B 的值。因為 B 節點是其上一層的左節點,也就是說以 A 為根節點的二叉樹,完成了后序遍歷的左子樹遍歷,所以再下一步訪問右子樹 D
【2】樹 bt2 中,二叉樹 H 遍歷結束后,輸出上一層的根節點 G 的值。因為 G 是其上一層的右節點,所以 G 為根節點的二叉樹遍歷結束后,下一步輸出 G 的上一層的根節點 E 的值。因為 E 是其上一層的左節點,也就是說以 T 為根節點的二叉樹,完成了后序遍歷的左子樹遍歷,所以再下一步訪問右子樹 K

根據以上兩個二叉樹的后序遍歷過程可以發現,右子樹遍歷結束后輸出根節點的值,雖然完成了一輪的左-右-根遍歷,但并不算操作結束。還要判斷根節點 root_i 是其上一層根節點 root_{i-1} 的左節點還是右節點:

  • root_iroot_{i-1} 的左節點,則說明 root_{i-1} 一層完成了左-右-根的左子樹遍歷,下一步訪問 root_{i-1} 的右子樹;
  • root_iroot_{i-1} 的右節點,則說明 root_{i-1} 一層完成了左-右-根的右子樹遍歷,下一步訪問輸出 root_{i-1} 的值,并判斷 root_{i-1} 是其上一層根節點 root_{i-2} 的左節點還是右節點。

根據遍歷完成的二叉樹的根節點是其上一層根節點的左節點或右節點的不同,進行迭代處理。其實就是迭代輸出根節點的值,直到根節點是其上一層根節點的左節點,則輸出根節點并訪問上一層根節點的右子樹。

非遞歸方式:

#non_recursive post-order traversal
def non_recursive_post_order_traversal_one(root):
    stack = []
    while root or len(stack) > 0:
        if root:
            stack.append(root)
            root = root.lchild
        else:
            if stack[-1].rchild:
                root = stack[-1].rchild
            else:
                while len(stack) > 1 and stack[-1] == stack[-2].rchild:
                    print(stack.pop().value, end=' ')
                print(stack.pop().value, end=' ')
                if len(stack) > 0:
                    root = stack[-1].rchild

觀察代碼可知,循環處理的入棧部分跟前序和中序一樣,不同之處在于二叉樹遍歷結束后,會對根節點進行迭代輸出。

后續遍歷有一種簡潔的寫法形式,后序遍歷順序為:左-右-根,可以另聲明一個列表倒序保存根-右-左順序的記錄,下面給個簡寫的示例:

非遞歸方式_簡潔形式:

# non_recursive post-order traversal
def non_recursive_post_order_traversal_two(root):
    stack = []
    save_list = []
    while root or len(stack) > 0:
        if root:
            stack.append(root)
            save_list.insert(0,root.value)
            root = root.rchild
        else:
            root = stack.pop()
            root = root.lchild
    print(save_list,end=' ')

代碼比較簡單,跟前序遍歷形式基本一致。

層次遍歷

層次遍歷就是按層遞增的順序輸出每層的節點,即順序的輸出每層節點的左、右子節點。這里借助具有先進先出特性的隊列對象完成遍歷。

代碼示例:

#hierarchical traversal
def hierarchical_traversal(root):
    node_queue = queue.Queue()
    node_queue.put(root)
    while node_queue.qsize() > 0:
        root = node_queue.get()
        print(root.value,end=' ')
        if root.lchild:
            node_queue.put(root.lchild)
        if root.rchild:
            node_queue.put(root.rchild)

層次遍歷的邏輯比較簡單,即順序記錄每層節點,主要利用了隊列先進先出的特性。

附錄

以上是四種遍歷方式的描述和代碼示例,下面給出測試和輸出。其中樹結構 Tree 引用自 二叉搜索樹 中定義。

if __name__ == '__main__':
    arr = [5, 3, 4, 0, 2, 1, 8, 6, 9, 7]
    T = Tree()
    for i in arr:
        T.insert(i)

    print('BST recursive pre-order traversal:',end=' ')
    recursive_pre_order_traversal(T.root)
    print('\nBST non_recursive pre-order traversal:',end=' ')
    non_recursive_pre_order_traversal(T.root)

    print('\n\nBST recursive in-order traversal:',end=' ')
    recursive_in_order_traversal(T.root)
    print('\nBST non_recursive in-order traversal:',end=' ')
    non_recursive_in_order_traversal(T.root)

    print('\n\nBST recursive post-order traversal:',end=' ')
    recursive_post_order_traversal(T.root)
    print('\nBST non_recursive post-order traversal method one:',end=' ')
    non_recursive_post_order_traversal_one(T.root)
    print('\nBST non_recursive post-order traversal method two:',end=' ')
    non_recursive_post_order_traversal_two(T.root)

    print('\n\nBST hierarchical traversal:',end=' ')
    hierarchical_traversal(T.root)

輸出結果為:

BST recursive pre-order traversal: 5 3 0 2 1 4 8 6 7 9 
BST non_recursive pre-order traversal: 5 3 0 2 1 4 8 6 7 9 

BST recursive in-order traversal: 0 1 2 3 4 5 6 7 8 9 
BST non_recursive in-order traversal: 0 1 2 3 4 5 6 7 8 9 

BST recursive post-order traversal: 1 2 0 4 3 7 6 9 8 5 
BST non_recursive post-order traversal method one: 1 2 0 4 3 7 6 9 8 5 
BST non_recursive post-order traversal method two: [1, 2, 0, 4, 3, 7, 6, 9, 8, 5] 

BST hierarchical traversal: 5 3 8 0 4 6 9 2 7 1

github 鏈接:二叉樹遍歷

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容