數(shù)據(jù)結(jié)構(gòu)(四):平衡二叉樹(AVL樹)

通過之前對二叉搜索樹介紹可知畔塔,將集合構(gòu)造為二叉搜索樹結(jié)構(gòu),該結(jié)構(gòu)下對樹中節(jié)點的查詢、刪除和插入三種操作澈吨,時間復(fù)雜度均為 O(log_2N)~O(N)把敢。影響時間復(fù)雜度的因素即為二叉樹的高,為了盡量避免樹中每層上只有一個節(jié)點的情況棚辽,這里引入平衡二叉樹技竟。

定義

平衡二叉樹也叫自平衡二叉搜索樹(Self-Balancing Binary Search Tree),所以其本質(zhì)也是一顆二叉搜索樹屈藐,不過為了限制左右子樹的高度差榔组,避免出現(xiàn)傾斜樹等偏向于線性結(jié)構(gòu)演化的情況,所以對二叉搜索樹中每個節(jié)點的左右子樹作了限制联逻,左右子樹的高度差稱之為平衡因子搓扯,樹中每個節(jié)點的平衡因子絕對值不大于 1,此時二叉搜索樹稱之為平衡二叉樹包归。

自平衡是指锨推,在對平衡二叉樹執(zhí)行插入或刪除節(jié)點操作后,可能會導(dǎo)致樹中某個節(jié)點的平衡因子絕對值超過 1公壤,即平衡二叉樹變得“不平衡”呐赡,為了恢復(fù)該節(jié)點左右子樹的平衡翔试,此時需要對節(jié)點執(zhí)行旋轉(zhuǎn)操作宦棺。

情景分析

在執(zhí)行插入或刪除節(jié)點操作后施流,平衡因子絕對值變?yōu)榇笥?1 的情況,即左右子樹的高度差為 -22 的情況确憨,可以歸納為如下四種:

  • 左左情況(LL)

LL 情況是指根節(jié)點的平衡因子為 2译荞,根節(jié)點的左子節(jié)點平衡因子為 01

LL_1

如圖 LL_1 所示休弃,當(dāng)節(jié)點 C 的子節(jié)點被刪除吞歼,或者節(jié)點 D 插入子節(jié)點 F 時,根節(jié)點 A 的平衡因子變?yōu)?2塔猾,A 的左子節(jié)點 B 的平衡因子為 1篙骡。

LL_2

或者如圖 LL_2 所示,當(dāng)節(jié)點 C 的子節(jié)點被刪除丈甸,根節(jié)點 A 的平衡因子變?yōu)?2医增,A 的左子節(jié)點 B 的平衡因子為 0

當(dāng)根節(jié)點的左子樹高度比右子樹的高度大 2老虫,因為平衡二叉樹是一種有序結(jié)構(gòu),節(jié)點值之間具有大小關(guān)系茫多,所以如果根節(jié)點保持不變祈匙,左右子樹始終分隔兩岸,則無論如何調(diào)整節(jié)點位置,二叉樹始終不可能恢復(fù)平衡夺欲。所以需要更換根節(jié)點跪帝,使得新的根節(jié)點的左右子樹的高度趨于平衡。

該情況下需要對平衡二叉樹執(zhí)行右旋操作:

  1. 設(shè)置根節(jié)點 root 的左子節(jié)點為新的根節(jié)點 root_{new}些阅;
  2. root_{new} 節(jié)點的右子樹作為 root 節(jié)點的左子樹伞剑,將 root 節(jié)點作為 root_{new} 的右子樹,即降低“左子樹”高度市埋,提升“右子樹”高度黎泣,使得新的左右子樹高度趨于平衡;

對于圖 LL_1缤谎,節(jié)點 B 的平衡因子為 1抒倚,設(shè) B 節(jié)點的左子樹 D 高度為 h,則右子樹 E 高度為h-1坷澡,因為 A 的平衡因子為 2托呕,所以二叉樹 C 的高度為: h-1。則右旋操作后频敛,B 的左子樹高度不變?yōu)?h项郊,右子樹高度為:1+max(height(C),height(E))=h,此時二叉樹為平衡二叉樹斟赚,如下圖 balanced_LL_1着降。

balanced_LL_1

對于圖 LL_2,節(jié)點 B 的平衡因子為 0汁展,設(shè) B 節(jié)點的左右子樹高度為 h鹊碍,則二叉樹 C 的高度為: h-1。右旋操作后食绿,B 的左子樹高度不變?yōu)?h侈咕,右子樹高度為:1+max(height(C),height(E))=h+1,此時二叉樹為平衡二叉樹器紧,如下圖 balanced_LL_2耀销。

