Swift 數(shù)據(jù)結(jié)構(gòu) - AVL樹(AVL Tree)

AVL樹

平衡二叉搜索樹(Self-balancing binary search tree)又被稱為AVL樹(有別于AVL算法)建蹄,且具有以下性質(zhì):它是一 棵空樹或它的左右兩個(gè)子樹的高度差的絕對值不超過1碌更,并且左右兩個(gè)子樹都是一棵平衡二叉樹裕偿。

image.png

平衡因子

某結(jié)點(diǎn)的左子樹與右子樹的 高度(深度)差 即為該結(jié)點(diǎn)的 平衡因子(BF,Balance Factor)。平衡二叉樹上所有結(jié)點(diǎn)的平衡因子只可能是 -1针贬,0 或 1击费。如果某一結(jié)點(diǎn)的平衡因子絕對值大于1則說明此樹不是平衡二叉樹。為了方便計(jì)算每一結(jié)點(diǎn)的平衡因子我們可以為每個(gè)節(jié)點(diǎn)賦予 height 這一屬性桦他,表示此節(jié)點(diǎn)的高度。

節(jié)點(diǎn)的 height 是獲取該節(jié)點(diǎn)最低葉子所需的步數(shù)谆棱。 例如快压,在下面的樹中,從A到E需要三個(gè)步垃瞧,因此A的高度為3蔫劣。B的高度為2,C的高度為1个从,其他的高度為0脉幢,因?yàn)樗鼈兪侨~節(jié)點(diǎn)。

image.png

高度和深度是相反的表示嗦锐,深度是 從上到下 數(shù)的嫌松,而高度是 從下往上 數(shù)。
我們先來看看高度和深度的定義奕污,某節(jié)點(diǎn)的深度是指從根節(jié)點(diǎn)到該節(jié)點(diǎn)的最長簡單路徑邊的條數(shù)萎羔,而高度是指從該節(jié)點(diǎn)到葉子節(jié)點(diǎn)的最長簡單路徑邊的條數(shù)。

基礎(chǔ)設(shè)計(jì)

首先我們可以設(shè)計(jì)出 AVL 樹節(jié)點(diǎn)碳默,并且實(shí)現(xiàn)一些簡單通用的方法供后續(xù)操作贾陷。

/**
 * AVLTree是BST,所以節(jié)點(diǎn)值必須是可比較的
 */
public class AvlTree<E: Comparable>{
    private class Node {
        public var e: E
        public var left: Node? = nil
        public var right: Node? = nil
        public var height: Int = 1

        public init(e: E){
            self.e = e
        }
    }
    private var root: Node?
    private var size: Int

    public init() {
        root = nil
        size = 0
    }
    
    //獲取某一結(jié)點(diǎn)的高度
    private func getHeight(node: Node?) -> Int {
        guard let node = node else {
            return 0
        }
        return node.height
    }
    
    public func getSize() -> Int {
        return size
    }
    
    public func isEmpty() -> Bool {
        return size == 0
    }
    
    /**
     * 獲取節(jié)點(diǎn)的平衡因子
     * @param node
     * @return
     */
    private func getBalanceFactor(node: Node?) -> Int {
        guard let node = node else {
            return 0
        }
        return abs(getHeight(node: node.left) - getHeight(node: node.right))
    }
    
    //判斷樹是否為平衡二叉樹
    public func isBalanced() -> Bool {
        return isBalanced(node: root)
    }
    
    private func isBalanced(node: Node?) -> Bool {
        guard let node = node else {
            return true
        }
        
        let balanceFactory = getBalanceFactor(node: node)
        if balanceFactory > 1 {
            return false
        }
        return isBalanced(node: node.left) && isBalanced(node: node.right)
    }
}

添加節(jié)點(diǎn)

往平衡二叉樹中添加節(jié)點(diǎn)很可能會(huì)導(dǎo)致二叉樹失去平衡嘱根,所以我們需要在每次插入節(jié)點(diǎn)后進(jìn)行平衡的維護(hù)操作髓废。插入節(jié)點(diǎn)破壞平衡性有如下四種情況:

LL(需要右旋平衡)

LL的意思是向左子樹(L)的左孩子(L)中插入新節(jié)點(diǎn)后導(dǎo)致不平衡,這種情況下需要 右旋操作该抒,而不是說LL的意思是右旋慌洪,后面的也是一樣。

image.png

我們將這種情況抽象出來柔逼,得到下圖:


image.png

我們需要對節(jié)點(diǎn)y進(jìn)行平衡的維護(hù)蒋譬。步驟如下圖所示:

