資料結構之二叉搜尋樹、AVL自平衡樹
前言
最近在幫公司校招~~ 所以來整理一些資料結構方面的知識,這些知識呢,光看一遍理解還是很淺的,看過跟動手做過一遍的同學還是很容易分辨的喲~
一直覺得資料結構跟演算法,就好比金庸小說裡的《九陽神功》,學會九陽神功後,有了內功基礎,再去學習其他武功,速度就有質的提升
內容大概包含這些,會分多篇文章來整理:
- 二叉搜尋樹
- 平衡二叉樹(AVL)
- 二叉堆
- 堆排序
- 四叉樹
- 八叉樹
- 圖,深度優先DFS、廣度優先BFS
- 最短路徑
二叉樹
二叉樹,也就是每個節點最多有兩個孩子的樹。多用於搜尋,查詢,還有可以用來求最短編碼的哈弗曼樹,也稱為最優二叉樹。
二叉排序/搜尋樹
如圖,樹的每個有孩子的節點都滿足: 左節點的值<根節點的值<右節點的值條件的樹 ,稱為二叉排序樹,也叫二叉搜尋樹。

如果對這個樹進行 中序遍歷 ,就能得到一個排序的數列,非常簡單,下面貼出插入操作跟遍歷的程式碼
插入操作
public void Add(BinaryTree node) { if (node.Value < Value) { if (this.Left != null) { this.Left.Add(node); } else { this.Left = node; } } else { if (this.Right != null) { this.Right.Add(node); } else { this.Right = node; } } }
中序遍歷輸出排序列表
public void InOrder(List<int> list) { if (Left != null) { Left.InOrder(list); } list.Add(this.Value); if (Right != null) { Right.InOrder(list); } }
但是二叉排序樹極端的情況,效率會變成連結串列線性結構,這樣查詢起來 時間複雜度會變成O(n) ,就失去了樹形結構的意義,如圖:

這時就要引出我們的另外一種二叉樹樹結構了
平衡二叉樹
平衡二叉樹(AVL)簡單來說就是插入的時候,要保證子節點的平衡,別老往一邊一直插入下去,那樣又成了連結串列效率了
首先來搞懂這個幾個定義
平衡因子:即左子樹的高度減去右子樹的高度
平衡二叉樹上所有節點的平衡因子都必須為:-1、0和1。否則該二叉樹就不是平衡二叉樹
如下圖,圖左邊是一顆平衡二叉樹,圖右根節點平衡因子為-2,則不是平衡二叉樹

如何保持樹的平衡
每當插入一個節點的時候,都檢查這次插入是否會破壞平衡性,若是,則找出最小不平衡子樹,在保持二叉排序樹的前提下,進行相應旋轉,使之成為新的平衡子樹。
通常會有四種旋轉情況:
單向右旋平衡處理
也有地方稱為Left Left旋轉,是不是覺得很奇怪,一下左,一下右邊的,它估計是想把你轉暈,好套出你的花唄密碼。
那麼到底是什麼意思呢,請看下圖

這棵樹有三個節點:6,4,2
我們把節點2當成是最新插入進來的節點,由於這個節點2的插入,導致節點6的平衡因子變成了2,不符合-1、0、1的規定,破壞了平衡性,所以我們需要對節點6進行右旋轉,而節點2又是節點6的 Left節點的Left節點 ,所以也稱為LL旋轉。
右旋操作
也就是如果結點6的左孩子節點4有右孩子,則將節點4的右孩子變成節點6的左孩子,最後將節點6變成節點4的右孩子
單向左旋平衡處理
左旋平衡處理也叫RR旋轉,是LL的映象操作

雙向旋轉(先右後左)平衡處理 (Right Left)
為什麼會有這種情況出現呢,因為我們的平衡樹,首先也是一顆二叉排序樹,必須滿足左節點<根節點<右節點的插入規則。
所以如下圖,節點4插入導致樹失去平衡,單向旋轉已經不能滿足要求了,需要先讓節點6右旋,然後再把節點2左旋

雙向旋轉(先左後右)平衡處理 (Left Right)
同理,是RL的映象操作

程式碼實現
//右旋轉 public BinaryTree RightRotate(BinaryTree root) { BinaryTree lchild = root.Left; root.Left = lchild.Right; lchild.Right = root; return lchild; } //左旋轉 public BinaryTree LeftRotate(BinaryTree root) { BinaryTree rchild = root.Right; root.Right = rchild.Left; rchild.Left = root; return rchild; } //先左後右旋轉 public BinaryTree LeftRightRotate(BinaryTree root) { root.Left = root.Left.LeftRotate(root); return RightRotate(root); } //先右後左旋轉 public BinaryTree RightLeftRotate(BinaryTree root) { root.Right = root.Right.RightRotate(root); return LeftRotate(root); } //計算平衡因子,取絕對值 public int Balance(BinaryTree root) { int val = 0; if (root.Left != null) val += Height(root.Left); if (root.Right != null) val -= Height(root.Right); return Math.Abs(val); } //計算樹的高度 public int Height(BinaryTree root) { int leftHeight = 0; int rightHeight = 0; if (root != null && root.Left != null) { leftHeight += Height(root.Left); } if (root != null && root.Right != null) { rightHeight += Height(root.Right); } return rightHeight > leftHeight ? ++rightHeight : ++leftHeight; }
插入操作
public BinaryTree Inster(BinaryTree root, int key) { if (root == null) { root = new BinaryTree(key); } else if (key < root.Value)//插入到左邊 { root.Left = Inster(root.Left, key); if (Balance(root) > 1)//插入左節點導致樹失衡了 { if (key < root.Left.Value)//LL處理,右旋 { root = RightRotate(root); } else { root = LeftRightRotate(root);//LR處理,先左後右 } } } else { root.Right = Inster(root.Right, key); if (Balance(root) > 1)//插入右節點導致失衡 { if (key > root.Right.Value)//RR處理, 左旋 { root = LeftRotate(root); } else { root = RightLeftRotate(root);//RL處理,先右後左 } } } return root; }
使用平衡二叉樹後,查詢起來時間複雜度就從O(n)變為了O( log n)。
總結
平衡二叉樹的優點在於因為樹結構維護的較好,所以搜尋查詢速度很快,但在插入,刪除的時候,為了保持樹的平衡會做一次或多次旋轉。
適合用於插入刪除操作少,而搜尋操作很多的情況。
為了減少插入,刪除在旋轉方面的消耗,另一種自平衡樹結構出現了
它就是: 紅黑樹
紅黑樹不追求"完全平衡",即不像AVL那樣要求節點的 |平衡因子| <= 1,它只要求部分達到平衡,但是提出了為節點增加顏色,紅黑是用非嚴格的平衡來換取增刪節點時候旋轉次數的降低,任何不平衡都會在三次旋轉之內解決,而AVL是嚴格平衡樹,因此在增加或者刪除節點的時候,根據不同情況,旋轉的次數比紅黑樹要多。
學會了AVL在去看紅黑樹也就很簡單了~~