balanced_LL_2
右旋代碼
# rotate from left to right with the left-child node as the axis
def rotateL2R(node):
    leftChild = node.lchild
    leftChild.rchild,node.lchild = node,leftChild.rchild   # rotate
    updateHeight(node)
    updateHeight(leftChild)
    return leftChild

其中 updateHeight 函數(shù)作用為更新調(diào)整后節(jié)點的平衡因子,因為右旋操作平衡因子變化的節(jié)點只有兩個:原根節(jié)點和新根節(jié)點铲汪,即根節(jié)點和根節(jié)點的左子節(jié)點熊尉,所以只需要對這兩個節(jié)點執(zhí)行 updateHeight 函數(shù)。函數(shù)代碼參考如下:

更新二叉樹高度
# update the height of the node
def updateHeight(root):
    if root.lchild and root.rchild:
        root.height = max(root.lchild.height, root.rchild.height) + 1
    elif root.lchild:
        root.height = root.lchild.height + 1
    elif root.rchild:
        root.height = root.rchild.height + 1
    else:
        root.height = 0

樹的高度也就是左右子樹的高度最大值加一掌腰,若無子樹狰住,則設(shè)置樹高為零。

  • 右右情況(RR)

該情況與上面的左左情況具有對稱性齿梁,對平衡二叉樹執(zhí)行插入或刪除節(jié)點操作后催植,根節(jié)點的平衡因子變?yōu)?-2肮蛹,根節(jié)點的右子節(jié)點平衡因子為 -10,為了恢復(fù)二叉樹的平衡创南,需要進行左旋伦忠,來使得新的左右子樹高度區(qū)域平衡。

RR

如上圖 RR 所示稿辙,該情況下需要對平衡二叉樹執(zhí)行左旋操作:

  1. 設(shè)置根節(jié)點 root 的右子節(jié)點為新的根節(jié)點 root_{new}昆码;
  2. root_{new} 節(jié)點的左子樹作為 root 節(jié)點的右子樹,將 root 節(jié)點作為 root_{new} 的左子樹邻储,即降低“右子樹”高度赋咽,提升“左子樹”高度,使得新的左右子樹高度趨于平衡芥备;

左旋操作后冬耿,平衡二叉樹如圖 balanced_RR 所示。

balanced_RR
左旋代碼
# rotate from right to left with the right-child node as the axis
def rotateR2L(node):
    rightChild = node.rchild
    rightChild.lchild, node.rchild = node, rightChild.lchild  # rotate
    updateHeight(node)
    updateHeight(rightChild)
    return rightChild

左旋操作同右旋一樣萌壳,只更改了原根節(jié)點和新根節(jié)點的平衡因子亦镶,所以只需要對這兩個節(jié)點執(zhí)行 updateHeight 函數(shù)。

  • 左右情況

該情況下根節(jié)點的平衡因子與左左情況相同袱瓮,都為 2缤骨,不同之處在于左子節(jié)點的平衡因子為 -1,若按照之前直接進行右旋操作尺借,則旋轉(zhuǎn)操作后二叉樹扔處于不平衡狀態(tài)绊起。

對于圖 LR,節(jié)點 B 的平衡因子為 -1燎斩,設(shè) B 節(jié)點的左子樹 D 高度為 h虱歪,則右子樹 E 高度為h+1,因為 A 的平衡因子為 2栅表,所以二叉樹 C 的高度為: h笋鄙。則右旋操作后,B 的左子樹高度不變?yōu)?h怪瓶,右子樹高度為:1+max(height(C),height(E))=h+2萧落,因為 B 的平衡因子為 -2,所以按此方式的旋轉(zhuǎn)操作沒有效果洗贰。

LR

所以該情況下找岖,首先需要對根節(jié)點的左子節(jié)點進行調(diào)整,即將左子節(jié)點的平衡因子由 -1 調(diào)整為 1敛滋, 使得當(dāng)前情況轉(zhuǎn)換為 LL 情況许布,然后再對二叉樹執(zhí)行右旋操作。

step 1:對左子樹執(zhí)行左旋操作

step_1

step 2:對二叉樹執(zhí)行右旋操作

step_2
  • 右左情況

該情況與上面左右情況對稱绎晃,根節(jié)點的平衡因子為 -2爹脾,右子節(jié)點平衡因子為 1帖旨,需要首先對右子樹進行右旋操作,調(diào)整二叉樹為 RR 情況灵妨,再對二叉樹執(zhí)行左旋操作。

RL

step 1:對右子樹執(zhí)行右旋操作

step_1

step 2:對二叉樹執(zhí)行左旋操作

step_2

性能分析

