二分法猜數字的游戲應該每個人都知道,通過對猜測數字“大了”、“小了”的情況判斷,來猜出最終的數字。序列范圍為
的集合,復雜度為
,即最多需要
次可以猜到最終數字。
引子
二分法的查找過程是,在一個有序的序列中,每次都會選擇有效范圍中間位置的元素作判斷,即每次判斷后,都可以排除近一半的元素,直到查找到目標元素或返回不存在,所以 個有序元素構成的序列,查找的時間復雜度為
。既然線性結構能夠做到查詢復雜度為
級別,那二叉搜索樹產生又有何必要呢?畢竟二叉搜索樹的查詢復雜度只是介于
~
之間,并不存在查詢優勢。
定義
二叉搜索樹是一種節點值之間具有一定數量級次序的二叉樹,對于樹中每個節點:
- 若其左子樹存在,則其左子樹中每個節點的值都不大于該節點值;
- 若其右子樹存在,則其右子樹中每個節點的值都不小于該節點值。
示例:
查詢復雜度
觀察二叉搜索樹結構可知,查詢每個節點需要的比較次數為節點深度加一。如深度為 0,節點值為 “6” 的根節點,只需要一次比較即可;深度為 1,節點值為 “3” 的節點,只需要兩次比較。即二叉樹節點個數確定的情況下,整顆樹的高度越低,節點的查詢復雜度越低。
二叉搜索樹的兩種極端情況:
【1】 完全二叉樹,所有節點盡量填滿樹的每一層,上一層填滿后還有剩余節點的話,則由左向右盡量填滿下一層。如上圖BST所示,即為一顆完全二叉樹;
【2】每一層只有一個節點的二叉樹。如下圖SP_BST所示:
第【1】種情況下的查找次數分析:由上一章 二叉樹 可知,完美二叉樹中樹的深度與節點個數的關系為:。設深度為
的完全二叉樹節點總數為
,因為完全二叉樹中深度為
的葉子節點層不一定填滿,所以有
,即:
,因為
為查找次數,所以完全二叉樹中查找次數為:
。
第【2】種情況下,樹中每層只有一個節點,該狀態的樹結構更傾向于一種線性結構,節點的查詢類似于數組的遍歷,查詢復雜度為 。
所以二叉搜索樹的查詢復雜度為 ~
。
構造復雜度
二叉搜索樹的構造過程,也就是將節點不斷插入到樹中適當位置的過程。該操作過程,與查詢節點元素的操作基本相同,不同之處在于:
- 查詢節點過程是,比較元素值是否相等,相等則返回,不相等則判斷大小情況,迭代查詢左、右子樹,直到找到相等的元素,或子節點為空,返回節點不存在
- 插入節點的過程是,比較元素值是否相等,相等則返回,表示已存在,不相等則判斷大小情況,迭代查詢左、右子樹,直到找到相等的元素,或子節點為空,則將節點插入該空節點位置。
由此可知,單個節點的構造復雜度和查詢復雜度相同,為 ~
。
刪除復雜度
二叉搜索樹的節點刪除包括兩個過程,查找和刪除。查詢的過程和查詢復雜度已知,這里說明一下刪除節點的過程。
節點的刪除有以下三種情況:
- 待刪除節點度為零;
- 待刪除節點度為一;
- 待刪除節點度為二。
第一種情況如下圖 s_1 所示,待刪除節點值為 “6”,該節點無子樹,刪除后并不影響二叉搜索樹的結構特性,可以直接刪除。即二叉搜索樹中待刪除節點度為零時,該節點為葉子節點,可以直接刪除;
第二種情況如下圖 s_2 所示,待刪除節點值為 “7”,該節點有一個左子樹,刪除節點后,為了維持二叉搜索樹結構特性,需要將左子樹“上移”到刪除的節點位置上。即二叉搜索樹中待刪除的節點度為一時,可以將待刪除節點的左子樹或右子樹“上移”到刪除節點位置上,以此來滿足二叉搜索樹的結構特性。
第三種情況如下圖 s_3 所示,待刪除節點值為 “9”,該節點既有左子樹,也有右子樹,刪除節點后,為了維持二叉搜索樹的結構特性,需要從其左子樹中選出一個最大值的節點,“上移”到刪除的節點位置上。即二叉搜索樹中待刪除節點的度為二時,可以將待刪除節點的左子樹中的最大值節點“移動”到刪除節點位置上,以此來滿足二叉搜索樹的結構特性。
其實在真實的實現代碼中,該情況下的實際節點刪除操作是:
1.查找出左子樹中的最大值節點
2.替換待刪除節點的值為
的值
3.刪除節點
因為作為左子樹的最大值節點,所以節點的度一定是 0 或 1,所以刪除節點的情況就轉移為以上兩種情況。
之前提到二叉搜索樹中節點的刪除操作,包括查詢和刪除兩個過程,這里稱刪除節點后,維持二叉搜索樹結構特性的操作為“穩定結構”操作,觀察以上三種情況可知:
- 前兩種情況下,刪除節點后,“穩定結構”操作的復雜度都是常數級別,即整個的節點刪除操作復雜度為
~
;
- 第三種情況下,設刪除的節點為
,“穩定結構”操作需要查找
節點左子樹中的最大值,也就是左子樹中最“右”的葉子結點,即“穩定結構”操作其實也是一種內部的查詢操作,所以整個的節點刪除操作其實就是兩個層次的查詢操作,復雜度同為
~
;
性能分析
由以上查詢復雜度、構造復雜度和刪除復雜度的分析可知,三種操作的時間復雜度皆為 ~
。下面分析線性結構的三種操作復雜度,以二分法為例:
- 查詢復雜度,時間復雜度為
,優于二叉搜索樹;
- 元素的插入操作包括兩個步驟,查詢和插入。查詢的復雜度已知,插入后調整元素位置的復雜度為
,即單個元素的構造復雜度為:
- 刪除操作也包括兩個步驟,查詢和刪除,查詢的復雜度已知,刪除后調整元素位置的復雜度為
,即單個元素的刪除復雜度為:
由此可知,二叉搜索樹相對于線性結構,在構造復雜度和刪除復雜度方面占優;在查詢復雜度方面,二叉搜索樹可能存在類似于斜樹,每層上只有一個節點的情況,該情況下查詢復雜度不占優勢。
總結
二叉搜索樹的節點查詢、構造和刪除性能,與樹的高度相關,如果二叉搜索樹能夠更“平衡”一些,避免了樹結構向線性結構的傾斜,則能夠顯著降低時間復雜度。二叉搜索樹的存儲方面,相對于線性結構只需要保存元素值,樹中節點需要額外的空間保存節點之間的父子關系,所以在存儲消耗上要高于線性結構。
代碼附錄
python版本:3.7,樹中的遍歷、節點插入和刪除操作使用的是遞歸形式
- 樹節點定義
# tree node definition
class Node(object):
def __init__(self, value, lchild=None, rchild=None):
self.value = value
self.lchild = lchild
self.rchild = rchild
- 樹定義
# tree definition
class Tree(object):
def __init__(self, root=None):
self.root = root
# node in-order traversal(LDR)
def traversal(self):
traversal(self.root)
# insert node
def insert(self, value):
self.root = insert(self.root, value)
# delete node
def delete(self, value):
self.root = delete(self.root, value)
- 模塊中對樹結構中的函數進行實現
# node in-order traversal(LDR)
def traversal(node):
if not node:
return
traversal(node.lchild)
print(node.value,end=' ')
traversal(node.rchild)
# insert node
def insert(root, value):
if not root:
return Node(value)
if value < root.value:
root.lchild = insert(root.lchild, value)
elif value > root.value:
root.rchild = insert(root.rchild, value)
return root
# delete node
def delete(root, value):
if not root:
return None
if value < root.value:
root.lchild = delete(root.lchild, value)
elif value > root.value:
root.rchild = delete(root.rchild, value)
else:
if root.lchild and root.rchild: # degree of the node is 2
target = root.lchild # find the maximum node of the left subtree
while target.rchild:
target = target.rchild
root = delete(root, target.value)
root.value = target.value
else: # degree of the node is [0|1]
root = root.lchild if root.lchild else root.rchild
return root
- 測試代碼與輸出
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 in-order traversal------------------')
T.traversal()
print('\ndelete test------------------')
for i in arr[::-1]:
print('after delete',i,end=',BST in-order is = ')
T.delete(i)
T.traversal()
print()
輸出結果為:
BST in-order traversal------------------
0 1 2 3 4 5 6 7 8 9
delete test------------------
after delete 7,BST in-order is = 0 1 2 3 4 5 6 8 9
after delete 9,BST in-order is = 0 1 2 3 4 5 6 8
after delete 6,BST in-order is = 0 1 2 3 4 5 8
after delete 8,BST in-order is = 0 1 2 3 4 5
after delete 1,BST in-order is = 0 2 3 4 5
after delete 2,BST in-order is = 0 3 4 5
after delete 0,BST in-order is = 3 4 5
after delete 4,BST in-order is = 3 5
after delete 3,BST in-order is = 5
after delete 5,BST in-order is =
github
鏈接:二叉搜索樹