資料結構之二叉樹——二叉查詢樹
定義
二叉查詢樹(Binary Search Tree),又稱為二叉搜尋樹,二叉排序樹。它可以是一棵空樹,如果不是空樹,則具有下列的性質:
- 非空左子樹的所有鍵值小於其根結點的鍵值
- 非空右子樹的所有鍵值大於其根結點的鍵值
- 左、右子樹都是二叉查詢樹
比如下面兩棵樹,左邊的樹,因為5小於10,應該在10的左子樹上,因此不是二叉查詢樹,右邊的樹則符合二叉查詢樹的條件。
由於二叉查詢樹的特性,中序遍歷二叉查詢樹,得到的就是一個升序的數列。以上圖右邊的樹為例,它的中序遍歷則是:15 30 33 41 50查詢
二叉查詢樹的查詢過程,可以分為下面的步驟:
- 從根結點開始,如果樹為空,則返回NULL
- 如果樹不為空,則根結點的值與查詢值X進行比較,並進行不同的處理
- 如果X等於根結點的值
- 如果X小於根結點的值,則在左子樹中繼續查詢
- 如果X大於根結點的值,則在右子樹中繼續查詢
- 如果X等於根結點的值
下圖以查詢33為例,展示了查詢的軌跡
根據上面的步驟,可以使用遞迴寫出該演演算法def find(root: TreeNode,key):
if not root:
return None
if key == root.val:
return root
elif key < root.val:
return find(root.left,key)
else:
return find(root.right,key)
複製程式碼
當然,上面的遞迴演演算法中存在尾遞迴,一般的編譯器都會自動優化尾遞迴,我們也可以將程式碼改為迭代的方式
def find(root: TreeNode,key):
if not root:
return None
while root:
if key == root.val:
return root
elif key < root.val:
root = root.left
else:
root = root.right
複製程式碼
可以看的出來,演演算法的複雜度和樹的高度有關,每一次對比之後都會捨棄掉原來資料的一半,因此演演算法的時間複雜為O(logn)
最小元素與最小元素
根據二叉查詢樹的性質:
- 最小元素一定是在樹的最左分枝的端結點上
- 最大元素一定是在樹的最右分枝的端結點上 演演算法的實現也比較清晰
def findMax(root: TreeNode):
if root:
while root.right:
root = root.right
return root
def findMin(root: TreeNode):
if root:
while root.left:
root = root.left
return root
複製程式碼
插入結點
由於二叉查詢樹具有特定的性質,因此在插入新的結點的時候,也要保證二叉查詢樹的性質,整個插入新結點的過程與查詢的過程類似,
以插入35為例,下圖展示了插入35的軌跡
以插入32為例,下圖展示例插入32的軌跡def insert(root: TreeNode,key):
if not root:
return TreeNode(key) # 如果是空樹,則新建根結點
while root:
if key == root.val:
break # 如果插入的key已經存在,則直接退出迴圈
elif key < root.val:
# 小於向左子樹查詢
if not root.left:
root.left = TreeNode(key)
else:
root = root.left
else:
# 大於向右子樹查詢
if not root.right:
root.right = TreeNode(key)
else:
root = root.right
return root
複製程式碼
刪除結點
刪除操作比較複雜,要分情況談論:
要刪除的結點為葉子結點
這種情況下,直接刪除。如下圖要刪除35結點
要刪除的結點只有一個孩子結點
這種情況下,直接將要刪除結點的孩子結點,向上提,替代要刪除的結點,如下圖要刪除33結點
要刪除的結點有左、右兩棵子樹
這種情況下,可以有兩種方案:
- 取左子樹的最大元素替代要刪除結點
- 取右子樹的最小元素替代要刪除結點
如果要刪除41結點(使用左子樹最大元素代替)
左子樹的最大元素,依舊比右子樹的所有元素小,但是比其他左子樹的元素大,因此它成為新的根結點的時候,依舊能保持左子樹下的所有元素比根結點小,右子樹下的所有元素比根結點大。
如果要刪除41結點(使用右子樹最小元素代替)
右子樹的最小元素,依舊比左子樹的所有元素大,但是比其他右子樹的元素小,成為新的根結點,依舊能保持二叉查詢樹的性質
綜合上面的情況,程式碼如下
def delete(root: TreeNode,key):
if not root:
return None
if key < root.val:
root.left = delete(root.left,key)
elif key > root.val:
root.right = delete(root.right,key)
else:
if root.left and root.right:
# 有左右孩子
leftMax = findMax(root.left)
root.val = leftMax.val
root.left = delete(root.left,leftMax.val)
elif not root.left and not root.right:
# 葉子結點
root = None
elif root.left:
# 只有左孩子
root = root.left
elif root.right:
# 只有右孩子
root = root.right
return root
複製程式碼
在有左右孩子的情況下,實際上只是把替代結點的值賦值給要刪除結點的值,真正刪除的是原來的替代結點,比如要刪除41結點,替代結點為35,則把41改為35,然後刪除原來的35結點。
總結
二叉查詢樹將資料按照順序組織好,排列成二叉樹結構,因此相關演演算法的複雜度基本取決於樹的高度,如果在構建二叉查詢樹的時候,不注意樹的高度,容易構建成一棵斜樹,這樣二叉查詢樹就失去了優勢。
二叉查詢樹有較高的插入和刪除效率,並且具備較高的查詢效率,對組織動態資料比較友好。
Thanks!