Go 高度平衡二元搜尋樹

tags: Go BinarySearchTree AVL Tree
category: 資料結構與演算法
description: Go 建立高度平衡二元搜尋樹
created_at: 2021/07/31 21:00:00

cover image

事前準備

  • 知道什麼是二元搜尋樹
  • 準備好想跟樹做好朋友的心
  • 看過上一篇 Go 二元搜尋樹

前言

很久很久以前,還在高三擔任程式選手的時候,有使用過 VB 實作 AVL Tree,那次寫了一整個早上才做出來,成就感滿滿,這次想說用 Go 練練手,而過了好幾年,寫出來也簡潔很多。


目標


高度平衡二元搜尋樹

先從二元搜尋樹最致命的缺點來看,如果你給他的資料順序剛好全部遞增或遞減,就會造成歪斜的狀況,就算不是全部剛好遞增或遞減,只要稍微有點歪斜,效率就會變得非常差。

如果你剛好使用 OOP 的方式建立一個完全歪斜的二元搜尋樹,那麼恭喜你,沒有浪費什麼空間,就只是找起來效率差的跟陣列一樣

但是如果你剛好使用 Array 的方式去建立呢,那麼恭喜你,記憶體可能會爆炸。總之就是很浪費空間就是了。

基本上跟二元搜尋樹的規則一樣,只是加入了自平衡的特性。


先來看一下陣列通常怎麼存二元樹

雖然說陣列從 0 開始編號,但是在存二元搜尋樹時,為了方便計算有時會刻意從 1 開始使用。 所以你可以發現各層的索引範圍

  • 第一層: 1
  • 第二層: 2 ~ 3
  • 第三層: 4 ~ 7
  • 第四層: 8 ~ 15
  • 第N層 :

所以換句話說,如果你有4個節點的歪斜樹,你就需要開 2^4 大小的陣列。

  • 如果你有10個節點,2^10=1024
  • 如果你有65536個,2^65536=19729位數的數字

搜尋起來雖然跟陣列 O(N) 一樣的速度,但是你所花費的空間多上太多。 所以我們需要讓二元搜尋樹自己平衡,不要長歪。


在操作之前

操作之前我們必須知道,在他旋轉完成之後,中序走訪結果不能有任何改變,如果改變,代表旋轉的操作是錯誤的。

中序走訪結果不同,代表結構已經被破壞掉了


什麼情況下不平衡

當左子樹與右子樹的樹高差距大於 1 的時候

想當初學的時候書上會寫 平衡因子(BF, Balance Factor) 這種東西,就是 左子樹高 - 右子樹高 的結果。

如果根據上面定義,就可以知道說當 BF值 < 0 代表樹往右邊歪,反之則是往左邊歪,而正好等於零的情況則是完全沒有失衡。


AVL Tree 的四種操作

有點懶得找跟畫示意圖,總之照著上面說的,操作完之後中序走訪結果要相同。

LL 旋轉

當樹往左邊傾斜的時候,應該把左子樹拉上來

LR 旋轉

雖然樹往左邊傾斜,但是左子樹往右傾斜,就必須先救一下左子樹(右旋轉)再進行左旋轉,所以要做兩次旋轉。

偷懶一下,另外兩個當然就是的 RR 跟 RL 啦,跟上面相反,應該可以想像。


Go 實作

我們希望我們的 Node 具有 Balance() 方法 , 負責把自己這個 子樹 平衡,而我們在每次新增節點都有可能造成失衡,所以要在新增最後吐回去 Balance 後的結果。

func (node *Node) Add(num int) *Node{
    if node == nil {
        return &Node{Value: num}
    }

    if num < node.Value {
        node.Left = node.Left.Add(num)
    } else {
        node.Right = node.Right.Add(num)
    }

    return node.Balance()
}

基本上只差在最後一行從回傳 node 變成 node.Balance()

在實作 Balance() 之前,我們需要先有辦法取得 BF值

func (node *Node) GetBFValue() int {
    return node.Left.GetHeight() - node.Right.GetHeight()
}

在寫好了取得樹高的函數之後,要取得 BF值 相當容易。


核心程式碼

func (node *Node) Balance() *Node {
    value := node.GetBFValue()

    if math.Abs(float64(value)) <= 1 {
        return node
    }

    if value > 1 {
        checkValue := node.Left.GetBFValue()
        if checkValue < 0 {
            node.Left = node.Left.RightRotate()
        }
        return node.LeftRotate()
    } else {
        checkValue := node.Right.GetBFValue()
        if checkValue > 0 {
            node.Right = node.Right.LeftRotate()
        }
        return node.RightRotate()
    }
}

先取得 BF值 之後,再根據 BF值 去做相對應的處理,再來就是要實現左旋轉與右旋轉的函數,基本上就是對於指標的操作。

func (node *Node) LeftRotate() *Node {
    newRoot := &Node{Value: node.Left.Value}
    newRoot.Left = node.Left.Left
    newRoot.Right = node
    newRoot.Right.Left = node.Left.Right

    return newRoot
}

func (node *Node) RightRotate() *Node {
    newRoot := &Node{Value: node.Right.Value}
    newRoot.Right = node.Right.Right
    newRoot.Left = node
    newRoot.Left.Right = node.Right.Left

    return newRoot
}

到這裡就完成 AVL Tree 的實作了。


測試程式

nums := []int {1,2,3,4,5,6,7}
var tree BinarySearchTree
tree.BuildTree(nums)
fmt.Printf("Build tree: %v\n", nums)
fmt.Printf("tree.PreOrder = %v\n", tree.PreOrder())
fmt.Printf("tree.InOrder = %v\n", tree.InOrder())
fmt.Printf("tree.PostOrder = %v\n", tree.PostOrder())
fmt.Printf("tree.GetHeight = %v\n", tree.GetHeight())

tree.Add(8)
tree.Add(9)
tree.Add(10)
fmt.Printf("Insert node 8, 9, 10: \n")
fmt.Printf("tree.PreOrder = %v\n", tree.PreOrder())
fmt.Printf("tree.InOrder = %v\n", tree.InOrder())
fmt.Printf("tree.PostOrder = %v\n", tree.PostOrder())
fmt.Printf("tree.GetHeight = %v\n", tree.GetHeight())

輸出:

Build tree: [1 2 3 4 5 6 7]
tree.PreOrder = [4 2 1 3 6 5 7]
tree.InOrder = [1 2 3 4 5 6 7]
tree.PostOrder = [1 3 2 5 7 6 4]
tree.GetHeight = 3
Insert node 8, 9, 10:
tree.PreOrder = [4 2 1 3 8 6 5 7 9 10]
tree.InOrder = [1 2 3 4 5 6 7 8 9 10]
tree.PostOrder = [1 3 2 5 7 6 10 9 8 4]
tree.GetHeight = 4

會發現建立出來的結果相當完美,以往1234567,應該會造成一棵完全歪斜的高度為7的二元搜尋樹,被轉換成了長滿的二元搜尋樹。

而繼續一個一個新增節點,也照著我們的邏輯去做旋轉,沒有產生歪斜的情況。




最後更新時間: 2021年07月31日.