1. 程式人生 > >數據結構(一)-- 平衡樹

數據結構(一)-- 平衡樹

empty 閱讀 教程 參考 可變 min itl 樹性能 9.png

文章是對鄧俊輝老師數據結構教程的總結,部分圖片資料來自鄧俊輝老師的教學PPT

建議閱讀前先閱讀參考文章的第二,三文章,總結得非常好!

文章部分代碼和圖片來自參考文章的第二,三文章!!

閱讀前提幾個問題吧 ,幫助思考

  • 為什麽需要平衡二叉樹
  • AVL 需要兩次旋轉的操作為什麽不直接分解為左旋和右旋,還要LR RL 呢
  • AVL 有什麽局限性

二叉查找樹 (Binary Search Tree -- BST)

若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;

從思路上講我們希望二叉查找樹可以結合向量和鏈表的結構,但是在某些情況下,時間復雜度還是沒能達到要求。

技術分享圖片

像上面的情況講,最壞的情況取決於樹的高度,當生成像圖中的結構時,二叉查找樹就成了單鏈表,查找效率降至O(n),查找時自然是從第一個查找到最後一個,這樣的時間復雜度是無法讓人接受的。

理想平衡

既然二叉搜索樹性能取決於其高度,那我們自然想到盡可能降低其高度,使兄弟子樹的高度彼此接近。

由於包含n個節點的二叉樹,高度不可能小於[log2n],若恰好高為[log2n],則稱為理想平衡樹,如完全二叉樹(CBT)、滿二叉樹均屬此類。

然而,葉節點只能出現於最底兩層的限制過於苛刻,此類二叉樹所占比例極低,從算法可行性角度來看,應當依照某種相對寬松的標準,重新定義。即是說我們可以約定一定的條件使得某棵樹可以接近或是等同於理想平衡樹,我們就可以達到目的了,就是下圖中的平衡二叉搜索樹(Balance BST---BBST)。

至此我們知道了有這麽幾種樹 :

  • 二叉搜索樹 BST
  • 完全二叉樹 CBT
  • 平衡二叉搜索樹 BBST

下圖解釋了理想平衡(理想目標)適度平衡(追求的目標)

技術分享圖片


技術分享圖片

等價BST

技術分享圖片

上面這張圖可以看到兩個平衡樹,中序遍歷是相同的(樹下面的數字),但是撲拓結構是不同的,兩者是等價的,他們之間的特點是 : 上下可變,左右不亂。(這個非常重要,後面為使樹適度平衡實際上就是在依據這兩個特點來進行的!!)

兩者之間的變換可以通過以下的方式 :

技術分享圖片

可以看到 zig 是順時針旋轉,而 zag 是逆時針旋轉,目的是使二叉樹平衡。最多操作的次數不要超過 Logn

次交換(Logn 是適度平衡的樹的高度,即是說時間復雜度達到了適度平衡樹的高度是最壞的情況----從開始變換到尾)。

我們後面講的AVL ,remove 方法最壞的情況時間復雜度是 Logn ,所以後面的紅黑樹等會繼續對他改良。

關於的BST 的API方法可以看這一篇文章 : http://www.cnblogs.com/penghuwan/p/8057482.html#_label10

AVL (Adelson-Velskii and Landis)

下面部分代碼和圖片來自參考資料

講 AVL 樹之前我們先來看看 CBT BBST BST 之間的關系,我們構建一棵適度平衡的樹最重要要解決的兩個問題就是 :

  • 如何界定BBST
  • rebalance 的邏輯實現

後面我們會接觸到各種適度平衡的樹就是圍繞這兩個方面展開的,回到圖中,其中 AVL 就是紫色點(有點小,認真看),存在於BBST中,當失衡的時候跑到了BBST之外,通過rebalance 操作重新回到了 BBST 之中。

技術分享圖片

下面開始介紹AVL 。AVL 是名字的縮寫,是發明這個數據結構的人。學習AVL 這個數據結構之前,我們先對二叉堆高度進行定義

接下來是AVL的定義 :(來自維基百科)

In a binary tree the balance factor of a node N is defined to be the height difference

BalanceFactor(N) := Height(RightSubtree(N)) – Height(LeftSubtree(N)) [6]

of its two child subtrees. A binary tree is defined to be an AVL tree if the invariant

