數據結構(二):二叉搜索樹(Binary Search Tree)

二分法猜數字的游戲應該每個人都知道,通過對猜測數字“大了”、“小了”的情況判斷,來猜出最終的數字。序列范圍為 n 的集合,復雜度為 O(log_2 n),即最多需要 log_2 n 次可以猜到最終數字。

引子

二分法的查找過程是,在一個有序的序列中,每次都會選擇有效范圍中間位置的元素作判斷,即每次判斷后,都可以排除近一半的元素,直到查找到目標元素或返回不存在,所以 n 個有序元素構成的序列,查找的時間復雜度為 O(log_2 n)。既然線性結構能夠做到查詢復雜度為 O(log_2 n) 級別,那二叉搜索樹產生又有何必要呢?畢竟二叉搜索樹的查詢復雜度只是介于 O(log_2 n)~O(n) 之間,并不存在查詢優勢。

定義

二叉搜索樹是一種節點值之間具有一定數量級次序的二叉樹,對于樹中每個節點:

  • 若其左子樹存在,則其左子樹中每個節點的值都不大于該節點值;
  • 若其右子樹存在,則其右子樹中每個節點的值都不小于該節點值。

示例:


BST

查詢復雜度

觀察二叉搜索樹結構可知,查詢每個節點需要的比較次數為節點深度加一。如深度為 0,節點值為 “6” 的根節點,只需要一次比較即可;深度為 1,節點值為 “3” 的節點,只需要兩次比較。即二叉樹節點個數確定的情況下,整顆樹的高度越低,節點的查詢復雜度越低。

二叉搜索樹的兩種極端情況:

【1】 完全二叉樹,所有節點盡量填滿樹的每一層,上一層填滿后還有剩余節點的話,則由左向右盡量填滿下一層。如上圖BST所示,即為一顆完全二叉樹;
【2】每一層只有一個節點的二叉樹。如下圖SP_BST所示:

SP_BST

第【1】種情況下的查找次數分析:由上一章 二叉樹 可知,完美二叉樹中樹的深度與節點個數的關系為:n=2^{d+1}-1。設深度為 d 的完全二叉樹節點總數為 n_c,因為完全二叉樹中深度為 d 的葉子節點層不一定填滿,所以有 n_c \le 2^{d+1}-1,即:d+1 \ge log_2{(n_c+1)},因為 d+1 為查找次數,所以完全二叉樹中查找次數為:\lceil log_2{(n_c+1)} \rceil

第【2】種情況下,樹中每層只有一個節點,該狀態的樹結構更傾向于一種線性結構,節點的查詢類似于數組的遍歷,查詢復雜度為 O(n)

所以二叉搜索樹的查詢復雜度為 O(log_2 n)~O(n)

構造復雜度

二叉搜索樹的構造過程,也就是將節點不斷插入到樹中適當位置的過程。該操作過程,與查詢節點元素的操作基本相同,不同之處在于:

  • 查詢節點過程是,比較元素值是否相等,相等則返回,不相等則判斷大小情況,迭代查詢左、右子樹,直到找到相等的元素,或子節點為空,返回節點不存在
  • 插入節點的過程是,比較元素值是否相等,相等則返回,表示已存在,不相等則判斷大小情況,迭代查詢左、右子樹,直到找到相等的元素,或子節點為空,則將節點插入該空節點位置。

由此可知,單個節點的構造復雜度和查詢復雜度相同,為 O(log_2 n)~O(n)

刪除復雜度

二叉搜索樹的節點刪除包括兩個過程,查找和刪除。查詢的過程和查詢復雜度已知,這里說明一下刪除節點的過程。

節點的刪除有以下三種情況:
  1. 待刪除節點度為零;
  2. 待刪除節點度為一;
  3. 待刪除節點度為二。

第一種情況如下圖 s_1 所示,待刪除節點值為 “6”,該節點無子樹,刪除后并不影響二叉搜索樹的結構特性,可以直接刪除。即二叉搜索樹中待刪除節點度為零時,該節點為葉子節點,可以直接刪除;

s_1
s_1'

