資料結構之二叉樹——遍歷(遞迴與非遞迴)
定義
二叉樹,一個有窮的結點集合。這個集合可以為空,如果不為空,則它是由根結點和稱其為左子樹和右子樹的兩個不相交的二叉樹組成。
二叉樹有五種基本形態:
順序儲存
完全二叉樹可以使用順序儲存結構,按從上至下,從左到右順序儲存,如果一顆完全二叉樹如下:
那麼順序儲存可以為: 由上面的結構,可以得到,n個結點的完全二叉樹的結點父子關係:- 非根結點(序號i>1)的父節點的序號為⌊i/2⌋
- 結點(序號為i)的左孩子結點的序號為2i(如果2i>n,則沒有左孩子)
- 結點(序號為i)的右孩子結點的序號為2i+1(如果2i+1>n,則沒有右孩子)
一般二叉樹也可以使用順序儲存,只是會造成空間的浪費。
鏈式儲存
由於一般的二叉樹使用順序儲存結構,容易造成空間的浪費,因此可以使用鏈式儲存。其結構如下
class TreeNode:
def __init__(self,x,left=None,right=None):
self.val = x # 值
self.left = left # 左孩子
self.right = right # 右孩子
複製程式碼
遍歷
由於二叉樹不是線性結構,因此它的遍歷也就不像陣列或者連結串列那麼簡單,它需要沿著某條搜尋路線,依次對樹中每個結點均做一次且僅做一次訪問。
前序遍歷
前序遍歷的遍歷過程:
- 訪問根結點
- 先序遍歷其左子樹
- 先序遍歷其右子樹
使用遞迴的方式實現如下:
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
中序遍歷
中序遍歷的遍歷過程:
- 中序遍歷其左子樹
- 訪問根結點
- 中序遍歷其右子樹
遞迴程式碼
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
後序遍歷
後序遍歷的遍歷過程:
- 後序遍歷其左子樹
- 後序遍歷其右子樹
- 訪問根結點
遞迴程式碼
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
遍歷的思考
從上面三幅圖的訪問路徑,可以看出,不論是前,中,後序遍歷,在遍歷過程中經過結點的路線都是一樣的,只是訪問各結點的時機不同
下圖使用ⓧ,☆,△三種符號分別標記出,前序,中序,後序訪問各結點的時刻
非遞迴中序遍歷
使用遞迴的方式可以比較容易的寫出遍歷演演算法,那麼如果不用遞迴呢?
我們知道,遞迴的實現需要藉助棧,那麼可以用棧+迴圈的方式來實現遍歷演演算法。
以中序遍歷為列:
- 遇到一個結點,因為不知道是否還有左子樹,因此將它壓棧,並去遍歷它的左子樹
- 當它的左子樹遍歷完了,從棧頂彈出這個結點並訪問它
- 然後轉向這個結點的右子樹繼續遍歷,相當於又回到第一步,直到遍歷完整棵樹
仍然以上面的二叉樹為例,來看看非遞迴的中序遍歷的執行過程:
- 結點A,壓棧
- A有左結點B,B壓棧
- B有左結點D,D壓棧
- 結點D,沒有左結點,則彈出棧頂元素D,並訪問
- 轉向D的右子樹,但是D沒有右子樹,因此繼續彈出棧頂元素B並訪問,
- 轉向B的右結點F,F壓棧
- F有左結點E,E壓棧
- E沒有左結點,彈出棧頂元素E並訪問
- 轉向E的右結點,沒有,繼續彈出F
- 轉向F的右結點,沒有,繼續彈出A
- 轉向A的右結點C,C壓棧
- C有左結點G,G壓棧
- G沒有左結點,彈出G並訪問
- 轉向G的右結點H,H壓棧
- H沒有左結點,彈出H並訪問
- H沒有右結點,繼續彈出,彈出C訪問
- 轉向C的右結點I,I壓棧
- I沒有左結點,彈出I訪問
- 轉向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!