golang 手擼 平衡二叉樹

golang 手擼 平衡二叉樹

樹是一種計算機數據結構中非常常用的一種結構,其中就包含了:平衡二叉樹,這種樹是一種特殊的二叉查找樹(二叉查找樹也就是,右孩子大于其父結點,左孩子小于其父結點的樹),但是簡單的二叉查找樹存在的問題就是不平衡,最差的查找效率為O(n),故就有人發明了一種平衡的額二叉查找樹。

特點

  1. 平衡二叉樹是一種二叉查找樹
  2. 每個結點的左子樹的高度減去右子樹的高度的絕對值不超過1
  3. 空樹和左右子樹都是平衡二叉樹
  4. 相比紅黑樹,平衡二叉樹比較適用于沒有刪除的情況

平衡因子

平衡二叉樹是在二叉查查找樹的基礎上進行構建了,為了維持平衡二叉樹的平衡,那么就需要一種機制來判斷平衡二叉樹是否是平衡的。這種機制就叫做平衡因子。

平衡二叉樹的每個結點都會維持一個值,這個值就是平衡因子,這個平衡因子就是這個結點的左子樹的高度減去右子樹的高度得到的值。如果這個平衡因子的值的絕對值大于1了,說明這個樹就不平衡了,那么就需要調整樹的結構了。

我們可以從如上這個這個圖中看的到:每個結點都維持了一個值,比如左邊的AVL 樹根結點的值為-1,這個-1是怎么來的呢,就是結點3的左子樹的高度為 2, 右子樹的高度為 3, 左子樹的高度減去右子樹的高度就得到這個結點的平衡因子。

如果某個結點的平衡因子的絕對值大于了 1 ,那么就說明這個平衡二叉樹不平衡了,就需要調整平衡二叉樹的結構。

旋轉

由于AVL樹需要做到平衡,所以每次插入葉子節點,如果發現不平衡,都需要進行旋轉以保持平衡。

找到了要給別人做的一個旋轉圖例,可以參考的看下:

我們來總結下以上圖片中出現的這幾種情況:

  1. 三個結點的單旋轉


  • 我們可以看到上途中結點3的平衡因子為2了,這樣就不平衡橫了,那么就需要進行一個對結點3的順時針旋轉,旋轉完的結果就是上圖中的右圖。
  1. 三個結點的雙旋轉


  • 針對這種情況其實就是上面一種情況的特殊版本:我們要先把這種情況先轉化為:三個結點的單旋轉這種情況。
  • 首先對結點11進行一個逆時針的旋轉,然后就變為了:三個結點的單旋轉
  • 然后再像;三個結點的單旋轉 一樣的對結點3進行右旋轉。