因為平衡二叉樹也是二叉搜索樹落竹,回顧二叉搜索樹中的操作復(fù)雜度泌霍,查詢、插入和刪除復(fù)雜度均為 log_2N~N述召。平衡二叉樹中查詢復(fù)雜度影響因素自然為樹的高度朱转;插入節(jié)點操作可以拆分為兩個步驟:查詢節(jié)點位置,插入節(jié)點后平衡操作积暖;刪除節(jié)點操作同理可以拆分為兩個步驟:查詢節(jié)點位置藤为,刪除節(jié)點后平衡操作。

平衡調(diào)節(jié)過程中可能存在旋轉(zhuǎn)操作夺刑,遞歸執(zhí)行的次數(shù)則依賴于樹的高度(可以優(yōu)化為當(dāng)前節(jié)點平衡因子不發(fā)生變化缅疟,則取消向上遞歸)。所以平衡二叉樹中查詢遍愿、插入和刪除節(jié)點操作的復(fù)雜度依賴于樹高存淫。

平衡二叉樹因為左右子樹的平衡因子限制,所以不可能存在類似于斜樹的情況沼填,因為任一節(jié)點的左右子樹高度差最大為一桅咆,且二叉樹具有對稱性,所以不妨設(shè)每個子樹的左子樹高度大于右子樹高度坞笙。

AVL

根據(jù)平衡二叉樹定義可知岩饼,若二叉樹左子樹高度為 h (h \ge 2),則右子樹高度最少也要是 h-1薛夜,方能滿足平衡二叉樹的平衡特性籍茧。以 F(H) 表示高度為 H 的平衡二叉樹的最少節(jié)點個數(shù),若二叉樹不是空樹則有:

F(0) = 1
F(1) = 2
F(2) = 4
F(H) = F(H-1)+F(H-2)+1 (H \ge 3)

根據(jù)推導(dǎo)公式可知却邓,平衡二叉樹的高度最大為 O(log_{\frac{1+\sqrt5}2}N)硕糊。當(dāng)二叉樹向完全二叉樹靠攏,盡量填滿每層上的節(jié)點時腊徙,樹的高度最小简十,為 O(log_2N)。所以相對于二叉搜索樹撬腾,平衡二叉樹避免了向線性結(jié)構(gòu)演化的傾向螟蝙,查詢、插入和刪除節(jié)點的時間復(fù)雜度為 O(log_2N)~O(log_{\frac{1+\sqrt5}2}N)民傻。因為每個節(jié)點上需要保存平衡因子胰默,所以空間復(fù)雜度要略高于二叉搜索樹场斑。

代碼附錄

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

  • 樹節(jié)點定義
# tree node definition
class Node(object):
    def __init__(self, value, height=0, lchild=None, rchild=None):
        self.lchild = lchild
        self.rchild = rchild
        self.value = value
        self.height = height   

相對于二叉搜索樹的節(jié)點定義漏隐,增加了 height 屬性。

  • 樹定義
# 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)定義與二叉搜索樹中結(jié)構(gòu)相同保持不變奴迅。

  • 模塊中對樹結(jié)構(gòu)中的函數(shù)進行實現(xiàn)
# node in-order traversal(LDR)
def traversal(node):
    if not node:
        return
    traversal(node.lchild)
    print(node.value, 'height=', node.height, end=', ')
    traversal(node.rchild)

# insert node
'''
the recursive insert operation has a flaw,
that the binary tree will recursively updates the height of the parent node 
even if the inserted element already exists.
'''
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 checkBalance(root)

# check whether the tree is balanced
def checkBalance(root):
    if not root:
        return None
    if abs(heightDiffL2R(root)) < 2:  # the tree is balance
        updateHeight(root)
    elif heightDiffL2R(root) == 2:  # situation L
        if heightDiffL2R(root.lchild) == -1:  # situation LR
            root.lchild = rotateR2L(root.lchild)
            root = rotateL2R(root)
        else:  # situation LL
            root = rotateL2R(root)
    elif heightDiffL2R(root) == -2:  # situation R
        if heightDiffL2R(root.rchild) == 1:  # situation RL
            root.rchild = rotateL2R(root.rchild)
            root = rotateR2L(root)
        else:  # situation RR
            root = rotateR2L(root)
    return root

# get the height difference between left-child and right-child
def heightDiffL2R(node):
    if node.lchild and node.rchild:
        return node.lchild.height - node.rchild.height
    if node.lchild:
        return node.lchild.height + 1
    if node.rchild:
        return -(node.rchild.height + 1)
    return 0

# update the height of the node
def updateHeight(root):
    if root.lchild and root.rchild:
        root.height = max(root.lchild.height, root.rchild.height) + 1
    elif root.lchild:
        root.height = root.lchild.height + 1
    elif root.rchild:
        root.height = root.rchild.height + 1
    else:
        root.height = 0