image.png

    /**
     * 右旋轉(zhuǎn),返回根節(jié)點(diǎn)
     */
    private func rightRotate(y: Node?) -> Node? {
        guard let y = y else {
            return nil
        }
        let x = y.left
        let t3 = x?.right
        x?.right = y
        y.left = t3
        //更新height
        y.height = max(getHeight(node: y.left),getHeight(node: y.right))+1
        x?.height = max(getHeight(node: x?.left),getHeight(node: x?.right))+1
        return x
    }

RR(需要左旋平衡)

image.png

我們將這種情況抽象出來,得到下圖:


image.png

我們需要對節(jié)點(diǎn)y進(jìn)行平衡的維護(hù)。步驟如下圖所示:

image.png

    /**
     * 左旋轉(zhuǎn)
     */
    private func leftRotate(y: Node?) -> Node? {
        guard let y = y else {
            return nil
        }
        let x = y.right
        let t3 = x?.left
        x?.left = y
        y.right = t3
        //更新height
        y.height = max(getHeight(node: y.left),getHeight(node: y.right)) + 1
        x?.height = max(getHeight(node: x?.left),getHeight(node: x?.right)) + 1
        return x
    }

LR(先左旋后右旋)

image.png

我們將這種情況抽象出來舶衬,得到下圖:


image.png

我們需要對節(jié)點(diǎn)y進(jìn)行平衡的維護(hù)猫胁。步驟如下圖所示:

image.png

RL(先右旋后左旋)

image.png

我們將這種情況抽象出來,得到下圖:
image.png

我們需要對節(jié)點(diǎn)y進(jìn)行平衡的維護(hù)剂买。步驟如下圖所示:
image.png

添加節(jié)點(diǎn)代碼

extension AvlTree {
    // 向二分搜索樹中添加新的元素(key, value)
    public func add(e: E) {
        root = add(node: root, e: e)
    }
    
    // 向以node為根的二分搜索樹中插入元素(key, value)惠爽,遞歸算法
    // 返回插入新節(jié)點(diǎn)后二分搜索樹的根
    private func add(node: Node?, e: E) -> Node? {
        guard let node = node else {
             size += 1
            return Node(e: e)
        }

        //判斷是插入根節(jié)點(diǎn)的左子樹還是右子樹
        if e < node.e {
            node.left = add(node: node.left, e: e)
        } else if e > node.e {
            node.right = add(node: node.right, e: e)
        }
        
        //更新height
        node.height = 1 + max(getHeight(node: node.left),getHeight(node: node.right))
        
        //計(jì)算平衡因子
        let balanceFactor = getBalanceFactor(node: node)
        if balanceFactor > 1 && getBalanceFactor(node: node.left) > 0 {
            //右旋LL
            return rightRotate(y: node)
        }
        if balanceFactor < -1 && getBalanceFactor(node: node.right) < 0  {
            //左旋RR
            return leftRotate(y: node)
        }
        
        //LR
        if balanceFactor > 1 && getBalanceFactor(node: node.left) < 0 {
            node.left = leftRotate(y: node.left)
            return rightRotate(y: node)
        }
        
        //RL
        if balanceFactor < -1 && getBalanceFactor(node: node.right) > 0 {
            node.right = rightRotate(y: node.right)
            return leftRotate(y: node)
        }
        return node
    }
}

刪除節(jié)點(diǎn)

在刪除AVL樹節(jié)點(diǎn)前需要知道二分搜索樹的節(jié)點(diǎn)刪除操作【點(diǎn)此學(xué)習(xí)吧!】瞬哼,和二分搜索樹刪除節(jié)點(diǎn)不同的是我們刪除AVL樹的節(jié)點(diǎn)后需要進(jìn)行平衡的維護(hù)操作婚肆。

extension AvlTree {
    //查找二叉搜索樹的節(jié)點(diǎn)
    private func getNode(root: Node?, e: E) -> Node? {
        guard let root = root else {
            return nil
        }
        
        if(root.e == e) {
            return root
        } else if e > root.e {
            return getNode(root: root.right, e: e)
        } else {
            return getNode(root: root.left, e: e)
        }
        return nil
    }
    //移出節(jié)點(diǎn)
    public func remove(e: E) -> E? {
        guard let node = getNode(root: root, e: e) else {
            return nil
        }
        root = remove(node: root, e: e)
        return node.e
    }
    