BalanceFactor(N) ∈ {–1,0,+1}[7]

holds for every node N in the tree.

平衡因子 = 左子樹節點高度 - 右子樹節點高度

平衡二叉樹(AVL): 所有結點的平衡因子的絕對值都不超過1。即對平衡二叉樹每個結點來說,其左子樹的高度 - 右子樹高度得到的差值只能為 1, 0 , -1 這三個值。 取得小於 -1或者大於1的值,都被視為打破了二叉樹的平衡。

例如下圖

技術分享圖片

為了使樹平衡,使用的手段有 : 左旋和右旋。

右旋(左旋一樣的)

左旋,即是逆時針旋轉;右旋, 即是順時針旋轉。

下面是最簡單的右旋(下面代碼和圖片出處)

技術分享圖片

當然還有這一種情況,其中數字4代表的節點可有可無,無的情況為NULL

技術分享圖片

下面是旋轉的情況

技術分享圖片

代碼應該是

  1 /**
  2    * @description: 右旋方法
  3    */
  4   private Node rotateRight (Node x) {
  5     Node y = x.left; // 取得x的左兒子
  6     x.left = y.right; // 將x左兒子的右兒子("拖油瓶"結點)鏈接到旋轉後的x的左鏈接中
  7     y.right = x; // 調轉x和它左兒子的父子關系,使x成為它原左兒子的右子樹
  8     x.height = max(height(x.left),height(x.right)) + 1; // 更新並維護受影響結點的height
  9     y.height = max(height(y.left),height(y.right)) + 1; // 更新並維護受影響結點的height
 10     return y; // 將y返回
 11   }

其中x為失衡點。而左旋的分析和右旋的情況是一樣的。但是一個失衡點有可能是包含了左旋後右旋,或是右旋後左旋。所有下面羅列一下使樹平衡會遇到的情況。

四種情況

下面總結來自參考資料

1. 單次右旋: 由於在a的左子樹的根結點的左子樹上插入結點(LL),使a的平衡因子由1變成2, 導致以a為根的子樹失去平衡, 則需進行一次的向右的順時針旋轉操作

技術分享圖片

2. 單次左旋: 由於在a的右子樹根結點的右子樹上插入結點(RR),a的平衡因子由-1變成-2,導致以a為根結點的子樹失去平衡,則需要進行一次向左的逆時針旋轉操作

技術分享圖片

3. 兩次旋轉、先左旋後右旋: 由於在a的左子樹根結點的右子樹上插入結點(LR), 導致a的平衡因子由1變成2,導致以a為根結點的子樹失去平衡,需要進行兩次旋轉, 先左旋後右旋

技術分享圖片

4.兩次旋轉, 先右旋後左旋: 由於在a的右子樹根結點的左子樹上插入結點(RL), a的平衡因子由-1變成-2,導致以a為根結點的子樹失去平衡, 則需要進行兩次旋轉,先右旋後左旋

技術分享圖片

那麽問題來了,怎麽分別判斷LL, RR,LR,RL這四種破環平衡的場景呢?

我們可以根據當前破壞平衡的結點的平衡因子, 以及其孩子結點的平衡因子來判斷,具體如下圖所示:

技術分享圖片

(BF表示平衡因子, 最下方的那個結點是新插入的結點)

插入和刪除

代碼出處見參考資料,非原創