# rotate from left to right with the left-child node as the axis
def rotateL2R(node):
    leftChild = node.lchild
    leftChild.rchild, node.lchild = node, leftChild.rchild
    updateHeight(node)
    updateHeight(leftChild)
    return leftChild

# rotate from right to left with the right-child node as the axis
def rotateR2L(node):
    rightChild = node.rchild
    rightChild.lchild, node.rchild = node, rightChild.lchild
    updateHeight(node)
    updateHeight(rightChild)
    return rightChild

def delete(root, value):
    if not root:
        return None
    if root.value > value:
        root.lchild = delete(root.lchild, value)
    elif root.value < value:
        root.rchild = delete(root.rchild, value)
    else:
        if root.lchild and root.rchild:  # degree of the node is 2
            target = transferDeleteNode(root)
            root = delete(root, target)
            root.value = target
        else:  # degree of the node is [0|1]
            root = root.lchild if root.lchild else root.rchild
    return checkBalance(root)

# find the maximum node or the minimum node in the tree
def transferDeleteNode(node):
    if node.rchild.height > node.lchild.height:
        target = node.rchild
        while target.lchild:
            target = target.lchild
    else:
        target = node.lchild
        while target.rchild:
            target = target.rchild
    return target.value
  • 測試代碼與輸出
if __name__ == '__main__':
    arr = [5, 3, 4, 0, 2, 1, 8, 6, 9, 7,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 height= 1, 1 height= 0, 2 height= 3, 3 height= 0, 4 height= 1, 5 height= 0, 6 height= 2, 7 height= 0, 8 height= 1, 9 height= 0, 
delete test------------------
after delete 7,BST in-order is = 0 height= 1, 1 height= 0, 2 height= 3, 3 height= 0, 4 height= 1, 5 height= 0, 6 height= 2, 8 height= 1, 9 height= 0, 
after delete 7,BST in-order is = 0 height= 1, 1 height= 0, 2 height= 3, 3 height= 0, 4 height= 1, 5 height= 0, 6 height= 2, 8 height= 1, 9 height= 0, 
after delete 9,BST in-order is = 0 height= 1, 1 height= 0, 2 height= 3, 3 height= 0, 4 height= 1, 5 height= 0, 6 height= 2, 8 height= 0, 
after delete 6,BST in-order is = 0 height= 1, 1 height= 0, 2 height= 3, 3 height= 0, 4 height= 1, 5 height= 2, 8 height= 0, 
after delete 8,BST in-order is = 0 height= 1, 1 height= 0, 2 height= 2, 3 height= 0, 4 height= 1, 5 height= 0, 
after delete 1,BST in-order is = 0 height= 0, 2 height= 2, 3 height= 0, 4 height= 1, 5 height= 0, 
after delete 2,BST in-order is = 0 height= 0, 3 height= 2, 4 height= 1, 5 height= 0, 
after delete 0,BST in-order is = 3 height= 0, 4 height= 1, 5 height= 0, 
after delete 4,BST in-order is = 3 height= 1, 5 height= 0, 
after delete 3,BST in-order is = 5 height= 0, 
after delete 5,BST in-order is = 

github 鏈接:平衡二叉樹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末青责,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子取具,更是在濱河造成了極大的恐慌脖隶,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暇检,死亡現(xiàn)場離奇詭異产阱,居然都是意外死亡,警方通過查閱死者的電腦和手機块仆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門构蹬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人榨乎,你說我怎么就攤上這事怎燥。” “怎么了蜜暑?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵铐姚,是天一觀的道長。 經(jīng)常有香客問我肛捍,道長隐绵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任拙毫,我火速辦了婚禮依许,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缀蹄。我一直安慰自己峭跳,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布缺前。 她就那樣靜靜地躺著蛀醉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪衅码。 梳的紋絲不亂的頭發(fā)上拯刁,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音逝段,去河邊找鬼垛玻。 笑死割捅,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的帚桩。 我是一名探鬼主播亿驾,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼朗儒!你這毒婦竟也來了颊乘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤醉锄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后浙值,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恳不,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年开呐,在試婚紗的時候發(fā)現(xiàn)自己被綠了烟勋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡筐付,死狀恐怖卵惦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瓦戚,我是刑警寧澤沮尿,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站较解,受9級特大地震影響畜疾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜印衔,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一啡捶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奸焙,春花似錦瞎暑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鲤桥,卻和暖如春揍拆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茶凳。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工嫂拴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留播揪,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓筒狠,卻偏偏與公主長得像猪狈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子辩恼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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