數據結構之二叉樹——遍歷(遞歸與非遞歸)

定義

二叉樹,一個有窮的結點集合。這個集合能夠爲空,若是不爲空,則它是由根結點和稱其爲左子樹右子樹的兩個不相交的二叉樹組成。node

二叉樹有五種基本形態:python

順序存儲

徹底二叉樹可使用順序存儲結構,按從上至下,從左到右順序存儲,若是一顆徹底二叉樹以下:算法

那麼順序存儲能夠爲:

由上面的結構,能夠獲得,n個結點的徹底二叉樹的結點父子關係:

  • 非根結點(序號i>1)的父節點的序號爲⌊i/2⌋
  • 結點(序號爲i)的左孩子結點的序號爲2i(若是2i>n,則沒有左孩子)
  • 結點(序號爲i)的右孩子結點的序號爲2i+1(若是2i+1>n,則沒有右孩子)

通常二叉樹也可使用順序存儲,只是會形成空間的浪費。數組

鏈式存儲

因爲通常的二叉樹使用順序存儲結構,容易形成空間的浪費,所以可使用鏈式存儲。其結構以下app

class TreeNode:
    def __init__(self, x, left=None, right=None):
        self.val = x  # 值
        self.left = left  # 左孩子
        self.right = right  # 右孩子
複製代碼

遍歷

因爲二叉樹不是線性結構,所以它的遍歷也就不像數組或者鏈表那麼簡單,它須要沿着某條搜索路線,依次對樹中每一個結點均作一次且僅作一次訪問。post

前序遍歷

前序遍歷的遍歷過程:spa

  1. 訪問根結點
  2. 先序遍歷其左子樹
  3. 先序遍歷其右子樹

使用遞歸的方式實現以下:3d

def pre_order_traversal(root: TreeNode):
    if root:
        print(root.val)
        pre_order_traversal(root.left)
        pre_order_traversal(root.right)
複製代碼

該樹的前序遍歷爲:A B D F E C G H I

中序遍歷

中序遍歷的遍歷過程:code

  1. 中序遍歷其左子樹
  2. 訪問根結點
  3. 中序遍歷其右子樹

遞歸代碼cdn

def in_order_traversal(root: TreeNode):
    if root:
        in_order_traversal(root.left)
        print(root.val)
        in_order_traversal(root.right)
複製代碼

該樹的中序序遍歷爲:D B E F A G H C I

後序遍歷

後序遍歷的遍歷過程:

  1. 後序遍歷其左子樹
  2. 後序遍歷其右子樹
  3. 訪問根結點

遞歸代碼

def post_order_traversal(root: TreeNode):
    if root:
        post_order_traversal(root.left)
        post_order_traversal(root.right)
        print(root.val)
複製代碼

該樹的後序序遍歷爲:D E F B H G I C A

遍歷的思考

從上面三幅圖的訪問路徑,能夠看出,不管是前,中,後序遍歷,在遍歷過程當中通過結點的路線都是同樣的,只是訪問各結點的時機不一樣

下圖使用ⓧ,☆,△三種符號分別標記出,前序,中序,後序訪問各結點的時刻

非遞歸中序遍歷

使用遞歸的方式能夠比較容易的寫出遍歷算法,那麼若是不用遞歸呢?

咱們知道,遞歸的實現須要藉助棧,那麼能夠用棧+循環的方式來實現遍歷算法。

以中序遍歷爲列:

  • 遇到一個結點,由於不知道是否還有左子樹,所以將它壓棧,並去遍歷它的左子樹
  • 當它的左子樹遍歷完了,從棧頂彈出這個結點並訪問它
  • 而後轉向這個結點的右子樹繼續遍歷,至關於又回到第一步,直到遍歷完整棵樹

