數(shù)據(jù)結(jié)構(gòu)(二):二叉搜索樹(Binary Search Tree)

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

引子

二分法的查找過程是,在一個有序的序列中猛蔽,每次都會選擇有效范圍中間位置的元素作判斷剥悟,即每次判斷后,都可以排除近一半的元素曼库,直到查找到目標元素或返回不存在区岗,所以 n 個有序元素構(gòu)成的序列,查找的時間復雜度為 O(log_2 n)毁枯。既然線性結(jié)構(gòu)能夠做到查詢復雜度為 O(log_2 n) 級別慈缔,那二叉搜索樹產(chǎn)生又有何必要呢?畢竟二叉搜索樹的查詢復雜度只是介于 O(log_2 n)~O(n) 之間种玛,并不存在查詢優(yōu)勢藐鹤。

定義

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

  • 若其左子樹存在赂韵,則其左子樹中每個節(jié)點的值都不大于該節(jié)點值娱节;
  • 若其右子樹存在,則其右子樹中每個節(jié)點的值都不小于該節(jié)點值祭示。

示例:


BST

查詢復雜度

觀察二叉搜索樹結(jié)構(gòu)可知肄满,查詢每個節(jié)點需要的比較次數(shù)為節(jié)點深度加一。如深度為 0,節(jié)點值為 “6” 的根節(jié)點稠歉,只需要一次比較即可掰担;深度為 1,節(jié)點值為 “3” 的節(jié)點怒炸,只需要兩次比較带饱。即二叉樹節(jié)點個數(shù)確定的情況下,整顆樹的高度越低横媚,節(jié)點的查詢復雜度越低纠炮。

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

【1】 完全二叉樹,所有節(jié)點盡量填滿樹的每一層灯蝴,上一層填滿后還有剩余節(jié)點的話,則由左向右盡量填滿下一層孝宗。如上圖BST所示穷躁,即為一顆完全二叉樹;
【2】每一層只有一個節(jié)點的二叉樹因妇。如下圖SP_BST所示:

SP_BST

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

第【2】種情況下谷炸,樹中每層只有一個節(jié)點北专,該狀態(tài)的樹結(jié)構(gòu)更傾向于一種線性結(jié)構(gòu),節(jié)點的查詢類似于數(shù)組的遍歷旬陡,查詢復雜度為 O(n)拓颓。

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

構(gòu)造復雜度

二叉搜索樹的構(gòu)造過程描孟,也就是將節(jié)點不斷插入到樹中適當位置的過程驶睦。該操作過程,與查詢節(jié)點元素的操作基本相同匿醒,不同之處在于:

  • 查詢節(jié)點過程是场航,比較元素值是否相等,相等則返回青抛,不相等則判斷大小情況旗闽,迭代查詢左、右子樹,直到找到相等的元素适室,或子節(jié)點為空嫡意,返回節(jié)點不存在
  • 插入節(jié)點的過程是,比較元素值是否相等捣辆,相等則返回蔬螟,表示已存在,不相等則判斷大小情況汽畴,迭代查詢左旧巾、右子樹,直到找到相等的元素忍些,或子節(jié)點為空鲁猩,則將節(jié)點插入該空節(jié)點位置。

由此可知罢坝,單個節(jié)點的構(gòu)造復雜度和查詢復雜度相同廓握,為 O(log_2 n)~O(n)

刪除復雜度

二叉搜索樹的節(jié)點刪除包括兩個過程嘁酿,查找和刪除隙券。查詢的過程和查詢復雜度已知,這里說明一下刪除節(jié)點的過程闹司。

節(jié)點的刪除有以下三種情況:
  1. 待刪除節(jié)點度為零娱仔;
  2. 待刪除節(jié)點度為一;
  3. 待刪除節(jié)點度為二游桩。

第一種情況如下圖 s_1 所示牲迫,待刪除節(jié)點值為 “6”,該節(jié)點無子樹众弓,刪除后并不影響二叉搜索樹的結(jié)構(gòu)特性恩溅,可以直接刪除。即二叉搜索樹中待刪除節(jié)點度為零時谓娃,該節(jié)點為葉子節(jié)點脚乡,可以直接刪除;

s_1
s_1'

第二種情況如下圖 s_2 所示滨达,待刪除節(jié)點值為 “7”奶稠,該節(jié)點有一個左子樹,刪除節(jié)點后捡遍,為了維持二叉搜索樹結(jié)構(gòu)特性锌订,需要將左子樹“上移”到刪除的節(jié)點位置上。即二叉搜索樹中待刪除的節(jié)點度為一時画株,可以將待刪除節(jié)點的左子樹或右子樹“上移”到刪除節(jié)點位置上辆飘,以此來滿足二叉搜索樹的結(jié)構(gòu)特性啦辐。

s_2
s_2'