先放出完整代碼

  1 package Avl;
  2 
  3 import java.util.LinkedList;
  4 
  5 /**
  6  * @Author: HuWan Peng
  7  * @Date Created in 10:35 2017/12/29
  8  */
  9 public class AVL {
 10   Node root; // 根結點
 11 
 12   private class Node {
 13     int key, val;
 14     Node left, right;
 15     int height = 1; // 每個結點的高度屬性
 16 
 17     public Node(int key, int val) {
 18       this.key = key;
 19       this.val = val;
 20     }
 21   }
 22 
 23   /**
 24    * @description: 返回兩個數中的最大值
 25    */
 26   private int max(int a, int b) {
 27     return a > b ? a : b;
 28   }
 29 
 30   /**
 31    * @description: 獲得當前結點的高度
 32    */
 33   private int height(Node x) {
 34     if (x == null)
 35       return 0;
 36     return x.height;
 37   }
 38 
 39   /**
 40    * @description: 獲得平衡因
 41    */
 42   private int getBalance(Node x) {
 43     if (x == null)
 44       return 0;
 45     return height(x.left) - height(x.right);
 46   }
 47 
 48   /**
 49    * @description: 右旋方法
 50    */
 51   private Node rotateRight(Node x) {
 52     Node y = x.left; // 取得x的左兒子
 53     x.left = y.right; // 將x左兒子的右兒子("拖油瓶"結點)鏈接到旋轉後的x的左鏈接中
 54     y.right = x; // 調轉x和它左兒子的父子關系,使x成為它原左兒子的右子樹
 55     x.height = max(height(x.left), height(x.right)) + 1; // 更新並維護受影響結點
 56     y.height = max(height(y.left), height(y.right)) + 1; // 更新並維護受影響結點
 57     return y; // 將y返回
 58   }
 59 
 60   /**
 61    * @description: 左旋方法
 62    */
 63   private Node rotateLeft(Node x) {
 64     Node y = x.right; // 取得x的右兒子
 65     x.right = y.left; // 將x右兒子的左兒子("拖油瓶"結點)鏈接到旋轉後的x的右鏈接中
 66     y.left = x; // 調轉x和它右兒子的父子關系,使x成為它原右兒子的左子樹
 67     x.height = max(height(x.left), height(x.right)) + 1; // 更新並維護受影響結點
 68     y.height = max(height(y.left), height(y.right)) + 1; // 更新並維護受影響結點
 69     return y; // 將y返回
 70   }
 71 
 72   /**
 73    * @description: 平衡 操作
 74    */
 75   private Node reBalance(Node x) {
 76     int balanceFactor = getBalance(x);
 77     if (balanceFactor > 1 && getBalance(x.left) > 0) { // LL型,進行單次右旋
 78       return rotateRight(x);
 79     }
 80     if (balanceFactor > 1 && getBalance(x.left) <= 0) { // LR型 先左旋再右旋
 81       Node t = rotateLeft(x);
 82       return rotateRight(t);
 83     }
 84     if (balanceFactor < -1 && getBalance(x.right) <= 0) {// RR型, 進行單次左旋
 85       return rotateLeft(x);
 86     }
 87     if (balanceFactor < -1 && getBalance(x.right) > 0) {// RL型,先右旋再左旋
 88       Node t = rotateRight(x);
 89       return rotateLeft(t);
 90     }
 91     return x;
 92   }
 93 
 94   /**
 95    * @description: 插入結點(鍵值對)
 96    */
 97   public Node put(Node x, int key, int val) {
 98     if (x == null)
 99       return new Node(key, val); // 插入鍵值對
100     if (key < x.key)
101       x.left = put(x.left, key, val); // 向左子樹遞歸插入
102     else if (key > x.key)
103       x.right = put(x.right, key, val); // 向右子樹遞歸插入
104     else
105       x.val = val; // key已存在, 替換val
106 
107     x.height = max(height(x.left), height(x.right)) + 1; // 沿遞歸路徑從下至上更新結點height屬性
108     x = reBalance(x); // 沿遞歸路徑從下往上, 檢測當前結點是否失衡,若失衡則進行平衡化  
109     return x;
110   }
111 
112   public void put(int key, int val) {
113     root = put(root, key, val);
114   }
115 
116   /**
117    * @description: 返回最小鍵
118    */
119   private Node min(Node x) {
120     if (x.left == null)
121       return x; // 如果左兒子為空,則當前結點鍵為最小值,返回
122     return min(x.left); // 如果左兒子不為空,則繼續向左遞歸
123   }
124 
125   public int min() {
126     if (root == null)
127       return -1;
128     return min(root).key;
129   }
130 
131   /**
132    * @description: 刪除最小鍵的結點
133    */
134   public Node deleteMin(Node x) {
135     if (x.left == null)
136       return x.right; // 如果當前結點左兒子空,則將右兒子返回給上一層遞歸的x.left
137     x.left = deleteMin(x.left);// 向左子樹遞歸, 同時重置搜索路徑上每個父結點指向左兒子的鏈接
138     return x; // 當前結點不是min
139   }
140 
141   public void deleteMin() {
142     root = deleteMin(root);
143   }
144 
145   /**
146    * @description: 刪除給定key的鍵值對
147    */
148   private Node delete(int key, Node x) {
149     if (x == null)
150       return null;
151     if (key < x.key)
152       x.left = delete(key, x.left); // 向左子樹查找鍵為key的結點
153     else if (key > x.key)
154       x.right = delete(key, x.right); // 向右子樹查找鍵為key的結點
155     else {
156       // 結點已經被找到,就是當前的x
157       if (x.left == null)
158         return x.right; // 如果左子樹為空,則將右子樹賦給父節點的鏈接
159       if (x.right == null)
160         return x.left; // 如果右子樹為空,則將左子樹賦給父節點的鏈接
161       Node inherit = min(x.right); // 取得結點x的繼承結點
162       inherit.right = deleteMin(x.right); // 將繼承結點從原來位置刪除,並重置繼承結點右鏈接
163       inherit.left = x.left; // 重置繼承結點左鏈接
164       x = inherit; // 將x替換為繼承結點
165     }
166     if (root == null)
167       return root;
168     x.height = max(height(x.left), height(x.right)) + 1; // 沿遞歸路徑從下至上更新結點height屬性
169     x = reBalance(x); // 沿遞歸路徑從下往上, 檢測當前結點是否失衡,若失衡則進行平衡化  
170     return x;
171   }
172 
173   public void delete(int key) {
174     root = delete(key, root);
175   }
176 
177 
178   /**
179    * 二叉樹層序遍歷
180    */
181   private void levelIterator() {
182     LinkedList<Node> queue = new LinkedList<Node>();
183     Node current = null;
184     int childSize = 0;
185     int parentSize = 1;
186     queue.offer(root);
187     while (!queue.isEmpty()) {
188       current = queue.poll();// 出隊隊頭元素並訪問
189       System.out.print(current.val + "-->");
190       if (current.left != null)// 如果當前節點的左節點不為空入隊
191       {
192         queue.offer(current.left);
193         childSize++;
194       }
195       if (current.right != null)// 如果當前節點的右節點不為空,把右節點入隊
196       {
197         queue.offer(current.right);
198         childSize++;
199       }
200       parentSize--;
201       if (parentSize == 0) {
202         parentSize = childSize;
203         childSize = 0;
204         System.out.println("");
205       }
206     }
207   }
208 
209   public void printOutMidNums(){
210 
211   }
212 
213   public static void main(String[] args) {
214     AVL avl = new AVL();
215     avl.put(1, 11);
216     avl.put(2, 22);
217     avl.put(3, 33);
218     avl.put(4, 44);
219     avl.put(5, 55);
220     avl.put(6, 66);
221     avl.levelIterator();
222   }
223 }

