1. 程式人生 > >二叉樹遍歷理解——遞迴及非遞迴方法中棧的利用

二叉樹遍歷理解——遞迴及非遞迴方法中棧的利用

1.二叉樹介紹

二叉樹是每個節點最多有兩個子樹的樹結構,遍歷方法有深度優先(包括:先序、中序、後序遍歷)和寬度優先(層序遍歷),層序遍歷通過佇列可以實現。這裡主要介紹深度優先遍歷的方法以及其中棧的應用,幫助理解二叉樹的結構、遞迴和非遞迴中棧的應用。程式python 3。

2.遞迴遍歷

先序遍歷:
    def pre_order(Tree, proc):  #proc是具體的節點資料操作
        if Tree is None:
            return
        proc(Tree.date)             #**位置1**
        pre_order(Tree.left, proc)
                                    #**位置2**
pre_order(Tree.right, proc) #**位置3**
這就是一個遞迴實現先序遍歷。當把proc從位置1分別移到位置2和位置3時,就變成了中序遍歷和後序遍歷。
注意,在遞迴函式中,只需要把具體的節點操作改變位置,就能實現先序、中序和後序遍歷,這是因為執行遞迴時,遞迴演算法中的棧儲存了函式的狀態,包括引數,返回地址。它在訪問了左右子樹和該節點的值後,棧才釋放了這個節點的資料。

3.非遞迴遍歷

    def pre_order_nonrec(Tree, proc):
        s = SStack                #SStack是一個棧類
while Tree is not None or not s.is_empty(): #當數不為空且棧不空時 while Tree: s.push(Tree) Tree = Tree.left #**位置2** if not s.is_empty() #棧不空 Tree = s.pop() proc(Tree.date) #**位置1**
Tree = Tree.right
當proc(Tree.date) 在位置一時,實現了中序遍歷,將他移到位置二時,實現了先序遍歷。那能否移動proc的位置實現後序遍歷呢?**這是不可能的。**
首先,我們要了解先序、中序、後序遍歷的實質,看下圖,

樹中各節點的訪問路徑(摘自MOOC網浙江大學資料結構一課)
樹中各節點的訪問路徑(摘自MOOC網浙江大學資料結構一課)
可以看到,沿著整個樹的外部輪廓,先序時在第一次遇見節點時訪問他,中序是第二次遇到節點是訪問他,後序時第三次遇到才訪問。
上面的非遞迴遍歷演算法用棧儲存節點,只能在入棧和出棧的時候可以訪問他,出棧後(兩次訪問後)就將這個節點丟掉,不能第三次去訪問他,而後序遍歷就是在第三次訪問他,所以上面的方法不能實現後序遍歷。
在採用遞迴方法時,同樣是棧,為什麼他能第三次訪問呢?這是因為遞迴函式的棧儲存了函式的狀態,包括引數,返回地址,是層層巢狀的,它在訪問了該節點後,並沒有丟掉這些資料,而是在,分別訪問完左子樹和右子樹後才丟掉了這個節點的相關資料。
詳細來說,在遞迴演算法中實際存在的那個棧中,訪問了當前節點(proc生效)之後並沒有彈出引數為這個節點的函式!因為需要再次呼叫自身(引數為T->right),而且當前函式並未結束。
為什麼這種非遞迴遍歷方法又正確呢?以先序遍歷為例說明。考慮某時刻某個節點Tree(也可以看成一棵子樹),按照先序遍歷規則先訪問Tree,如果之後左子樹為空,將Tree.right入棧。當子樹Tree.right遍歷完成並返回Tree這一層函式的時候,Tree這一層的函式也結尾了,那麼需要pop出並將控制權交給上一層。在前序非遞迴演算法中,Tree被訪問後直接出棧,當其右子樹Tree.right訪問完成後,Ti本身就不在棧裡面了,在這時省去了一個pop的流程。
所以,要用非遞迴方式實現後序遍歷,則需要將節點入棧兩次,這樣才能他對他進行第三次訪問,這可以通過給Tree增加狀態量來實現第二次入棧,如有0/1/2三種狀態,遇到節點時檢查其狀態,遇到0/1狀態,入棧且狀態值加一,變成1/2,遇到2狀態,不需要再入棧。
最後,瞭解了二叉樹遍歷的實質後,我們可以對之前的先序遍歷化簡。

    def pre_order_nonrec(Tree, proc):
        s = SStack                #SStack是一個棧類
        while Tree is not None or not s.is_empty():                     #當數不為空且棧不空時
            while Tree:
                proc(Tree.date)
                s.push(Tree.right)
                Tree = Tree.left
                                  #**位置2**
            if not s.is_empty()   #棧不空
                Tree = s.pop()
化簡的方法就是直接將右子樹入棧,而不是將該節點入棧且彈出時Tree = Tree.right。 這正是前面解釋的,先序遍歷只需要在第一次經過節點時訪問。

**總結:三種寬度優先遍歷方法的實質就是在如圖所示的路徑中第幾次訪問節點。所以對於上面的方法,        
遞迴遍歷能實現第三次訪問,可以改變proc位置分別實現先序、中序、後序;
未化簡的非遞迴演算法,在入棧和出棧是能兩次訪問到該節點,所以可以改變proc位置分別實現先序、中序遍歷; 
而化簡的非遞迴遍歷方法,未將該節點入棧,只能訪問一次,所以只能實現先序遍歷。**