第三種情況如下圖 s_3 所示,待刪除節(jié)點值為 “9”蜈项,該節(jié)點既有左子樹芹关,也有右子樹,刪除節(jié)點后紧卒,為了維持二叉搜索樹的結(jié)構(gòu)特性侥衬,需要從其左子樹中選出一個最大值的節(jié)點,“上移”到刪除的節(jié)點位置上跑芳。即二叉搜索樹中待刪除節(jié)點的度為二時轴总,可以將待刪除節(jié)點的左子樹中的最大值節(jié)點“移動”到刪除節(jié)點位置上,以此來滿足二叉搜索樹的結(jié)構(gòu)特性博个。

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

s_3
s_3'

之前提到二叉搜索樹中節(jié)點的刪除操作,包括查詢和刪除兩個過程罪塔,這里稱刪除節(jié)點后,維持二叉搜索樹結(jié)構(gòu)特性的操作為“穩(wěn)定結(jié)構(gòu)”操作养葵,觀察以上三種情況可知:

  • 前兩種情況下征堪,刪除節(jié)點后,“穩(wěn)定結(jié)構(gòu)”操作的復雜度都是常數(shù)級別关拒,即整個的節(jié)點刪除操作復雜度為 O(log_2 n)~O(n)佃蚜;
  • 第三種情況下,設(shè)刪除的節(jié)點為 p着绊,“穩(wěn)定結(jié)構(gòu)”操作需要查找 p 節(jié)點左子樹中的最大值谐算,也就是左子樹中最“右”的葉子結(jié)點,即“穩(wěn)定結(jié)構(gòu)”操作其實也是一種內(nèi)部的查詢操作归露,所以整個的節(jié)點刪除操作其實就是兩個層次的查詢操作洲脂,復雜度同為 O(log_2 n)~O(n)

性能分析

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

  • 查詢復雜度一铅,時間復雜度為 O(log_2 n),優(yōu)于二叉搜索樹堕油;
  • 元素的插入操作包括兩個步驟潘飘,查詢和插入肮之。查詢的復雜度已知,插入后調(diào)整元素位置的復雜度為 O(n)卜录,即單個元素的構(gòu)造復雜度為:O(n)
  • 刪除操作也包括兩個步驟戈擒,查詢和刪除,查詢的復雜度已知暴凑,刪除后調(diào)整元素位置的復雜度為 O(n)峦甩,即單個元素的刪除復雜度為:O(n)

由此可知,二叉搜索樹相對于線性結(jié)構(gòu)现喳,在構(gòu)造復雜度和刪除復雜度方面占優(yōu)凯傲;在查詢復雜度方面,二叉搜索樹可能存在類似于斜樹嗦篱,每層上只有一個節(jié)點的情況冰单,該情況下查詢復雜度不占優(yōu)勢。

總結(jié)

二叉搜索樹的節(jié)點查詢灸促、構(gòu)造和刪除性能诫欠,與樹的高度相關(guān),如果二叉搜索樹能夠更“平衡”一些浴栽,避免了樹結(jié)構(gòu)向線性結(jié)構(gòu)的傾斜荒叼,則能夠顯著降低時間復雜度。二叉搜索樹的存儲方面典鸡,相對于線性結(jié)構(gòu)只需要保存元素值被廓,樹中節(jié)點需要額外的空間保存節(jié)點之間的父子關(guān)系,所以在存儲消耗上要高于線性結(jié)構(gòu)萝玷。

代碼附錄

python版本:3.7嫁乘,樹中的遍歷、節(jié)點插入和刪除操作使用的是遞歸形式

  • 樹節(jié)點定義
# 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)
  • 模塊中對樹結(jié)構(gòu)中的函數(shù)進行實現(xiàn)
# 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()

輸出結(jié)果為:

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末球碉,一起剝皮案震驚了整個濱河市蜓斧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌睁冬,老刑警劉巖挎春,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異痴突,居然都是意外死亡搂蜓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門辽装,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帮碰,“玉大人,你說我怎么就攤上這事拾积⊙惩欤” “怎么了丰涉?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長斯碌。 經(jīng)常有香客問我一死,道長,這世上最難降的妖魔是什么傻唾? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任投慈,我火速辦了婚禮,結(jié)果婚禮上冠骄,老公的妹妹穿的比我還像新娘伪煤。我一直安慰自己,他們只是感情好凛辣,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布抱既。 她就那樣靜靜地躺著,像睡著了一般扁誓。 火紅的嫁衣襯著肌膚如雪防泵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天蝗敢,我揣著相機與錄音捷泞,去河邊找鬼。 笑死寿谴,一個胖子當著我的面吹牛肚邢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拭卿,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贱纠!你這毒婦竟也來了峻厚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤谆焊,失蹤者是張志新(化名)和其女友劉穎惠桃,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辖试,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡辜王,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了罐孝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呐馆。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖莲兢,靈堂內(nèi)的尸體忽然破棺而出汹来,到底是詐尸還是另有隱情续膳,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布收班,位于F島的核電站坟岔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏摔桦。R本人自食惡果不足惜社付,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望邻耕。 院中可真熱鬧鸥咖,春花似錦、人聲如沸赊豌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碘饼。三九已至熙兔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間艾恼,已是汗流浹背住涉。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钠绍,地道東北人舆声。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像柳爽,于是被迫代替她去往敵國和親媳握。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容