插入操作的代碼,需要註意的是rotateLeft 或是 rotateRight 方法內部是沒有連接上一個父節點的操作的,重連父節點的操作在遞歸中。

刪除操作的解釋見 http://www.cnblogs.com/penghuwan/p/8057482.html#_label10,下面引用這篇文章的話來解釋刪除操作。

首先介紹 繼承結點,繼承結點就是某個結點被刪除後,能夠“繼承”某個結點的結點,下面的圖片是繼承結點的定義

技術分享圖片

它的作用,用一個例子來說明一下。

技術分享圖片技術分享圖片

相對於14,15是它的繼承結點,假若14 被15替換掉,16成為18的左子樹節點。那麽仍然能保持整顆二叉查找樹的有序性。

下面說一下刪除的三種情況

技術分享圖片技術分享圖片技術分享圖片

其中第三種就是使用繼承結點的情況。其中需要註意的是,

技術分享圖片

有可能刪除操作後,子樹的高度 –1 ,而連接子樹的上層父節點或是祖父節點因為子樹的高度 –1 ,導致繼續 rebalance,所以刪除操作的最壞情況可以達到 Logn

AVL 綜合評價

技術分享圖片

補充

這裏推薦一個網站,動態算法的網站 :

https://www.cs.usfca.edu/~galles/visualization/AVLtree.html

參考資料

  • https://zhuanlan.zhihu.com/p/40987633
  • https://www.cnblogs.com/patientcat/p/9720308.html
  • https://www.cnblogs.com/penghuwan/p/8166133.html
  • http://www.cnblogs.com/penghuwan/p/8057482.html#_label10

數據結構(一)-- 平衡樹