第二種情況如下圖 s_2 所示,待刪除節點值為 “7”,該節點有一個左子樹,刪除節點后,為了維持二叉搜索樹結構特性,需要將左子樹“上移”到刪除的節點位置上。即二叉搜索樹中待刪除的節點度為一時,可以將待刪除節點的左子樹或右子樹“上移”到刪除節點位置上,以此來滿足二叉搜索樹的結構特性。

s_2
s_2'

第三種情況如下圖 s_3 所示,待刪除節點值為 “9”,該節點既有左子樹,也有右子樹,刪除節點后,為了維持二叉搜索樹的結構特性,需要從其左子樹中選出一個最大值的節點,“上移”到刪除的節點位置上。即二叉搜索樹中待刪除節點的度為二時,可以將待刪除節點的左子樹中的最大值節點“移動”到刪除節點位置上,以此來滿足二叉搜索樹的結構特性。

其實在真實的實現代碼中,該情況下的實際節點刪除操作是:
1.查找出左子樹中的最大值節點 Node_{max}
2.替換待刪除節點 node 的值為 Node_{max} 的值
3.刪除 Node_{max} 節點
因為 Node_{max} 作為左子樹的最大值節點,所以節點的度一定是 0 或 1,所以刪除節點的情況就轉移為以上兩種情況。

s_3
s_3'

之前提到二叉搜索樹中節點的刪除操作,包括查詢和刪除兩個過程,這里稱刪除節點后,維持二叉搜索樹結構特性的操作為“穩定結構”操作,觀察以上三種情況可知:

  • 前兩種情況下,刪除節點后,“穩定結構”操作的復雜度都是常數級別,即整個的節點刪除操作復雜度為 O(log_2 n)~O(n)
  • 第三種情況下,設刪除的節點為 p,“穩定結構”操作需要查找 p 節點左子樹中的最大值,也就是左子樹中最“右”的葉子結點,即“穩定結構”操作其實也是一種內部的查詢操作,所以整個的節點刪除操作其實就是兩個層次的查詢操作,復雜度同為 O(log_2 n)~O(n)

性能分析

由以上查詢復雜度、構造復雜度和刪除復雜度的分析可知,三種操作的時間復雜度皆為 O(log_2 n)~O(n)。下面分析線性結構的三種操作復雜度,以二分法為例:

  • 查詢復雜度,時間復雜度為 O(log_2 n),優于二叉搜索樹;
  • 元素的插入操作包括兩個步驟,查詢和插入。查詢的復雜度已知,插入后調整元素位置的復雜度為 O(n),即單個元素的構造復雜度為:O(n)
  • 刪除操作也包括兩個步驟,查詢和刪除,查詢的復雜度已知,刪除后調整元素位置的復雜度為 O(n),即單個元素的刪除復雜度為:O(n)

由此可知,二叉搜索樹相對于線性結構,在構造復雜度和刪除復雜度方面占優;在查詢復雜度方面,二叉搜索樹可能存在類似于斜樹,每層上只有一個節點的情況,該情況下查詢復雜度不占優勢。

總結

二叉搜索樹的節點查詢、構造和刪除性能,與樹的高度相關,如果二叉搜索樹能夠更“平衡”一些,避免了樹結構向線性結構的傾斜,則能夠顯著降低時間復雜度。二叉搜索樹的存儲方面,相對于線性結構只需要保存元素值,樹中節點需要額外的空間保存節點之間的父子關系,所以在存儲消耗上要高于線性結構。

代碼附錄

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 鏈接:二叉搜索樹

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容

  • 一些概念 數據結構就是研究數據的邏輯結構和物理結構以及它們之間相互關系,并對這種結構定義相應的運算,而且確保經過這...
    Winterfell_Z閱讀 5,885評論 0 13
  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,125評論 0 12
  • 基于樹實現的數據結構,具有兩個核心特征: 邏輯結構:數據元素之間具有層次關系; 數據運算:操作方法具有Log級的平...
    yhthu閱讀 4,300評論 1 5
  • 香港購物網科技貿易有限公司【WWW.HKBUYER.HK】是一家專注於全球進出口商品購物的大型B2B2C網站,總部...
    別有根涯閱讀 507評論 0 1
  • <火藍刀鋒>,看了三遍了,每看一遍都有不同的感受,每看一遍都會回味里面的情節,其中一句話讓我印象深刻,龍百川說:你...
    小彬lv閱讀 304評論 0 1