仍然以上面的二叉樹爲例,來看看非遞歸的中序遍歷的運行過程:

  1. 結點A,壓棧
  2. A有左結點B,B壓棧
  3. B有左結點D,D壓棧
  4. 結點D,沒有左結點,則彈出棧頂元素D,並訪問
  5. 轉向D的右子樹,可是D沒有右子樹,所以繼續彈出棧頂元素B並訪問,
  6. 轉向B的右結點F,F壓棧
  7. F有左結點E,E壓棧
  8. E沒有左結點,彈出棧頂元素E並訪問
  9. 轉向E的右結點,沒有,繼續彈出F
  10. 轉向F的右結點,沒有,繼續彈出A
  11. 轉向A的右結點C,C壓棧
  12. C有左結點G,G壓棧
  13. G沒有左結點,彈出G並訪問
  14. 轉向G的右結點H,H壓棧
  15. H沒有左結點,彈出H並訪問
  16. H沒有右結點,繼續彈出,彈出C訪問
  17. 轉向C的右結點I,I壓棧
  18. I沒有左結點,彈出I訪問
  19. 轉向I的右結點,沒有,棧也爲空,遍歷結束
def in_order_traversal(root: TreeNode):
    stack = []
    while root or stack:
        while root:
            stack.append(root)
            root = root.left
        if stack:
            node = stack.pop()
            print(node.val)
            root = node.right
複製代碼

非遞歸前序遍歷

中序遍歷是在第二次通過結點的時候,才訪問該結點的,所以參照中序遍歷的非遞歸算法,把print語句移到第一次通過結點時,就訪問該結點,那麼非遞歸前序遍歷的實現也就出來了。

def pre_order_traversal(root: TreeNode):
    stack = []
    while root or stack:
        while root:
            stack.append(root)
            print(node.val)
            root = root.left
        if stack:
            node = stack.pop()
            root = node.right
複製代碼

非遞歸後序遍歷

非遞歸後序遍歷比較複雜,並且實現的方式也有多種,這裏提供一個比較好理解的標記法。根據後序遍歷的定義,要先訪問完左子樹,再訪問完右子樹,最後才訪問根結點,那麼仍是套以前的代碼結構,可是作個標記,在彈出元素的時候,判斷是否有右結點或者右結點是否被訪問過,若是知足則訪問該結點,不知足就將它再次壓回棧中,並轉向它的右結點

def post_order_traversal(root: TreeNode):
    stack = []
    visited_node = None # 前一個被訪問的結點
    while root or stack:
        while root:
            stack.append(root)
            root = root.left
        if stack:
            node = stack.pop()
            if not node.right or node.right == visited_node:
                # 沒有右孩子或者右孩子已經被訪問了,才訪問該結點
                print(node.val)
                visited_node = node
            else:
                # 不然就將該結點從新壓回棧了,並轉向它的右結點
                stack.append(node)
                root = node.right
複製代碼

層次遍歷

二叉樹的遍歷,除了上面三種以外,還有一種層次遍歷,即一層一層的訪問

該樹的層次遍歷:A B C D F G I E H

算法實現能夠藉助隊列實現,先根結點入隊,而後:

  • 從隊列中取出一個元素
  • 訪問該元素
  • 若是該元素有左、右結點,則將其左右結點順序入隊
  • 直到隊列爲空
def level_order_traversal(root: TreeNode):
    queue = [root]
    while queue:
        node = queue.pop(0)
        print(node.val)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)
複製代碼

總結

實際上二叉樹的遍歷核心問題:二維結構的線性化,當訪問一個結點的時候,還須要訪問其左右結點,可是訪問左結點以後,若是再返回訪問其右結點? 所以須要一個存儲結構保存暫時不訪問的結點,那麼存儲結構能夠爲棧,或者隊列,對應的就有前,中,後,層次遍歷的出現。

二叉樹的遍歷有許多應用,好比:輸出二叉樹中的葉子結點,求二叉樹的高度等等,所以遍歷對二叉樹來講是十分重要的。

Thanks!

相關文章
相關標籤/搜索