    //二叉搜索樹的最小值一直找左孩子就行了
    private func minNode(node: Node?) -> Node? {
        guard let node = node else {
            return nil
        }
        
        if let node = node.left {
            return minNode(node: node.left)
        } else {
            return node
        }
    }

    private func remove(node: Node?, e: E) -> Node? {
        guard let node = node else {
            return nil
        }
        var retNode: Node?
        if e < node.e {
            node.left = remove(node: node.left , e: e)
            retNode = node
        } else if e > node.e {
            node.right = remove(node: node.right, e: e)
            retNode = node
        } else {
            // 待刪除節(jié)點(diǎn)左子樹為空的情況
            if node.left == nil {
                let rightNode = node.right
                node.right = nil
                size -= 1
                retNode = rightNode
            } else if node.right == nil {
                // 待刪除節(jié)點(diǎn)右子樹為空的情況
                let leftNode = node.left
                node.left = nil
                size -= 1
                retNode = leftNode
            } else {
                // 待刪除節(jié)點(diǎn)左右子樹均不為空的情況
                // 找到比待刪除節(jié)點(diǎn)大的最小節(jié)點(diǎn), 即待刪除節(jié)點(diǎn)右子樹的最小節(jié)點(diǎn)
                // 用這個(gè)節(jié)點(diǎn)頂替待刪除節(jié)點(diǎn)的位置
                let successor = minNode(node: node.right)!
                successor.right = remove(node: node.right, e: successor.e)
                successor.left = node.left

                node.left = nil
                node.right = nil
                retNode = successor
            }
        }
        
        guard let retNodeTmp = retNode else {
            return nil
        }
        //維護(hù)平衡
        //更新height
        retNodeTmp.height = 1 + max(getHeight(node: retNodeTmp.left),getHeight(node: retNodeTmp.right))
        
        //計(jì)算平衡因子
        let balanceFactor = getBalanceFactor(node: retNodeTmp)
        if balanceFactor > 1 && getBalanceFactor(node: retNodeTmp.left) >= 0 {
            //右旋LL
            return rightRotate(y: retNodeTmp)
        }
        
        if balanceFactor < -1 && getBalanceFactor(node: retNodeTmp.right) <= 0 {
            //左旋RR
            return leftRotate(y: retNodeTmp)
        }
        
        //LR
        if balanceFactor > 1 && getBalanceFactor(node: retNodeTmp.left) < 0 {
            node.left = leftRotate(y: retNodeTmp.left)
            return rightRotate(y: retNodeTmp)
        }
        
        //RL
        if balanceFactor < -1 && getBalanceFactor(node: retNodeTmp.right) > 0 {
            node.right = rightRotate(y: retNodeTmp.right)
            return leftRotate(y: retNodeTmp)
        }
        return retNodeTmp
    }
}

參考文章

詳細(xì)圖文——AVL樹

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市坐慰,隨后出現(xiàn)的幾起案子较性,更是在濱河造成了極大的恐慌,老刑警劉巖结胀,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赞咙,死亡現(xiàn)場離奇詭異,居然都是意外死亡糟港,警方通過查閱死者的電腦和手機(jī)攀操,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秸抚,“玉大人速和,你說我怎么就攤上這事“溃” “怎么了颠放?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秀姐。 經(jīng)常有香客問我慈迈,道長,這世上最難降的妖魔是什么省有? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任痒留,我火速辦了婚禮,結(jié)果婚禮上蠢沿,老公的妹妹穿的比我還像新娘伸头。我一直安慰自己,他們只是感情好舷蟀,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布恤磷。 她就那樣靜靜地躺著,像睡著了一般野宜。 火紅的嫁衣襯著肌膚如雪扫步。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天匈子,我揣著相機(jī)與錄音河胎,去河邊找鬼。 笑死虎敦,一個(gè)胖子當(dāng)著我的面吹牛游岳,可吹牛的內(nèi)容都是我干的政敢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼胚迫,長吁一口氣:“原來是場噩夢啊……” “哼喷户!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起访锻,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤褪尝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后期犬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恼五,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年哭懈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茎用。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡遣总,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轨功,到底是詐尸還是另有隱情旭斥,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布古涧,位于F島的核電站垂券,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏羡滑。R本人自食惡果不足惜菇爪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柒昏。 院中可真熱鬧凳宙,春花似錦、人聲如沸职祷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽有梆。三九已至是尖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泥耀,已是汗流浹背饺汹。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爆袍,地道東北人首繁。 一個(gè)月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓作郭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親弦疮。 傳聞我的和親對象是個(gè)殘疾皇子夹攒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348