什么時候怎樣旋轉呢?

  1. 我們插入一個結點后,某個結點p的平衡因子由原來的1變成了2。

    • 那就可能是這兩種情況:
    1. 新插入的結點插入到了結點p的左子樹的左孩子下

      • golang 代碼實現

        // 順時針旋轉,右旋
        func (avlNode *AVLNode) RightRotate() *AVLNode {
            headNode := avlNode.Left
            avlNode.Left = headNode.Right
            headNode.Right = avlNode
        
            // 更新旋轉后結點的高度
            avlNode.height = Max(avlNode.Left.GetHeight(), avlNode.Right.GetHeight())+1
            headNode.height = Max(headNode.Left.GetHeight(), headNode.Right.GetHeight())+1
        
            return headNode
        }
        
    2. 新插入的結點插入到了結點p的左子樹的右孩子下

      • 從以上例子中我們就可以發現,順時針旋轉和逆時針旋轉的規律所在

      • golang 代碼實現

        // 先逆時針旋轉再順時針旋轉,先左旋,在右旋
        func (avlNode *AVLNode) LeftThenRightRotate() *AVLNode {
            // 先把左孩子結點進行左旋
            avlNode.Left = avlNode.Left.LeftRotate()
            // 然后把自己右旋
            return avlNode.RightRotate()
        }
        
    • 從以上的三張圖中我們就可以發現這樣的一個容易忽略的問題
      1. 假設有一個結點,這個結點是P,我們在逆時針旋轉一個結點的時候需要把結點P的右孩子的左子樹移動為結點P的右子樹
      2. 假設有一個結點,這個結點是P,我們在順時針旋轉時,需要把P結點的左孩子的右子樹移動為P結點的左子樹
  2. 我們插入一個結點后,某個結點p的平衡因子由原來的-1變成了-2。

    • 那就可能是這兩種情況:
    1. 新插入的結點插入到了結點p的右子樹的右孩子下

      • 那就應該進行對結點p的逆時針旋轉
      • 旋轉過程可以參考上圖
      • golang 實現
        // 逆時針旋轉,左旋
        func (avlNode *AVLNode) LeftRotate() *AVLNode {
            headNode := avlNode.Right
            avlNode.Right = headNode.Left
            headNode.Left = avlNode
        
            // 更新結點的高度
            // 這里應該注意的俄式應該先更新avlNode 的高度,因為headNode結點在avlNode結點的上面
            // headNode計算高度的時候要根據avlNode的高度來計算
            avlNode.height = Max(avlNode.Left.GetHeight(), avlNode.Right.GetHeight())+1
            headNode.height = Max(headNode.Left.GetHeight(), headNode.Right.GetHeight())+1
        
            return headNode
        }
        
    2. 新插入的結點插入到了結點p的右子樹的左孩子下

      • 那就因該先對結點p的右孩子進行順時針旋轉
      • 然后在對結點p進行逆時針旋轉
      • 旋轉過程可以參考上圖
      • golang 實現
        // 先順時針旋轉再逆時針旋轉,先右旋,再左旋
        func (avlNode *AVLNode) RightThenLeftRotate() *AVLNode {
            // 先把右孩子進行右旋
            avlNode.Right = avlNode.Right.RightRotate()
            // 然后把自己右旋
            return avlNode.LeftRotate()
        }
        

上面我們展示了針對具體的四種情況,是怎么怎么旋轉的,但是我們要寫一個函數用來判斷這四種情況:

// 調整AVL樹的平衡
func (avlNode *AVLNode) adjust() *AVLNode {
    // 如果右子樹的高度比左子樹的高度大于2
    if avlNode.Right.GetHeight() - avlNode.Left.GetHeight() == 2 {
        // 如果 avlNode.Right 的右子樹的高度比avlNode.Right的左子樹高度大
        // 直接對avlNode進行左旋轉
        // 否則先對 avlNode.Right進行右旋轉然后再對avlNode進行左旋轉
        if avlNode.Right.Right.GetHeight() > avlNode.Right.Left.GetHeight() {
            avlNode = avlNode.LeftRotate()
        }else{
            avlNode = avlNode.RightThenLeftRotate()
        }
        // 如果左子樹的高度比右子樹的高度大2
    }else if avlNode.Right.GetHeight() - avlNode.Left.GetHeight() == -2 {
        // 如果avlNode.Left的左子樹高度大于avlNode.Left的右子樹高度
        // 那么就直接對avlNode進行右旋
        // 否則先對avlNode.Left進行左旋,然后對avlNode進行右旋
        if avlNode.Left.Left.GetHeight() > avlNode.Left.Right.GetHeight() {
            avlNode = avlNode.RightRotate()
        }else {
            avlNode = avlNode.LeftThenRightRotate()
        }
    }

    return avlNode
}

golang 實現

AVLNode

type AVLNode struct {
    Left, Right *AVLNode    // 表示指向左孩子和右孩子
    Data        interface{} // 結點存儲數據
    height      int         // 記錄這個結點此時的高度
}

AVL樹還需要一些簡單的獲取和設置結點性質的方法

注意:每次NewAVLNode的時候height一定是1,因為一個簡單的高度最低就是1

// 定義comparer 指針類型
// 用來比較兩個結點中Data的大小
type comparator func(a, b interface{}) int

// compare 指針
var compare comparator

// 新建一個結點
func NewAVLNode(data interface{}) *AVLNode {
    return &AVLNode{
        Left:   nil,
        Right:  nil,
        Data:   data,
        height: 1,
    }
}

// 新建AVL 樹
func NewAVLTree(data interface{}, myfunc comparator) (*AVLNode, error) {
    if data == nil && myfunc == nil {
        return nil, errors.New("不能為空")
    }
    compare = myfunc
    return NewAVLNode(data), nil
}

// 獲取結點數據
func (avlNode *AVLNode) GetData() interface{} {
    return avlNode.Data
}

