平衡二叉樹(shù)-樹(shù)的旋轉(zhuǎn)

樹(shù)結(jié)構(gòu)

  1. 樹(shù)

    樹(shù)結(jié)構(gòu)是由一個(gè)父節(jié)點(diǎn)以及若干個(gè)子節(jié)點(diǎn),然后子節(jié)點(diǎn)又是其他子節(jié)點(diǎn)的父節(jié)點(diǎn)埂陆,由此而形成的一種結(jié)構(gòu)即是樹(shù)刽沾。其中節(jié)點(diǎn)的子節(jié)點(diǎn)的子節(jié)點(diǎn)叫做該節(jié)點(diǎn)的孫節(jié)點(diǎn)。如下所示:

  1. 二叉樹(shù)(Binary Tree, BT)

    二叉樹(shù)是樹(shù)結(jié)構(gòu)的應(yīng)用形式之一峭跳,二叉樹(shù)每個(gè)節(jié)點(diǎn)至多有兩個(gè)子節(jié)點(diǎn)膘婶,如上面第三個(gè)樹(shù)結(jié)構(gòu)所示,位于左邊的子節(jié)點(diǎn)叫做左孩子或者左子節(jié)點(diǎn)蛀醉,位于右邊的叫做右孩子或者右子節(jié)點(diǎn)悬襟。

  2. 二叉搜索(查找)樹(shù)(Binary Search Tree, BST)

    二叉搜索樹(shù)是二叉樹(shù)的應(yīng)用之一,在一棵二叉搜索樹(shù)中拯刁,左子樹(shù)上所有節(jié)點(diǎn)的值都小于或者等于其根節(jié)點(diǎn)的值脊岳,而右子樹(shù)上所有節(jié)點(diǎn)的值總是大于或者等于根節(jié)點(diǎn)的值,由此便構(gòu)成了一棵有序的樹(shù)結(jié)構(gòu)。如下圖所示:

  1. 平衡二叉樹(shù)(AVL)

    二叉搜索樹(shù)是一棵有序的樹(shù)割捅,但是大多數(shù)情況下奶躯,往二叉搜索樹(shù)中插入節(jié)點(diǎn)時(shí),可能存在的情況是插入的節(jié)點(diǎn)始終位于一個(gè)分支上亿驾,如下圖所示:

這樣就出現(xiàn)了一種不盡如人意的情況嘹黔,就是在一棵二叉搜索樹(shù)中,某一個(gè)分支節(jié)點(diǎn)很少莫瞬,而另一個(gè)分支上節(jié)點(diǎn)卻很多儡蔓,導(dǎo)致在查找、插入疼邀、刪除等操作上效率很低喂江。

在一棵二叉搜索樹(shù)中,對(duì)于某一個(gè)節(jié)點(diǎn)旁振,如果該節(jié)點(diǎn)左子樹(shù)和右子樹(shù)的高度差的絕對(duì)值超過(guò)了abs(h) > 1获询,則稱該樹(shù)為非平衡二叉搜索樹(shù)。為了改善這種情況拐袜,便出現(xiàn)了平衡二叉樹(shù)吉嚣,顧名思義,平衡二叉樹(shù)任意一個(gè)節(jié)點(diǎn)的左子樹(shù)和右子樹(shù)高度差的絕對(duì)值都abs(h)<=1阻肿。平衡二叉樹(shù)是在二叉搜索樹(shù)的基礎(chǔ)上瓦戚,增加了平衡二叉樹(shù)的操作,使得二叉搜索樹(shù)是一棵平衡樹(shù)丛塌。如下圖為一棵平衡二叉樹(shù):

平衡查找樹(shù)的平衡

前面已經(jīng)提到什么是平衡二叉樹(shù)较解,那么怎么樣形成一棵平衡的二叉樹(shù)呢?

權(quán)威們給出的答案是旋轉(zhuǎn)赴邻,即通過(guò)對(duì)二叉樹(shù)進(jìn)行旋轉(zhuǎn)來(lái)改變樹(shù)的結(jié)構(gòu)并且不改變節(jié)點(diǎn)值的順序印衔,從而得到一棵平衡的二叉樹(shù)。下面介紹樹(shù)的旋轉(zhuǎn)姥敛,樹(shù)的旋轉(zhuǎn)分為左旋轉(zhuǎn)奸焙、右旋轉(zhuǎn)以及左右旋轉(zhuǎn),右左旋轉(zhuǎn)彤敛。

因?yàn)闃?shù)的節(jié)點(diǎn)個(gè)數(shù)變化為1与帆、2、3...n墨榄,所以當(dāng)節(jié)點(diǎn)總數(shù)小于3時(shí)一棵二叉搜索樹(shù)一定是平衡的玄糟,如下圖:

此時(shí)左子樹(shù)的高度與右子樹(shù)的高度差的絕對(duì)值為1,所以是平衡的(在下文中提到的高度差都是左子樹(shù)高度減去右子樹(shù)高度)袄秩。但是隨著節(jié)點(diǎn)的插入阵翎,有可能變成不平衡的了逢并。以下為需要旋轉(zhuǎn)操作進(jìn)行平衡二叉樹(shù)的情況,均是針對(duì)最少節(jié)點(diǎn)數(shù)的情況郭卫,即需要旋轉(zhuǎn)操作的最小子樹(shù)砍聊。

注: 下文提到的高度差均為左子樹(shù)減去右子樹(shù)的高度差的絕對(duì)值,同時(shí)本文中的平衡二叉樹(shù)為左節(jié)點(diǎn)小于父節(jié)點(diǎn)贰军,父節(jié)點(diǎn)小于右節(jié)點(diǎn)

  1. 左旋轉(zhuǎn)
  • 在右子樹(shù)添加節(jié)點(diǎn)造成不平衡玻蝌。root只有右孩子的情況,以root的右孩子為中心谓形,向左(逆時(shí)針)旋轉(zhuǎn)root節(jié)點(diǎn)灶伊,旋轉(zhuǎn)結(jié)果為root節(jié)點(diǎn)變?yōu)閞oot右孩子的左孩子,如下圖, 在右子樹(shù)添加節(jié)點(diǎn)(圖中的16)寒跳,造成不平衡:
  • 在右子樹(shù)添加節(jié)點(diǎn)造成不平衡,其中root同時(shí)有左右子樹(shù)竹椒,左子樹(shù)只有一個(gè)節(jié)點(diǎn)童太,右孩子只有一個(gè)右子節(jié)點(diǎn),添加一個(gè)節(jié)點(diǎn)(下圖中的17)后造成不平衡樹(shù)胸完,此時(shí)可以看到书释,root的右子樹(shù)不平衡,此時(shí)按照第一種旋轉(zhuǎn)方式可以將右子樹(shù)旋轉(zhuǎn)平衡赊窥,進(jìn)而使整棵樹(shù)平衡爆惧,如下圖:
  • 在右子樹(shù)添加節(jié)點(diǎn)造成不平衡,其中root只有一個(gè)左孩子锨能,root的右孩子同時(shí)存在左右孩子扯再,如下圖:

從上面可以看出,如果root右子樹(shù)比左子樹(shù)高2址遇,并且右子樹(shù)的右子樹(shù)比右子樹(shù)的左子樹(shù)高熄阻,則執(zhí)行左旋轉(zhuǎn),以下是左旋轉(zhuǎn)的代碼(Golang):

    type Element interface {
        Value() interface{}
        Compare(Element) int //相等返回0倔约,小于返回負(fù)數(shù)秃殉,大于返回正數(shù)
    }
   
   type AVLNode struct {
        Data   Element  //存放的元素
        Height int      //存放節(jié)點(diǎn)的高度
        Left   *AVLNode //左子樹(shù)
        Right  *AVLNode //右子樹(shù)
   }
    
   func (avl *AVLNode) Max() *AVLNode {
    node := avl
    for node.Right != nil {
        node = node.Right
    }
    return node
   }
   
   func getHeight(avl *AVLNode) int {
    if avl != nil {
        return avl.Height
    }
    return 0
   }
   
   func max(h1, h2 int) int {
    if h1 > h2 {
        return h1
    }
    return h2
   }
   
   func leftRotate(avl *AVLNode) *AVLNode {
       if avl == nil {
           return nil
       }
        node := avl.Right
        avl.Right = node.Left
        node.Left = avl
    
        avl.Height = max(getHeight(avl.Left), getHeight(avl.Right)) + 1
        node.Height = max(getHeight(node.Left), getHeight(node.Right)) + 1
        return node
   }
  1. 右旋轉(zhuǎn)
  • 在左子樹(shù)添加節(jié)點(diǎn)造成不平衡, root沒(méi)有右孩子,同時(shí)左孩子只有左孩子一個(gè)節(jié)點(diǎn), 此時(shí)以root的左孩子為中心浸剩,進(jìn)行右旋轉(zhuǎn)(順時(shí)針旋轉(zhuǎn)), 將root左孩子提升為root钾军,root降為左孩子的右孩子,如下圖:
  • 在左子樹(shù)添加節(jié)點(diǎn)造成不平衡, root同時(shí)包含左右孩子绢要,右孩子沒(méi)有子節(jié)點(diǎn)吏恭,左孩子只有一個(gè)左孩子節(jié)點(diǎn),此時(shí)root的左子樹(shù)為不平衡樹(shù)袖扛,按照上面的方式對(duì)左子樹(shù)進(jìn)行右旋轉(zhuǎn)得到平衡樹(shù)砸泛,如下圖:
  • 在左子樹(shù)添加節(jié)點(diǎn)造成不平衡, root只有一個(gè)右孩子, 左孩子同時(shí)有左右孩子, 在左孩子的左孩子下添加一個(gè)節(jié)點(diǎn), 如下圖:

從上面可以看出, 如果root左子樹(shù)比右子樹(shù)高2十籍,并且左子樹(shù)的左子樹(shù)比左子樹(shù)的右子樹(shù)高,則執(zhí)行右旋轉(zhuǎn)唇礁,以下是右旋轉(zhuǎn)的代碼(Golang):

    func rightRotate(avl *AVLNode) *AVLNode {
       if avl == nil {
           return nil
       }        
       node := avl.Left      //左子樹(shù)
       avl.Left = node.Right //左子樹(shù)的右子樹(shù)變?yōu)樽笞訕?shù)
       node.Right = avl      //avl降為左子樹(shù)的右子樹(shù)
    
        //更新節(jié)點(diǎn)高度
        avl.Height = max(getHeight(avl.Left), getHeight(avl.Right)) + 1
        node.Height = max(getHeight(node.Left), getHeight(node.Right)) + 1
        return node
    }
  1. 左右旋轉(zhuǎn)

左右旋轉(zhuǎn)是指先執(zhí)行左旋轉(zhuǎn)再執(zhí)行右旋轉(zhuǎn)勾栗。

  • 在左子樹(shù)添加節(jié)點(diǎn)造成不平衡,root只有左子樹(shù)盏筐,且左子樹(shù)只有一個(gè)節(jié)點(diǎn)围俘,如下圖:
  • 在左子樹(shù)添加節(jié)點(diǎn)造成不平衡,root同時(shí)有左右孩子琢融,root的左孩子同時(shí)有左右孩子界牡,在root的左孩子的右孩子上添加左節(jié)點(diǎn)造成不平衡,如下圖:
  • 在左子樹(shù)添加節(jié)點(diǎn)造成不平衡漾抬,root同時(shí)有左右孩子宿亡,root的左孩子同時(shí)有左右孩子,在root的左孩子的左孩子上添加右節(jié)點(diǎn)造成不平衡纳令,如下圖:

從上面可以看出, 如果root左子樹(shù)比右子樹(shù)高2挽荠,并且左子樹(shù)的右子樹(shù)比左子樹(shù)的左子樹(shù)高2,則先執(zhí)行左旋轉(zhuǎn)平绩,再執(zhí)行右旋轉(zhuǎn)圈匆,以下是左右旋轉(zhuǎn)的代碼(Golang):

    //先將左孩子左旋轉(zhuǎn),自己再右旋轉(zhuǎn)
    func (avl *AVLNode) leftRightRotate() *AVLNode {
        avl.Left = avl.Left.leftRotate()
        return avl.rightRotate()
    }
  1. 右左旋轉(zhuǎn)

右左旋轉(zhuǎn)是指先執(zhí)行右旋轉(zhuǎn)再執(zhí)行左旋轉(zhuǎn)捏雌。

  • 在右子樹(shù)添加節(jié)點(diǎn)造成不平衡, root只有右子樹(shù)跃赚,且右子樹(shù)只有一個(gè)節(jié)點(diǎn),如下圖:
  • 在右子樹(shù)添加節(jié)點(diǎn)造成不平衡, root同時(shí)有左右孩子性湿,root的右孩子同時(shí)有左右孩子纬傲,在root的右孩子的左孩子上添加右節(jié)點(diǎn),如下圖:
  • 在右子樹(shù)添加節(jié)點(diǎn)造成不平衡, root同時(shí)有左右孩子窘奏,root的右孩子同時(shí)有左右孩子嘹锁,在root的右孩子的左孩子上添加左節(jié)點(diǎn),如下圖:

從上面可以看出, 如果root右子樹(shù)比左子樹(shù)高2, 并且右子樹(shù)的右子樹(shù)比右子樹(shù)的左子樹(shù)矮, 則執(zhí)行右旋轉(zhuǎn)后再執(zhí)行左旋轉(zhuǎn)着裹,以下是左右旋轉(zhuǎn)的代碼(Golang):

    //先將右孩子右旋轉(zhuǎn)领猾,然后自己右旋轉(zhuǎn)
    func (avl *AVLNode) rightLeftRotate() *AVLNode {
        avl.Right = avl.Right.rightRotate()
        return avl.leftRotate()
    }

綜上可以得出平衡二叉查找樹(shù)的函數(shù)為:

//調(diào)整樹(shù)為二叉平衡樹(shù)
func balance(avl *AVLNode) *AVLNode {
    if avl == nil {
        return nil
    }
    if getHeight(avl.Right)-getHeight(avl.Left) == 2 {
        if getHeight(avl.Right.Right) > getHeight(avl.Right.Left) {
            avl = avl.leftRotate()
        } else {
            avl = avl.rightLeftRotate()
        }
    } else if getHeight(avl.Left)-getHeight(avl.Right) == 2 {
        if getHeight(avl.Left.Left) > getHeight(avl.Left.Right) {
            avl = avl.rightRotate()
        } else {
            avl = avl.leftRightRotate()
        }
    }
    return avl
}

插入、刪除節(jié)點(diǎn)

以上為樹(shù)的旋轉(zhuǎn)操作骇扇,用于平衡二叉查找摔竿,對(duì)于二叉查找樹(shù),每添加或者插入一個(gè)節(jié)點(diǎn)后均需要執(zhí)行一次平衡操作少孝。代碼如下:

//添加節(jié)點(diǎn)
func (avl *AVLNode) AddNode(data Element) (root *AVLNode, ok bool) {
    defer func() {
        if ok {
            root = balance(avl)
            root.Height = max(getHeight(root.Left), getHeight(root.Right)) + 1
        }
    }()
    
    var target = &AVLNode{
        Data:   data,
        Height: 1,
        Left:   nil,
        Right:  nil,
    }
    
    cmpResult := avl.Data.Compare(data)
    switch {
    case cmpResult > 0:
        if avl.Left == nil {
            avl.Left = target
            ok = true
            return
        }
        avl.Left, ok = avl.Left.AddNode(data)
        return
    case cmpResult < 0:
        if avl.Right == nil {
            avl.Right = target
            ok = true
            return
        }
        avl.Right, ok = avl.Right.AddNode(data)
        return
    default:
        ok = true
        return
    }
}

func (avl *AVLNode) RemoveNode(data Element) (root *AVLNode, ok bool) {
    defer func() {
        if ok && root != nil {
            root = balance(root)
            root.Height = max(getHeight(root.Left), getHeight(root.Right)) + 1
        }
    }()
    
    var temp *AVLNode
    cmpResult := avl.Data.Compare(data)
    switch {
    case cmpResult > 0:
        if avl.Left == nil {
            return nil, false
        }
        temp, ok = avl.Left.RemoveNode(data)
        if ok {
            avl.Left = temp
        }
        root = avl
        return
    case cmpResult < 0:
        if avl.Right == nil {
            return nil, false
        }
        temp, ok = avl.Right.RemoveNode(data)
        if ok {
            avl.Right = temp
        }
        root = avl
        return
    default:
        if avl.Left == nil && avl.Right == nil {
            return nil, true
        }
        if avl.Left == nil {
            root, ok = avl.Right, true
            return
        }
        if avl.Right == nil {
            root, ok = avl.Left, true
            return
        }
        //將左子樹(shù)最大節(jié)點(diǎn)提升到頭節(jié)點(diǎn)继低,并將該最大節(jié)點(diǎn)從左子樹(shù)中刪除
        avl.Data = avl.Left.Max().Data
        avl.Left, ok = avl.Left.RemoveNode(avl.Data)
        root = avl
        return
    }
}

最后

原文鏈接

源碼跳轉(zhuǎn)

謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末稍走,一起剝皮案震驚了整個(gè)濱河市袁翁,隨后出現(xiàn)的幾起案子柴底,更是在濱河造成了極大的恐慌,老刑警劉巖粱胜,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柄驻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡焙压,警方通過(guò)查閱死者的電腦和手機(jī)鸿脓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)涯曲,“玉大人野哭,你說(shuō)我怎么就攤上這事』眉” “怎么了拨黔?”我有些...
    開(kāi)封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)傲武。 經(jīng)常有香客問(wèn)我蓉驹,道長(zhǎng),這世上最難降的妖魔是什么揪利? 我笑而不...
    開(kāi)封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮狠持,結(jié)果婚禮上疟位,老公的妹妹穿的比我還像新娘。我一直安慰自己喘垂,他們只是感情好甜刻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著正勒,像睡著了一般得院。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上章贞,一...
    開(kāi)封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天祥绞,我揣著相機(jī)與錄音,去河邊找鬼鸭限。 笑死蜕径,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的败京。 我是一名探鬼主播兜喻,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼赡麦!你這毒婦竟也來(lái)了朴皆?” 一聲冷哼從身側(cè)響起帕识,我...
    開(kāi)封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎遂铡,沒(méi)想到半個(gè)月后肮疗,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忧便,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年族吻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片珠增。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡超歌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒂教,到底是詐尸還是另有隱情巍举,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布凝垛,位于F島的核電站懊悯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏梦皮。R本人自食惡果不足惜炭分,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望剑肯。 院中可真熱鬧捧毛,春花似錦、人聲如沸让网。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)溃睹。三九已至而账,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間因篇,已是汗流浹背泞辐。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惜犀,地道東北人铛碑。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像虽界,于是被迫代替她去往敵國(guó)和親汽烦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356