// 設置結點數據
func (avlNode *AVLNode) SetData(data interface{}) {
    if avlNode == nil {
        return
    }
    avlNode.Data = data
}

// 獲取結點的右孩子結點
func (avlNode *AVLNode) GetRight() *AVLNode {
    if avlNode == nil {
        return nil
    }
    return avlNode.Right
}

// 獲取結點的左孩子結點
func (avlNode *AVLNode) GetLeft() *AVLNode {
    if avlNode == nil {
        return nil
    }
    return avlNode.Left
}

// 獲取結點的高度
func (avlNode *AVLNode) GetHeight() int {
    if avlNode == nil {
        return 0
    }
    return avlNode.height
}

//比較兩個子樹高度的大小
func Max(a, b int) int {
    if a >= b {
        return a
    } else {
        return b
    }
}

查找結點

查找指定結點

// 查找指定值
func (avlNode *AVLNode) Find(data interface{}) *AVLNode {
    var finded *AVLNode
    // 調用比較函數比較兩個結點的指的大小
    switch compare(data, avlNode.Data) {
    case -1:
        finded = avlNode.Left.Find(data)
    case 1:
        finded = avlNode.Right.Find(data)
    case 0:
        return avlNode
    }

    return finded
}

查找最大結點

// 查找最大值
func (avlNode *AVLNode) FindMax() *AVLNode {
    var finded *AVLNode

    if avlNode.Right != nil {
        finded = avlNode.Right.FindMin()
    } else {
        finded = avlNode
    }

    return finded
}

查找最小結點

// 查找最小值
func (avlNode *AVLNode) FindMin() *AVLNode { // 遞歸寫法
    var finded *AVLNode

    if avlNode.Left != nil {
        finded = avlNode.Left.FindMin()
    } else {
        finded = avlNode
    }

    return finded
}

插入和刪除

插入

// 插入數據
// 因為沒有定義 結點的parent指針,所以你插入數據就只能遞歸的插入,因為我要調整樹的平衡和高度
func (avlNode *AVLNode) Insert(value interface{}) *AVLNode {
    if avlNode == nil {
        return NewAVLNode(value)
    }

    switch compare(value, avlNode.Data) {
    case -1:
        // 如果value 小于 avlNode.Data 那么就在avlNode的左子樹上去插入value
        avlNode.Left = avlNode.Left.Insert(value)
        avlNode = avlNode.adjust() // 自動調整平衡
    case 1:
        avlNode.Right = avlNode.Right.Insert(value)
        avlNode = avlNode.adjust()
    case 0:
        fmt.Print("數據已經存在")
    }
    // 修改結點的高度
    avlNode.height = Max(avlNode.Left.GetHeight(), avlNode.Right.GetHeight()) + 1

    return avlNode
}

插入數據的時候我們應該注意的是,我們要針對插入點相關的父結點,都要判斷是否平衡,然后就行平衡的調整。

刪除

// 刪除數據
func (avlNode *AVLNode) Delete(value interface{}) *AVLNode {
    // 沒有找到匹配的數據
    if avlNode == nil {
        //fmt.Println("不存在", value)
        return nil
    }

    switch compare(value, avlNode.Data) {
    case 1:
        avlNode.Right = avlNode.Right.Delete(value)
    case -1:
        avlNode.Left = avlNode.Left.Delete(value)
    case 0:
        // 找到數據,刪除結點
        if avlNode.Left != nil && avlNode.Right != nil { // 結點有左孩子和右孩子
            avlNode.Data = avlNode.Right.FindMin().Data
            avlNode.Right = avlNode.Right.Delete(avlNode.Data)
        } else if avlNode.Left != nil { // 結點只有左孩子,無右孩子
            avlNode = avlNode.Left
        } else { // 結點只有右孩子或者無孩子
            avlNode = avlNode.Right
        }

    }

    // 自動調整平衡, 如果avlNode!=nil說明執行了對avlNode 的某個子樹執行了刪除結點,那么就需要重新調整樹的平衡
    if avlNode != nil {
        avlNode.height = Max(avlNode.Left.GetHeight(), avlNode.Right.GetHeight()) + 1
        avlNode = avlNode.adjust() // 自動平衡
    }

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

推薦閱讀更多精彩內容