AVL樹(平衡二叉查找樹)
首先要說AVL樹,我們就必須先說二叉查找樹,先介紹二叉查找樹的一些特性,然後我們再來說平衡樹的一些特性,結合這些特性,然後來介紹AVL樹。
一、二叉查找樹
1、二叉樹查找樹的相關特征定義
二叉樹查找樹,又叫二叉搜索樹,是一種有順序有規律的樹結構。它可以有以下幾個特征來定義它:
(1)首先它是一個二叉樹,具備二叉樹的所有特性,他可以有左右子節點(左右孩子),可以進行插入,刪除,遍歷等操作;
(2)如果根節點有左子樹,則左子樹上的所有節點的值均小於根節點上的值,如果根節點有右子樹,則有字數上的所有節點的值均大於根節點的值;
(3)它的中序遍歷是一個有序數組。
下圖即是一個比較典型的二叉查找樹:
2、二叉查找樹的插入操作
首先,我們記住一點,二叉查找樹的插入操作,插入後的節點一定為葉子節點,你可以這樣想,一個剛插入的節點,它肯定沒有孩子節點,這樣它只能是葉子結點。這樣,二叉查找樹的插入操作就顯得很簡單,我們只要將插入的值依次和相應的根節點比較:
(1)如果根節點為空時,則創建一個節點,這個節點的值為插入的值,將該節點賦值給根節點;
(2)如果比根節點值小,則進入左子樹,獲得左子樹的根節點;(即這個節點一定是插入左子樹的某一位置)
(3)如果比根節點值大,則進入右子樹,獲得右子樹的根節點;(即這個節點一定是插入左子樹的某一位置)
例如:利用上圖插入值為1的節點,我們的操作如下:
(1)首先插入的值1與根節點值5比較,比5小,則進入左子樹,獲得左子樹的根節點
(2)將插入的值1與當前根節點值3比較,比3小,則進入左子樹,獲得左子樹的根節點
(3)將插入的值1與當前根節點值2比較,比2小,則進入左子樹,獲得左子樹的根節點
(4)判斷發下當前根節點為null,創建值為1的節點,並將該節點賦值給當前根節點。
則插入後的二叉查找樹為:
3、二叉查找樹的刪除操作
二叉查找樹相對於插入會相對麻煩一點,但是我們大致可以將刪除的情況分為以下四種情況:
(1)刪除的是葉子節點,即節點沒有孩子節點,這個最為簡單直接將其刪除就可以;
例如:我們要刪除值為1的葉子節點:
(2)刪除的節點只有有左孩子節點,則將左孩子節點直接代替根節點;
例如:刪除節點值為2的節點
(3)刪除的節點只有有右孩子節點,則將右孩子節點直接代替根節點;(跟上述(2)基本相同,這裏不進行舉例)
(4)刪除的節點既有左孩子也有右孩子,這種情況相對復雜,我們盡量不去改動此節點上層的節點,我們去左右子樹找一個與此節點的值比較相近的節點來代替這個節點即可,然後刪除代替節點原本節點(而很顯然這個節點一定為葉子節點)。我們采取的方法兩種是:
1)在左子樹中找到最大的值節點,將改值復制給根節點,然後刪除左子樹找到的節點;
2)在右子樹中找到最小的值節點,將改值復制給根節點,然後刪除右子樹找到的節點;
例如:我們想要刪除值為5的節點(我們采取的是1)的方式)
4、二叉查找樹的中序遍歷
二叉查找樹的中序遍歷是非常有意義的,所以這裏我只介紹中序遍歷,二叉查找樹的所有遍歷和正常的二叉樹遍歷沒有區別。
中序遍歷的執行順序為:左 根 右,根據二叉查找樹的性質,我們可以清楚知道這是一個有序數組。
例如遍歷上述的樹:
中序遍歷的結果為:1-2-3-4-5-6-7-8
二、平衡樹
1、平衡樹的相關定義
平衡樹相對一般樹多引入了一個高度的概念,即每個節點記錄了以此節點為根節點的子樹的高度。然後每個節點的所有子樹的高度差必須小於等於平衡值(一般為1)。
節點的高度規定:
(1)若節點為null時,節點的高度為-1;
(2)若節點不為null時,節點的高度為子樹的高度的最大值+1;
以二叉平橫樹為例:
2、關於平橫樹的平衡操作
對於平衡樹來說,往往是在插入和刪除操作之後,導致原本的平橫樹失衡,這裏我們不進行插入和刪除操作的講解了,而且這裏我也不準備對平衡操作進行講解,因為平衡的話我們必須要有平衡的限制條件,不然我們對於一個情況來說,我們平衡操作不唯一,價值性其實不高。我準備在平衡二叉查找樹中講解平衡操作,這樣有二叉查找樹的相關要求限制平衡操作,而且這種平衡會顯得很意義。
三、AVL(平衡二叉查找樹)
寫了這麽久,終於引出了AVL樹了,不容易啊。
1、AVL(平衡二叉查找樹)的相關特性和定義
我們可以通過二叉查找樹和平衡樹來定義AVL樹:
(1)首先它有二叉查找樹的所有特性,如果根節點有左子樹,則左子樹上的所有節點的值均小於根節點的值,如果根節點有右子樹,則有字數上的所有節點的值均大於根節點的值。
(2)每個節點都會有一個高度值,左子樹的高度值和右子樹的高度值差值應該小於規定的平衡值(一般為1)。
總結在一起為:AVL樹是一個帶有平衡條件約束的二叉查找樹。
2、AVL樹的平衡操作(旋轉)
首先,作為一個樹形結構,它一定有插入和刪除操作,之前不是說了平衡樹的平衡操作是在插入刪除操作之後進行的麽,為什麽不先將AVL樹的插入和刪除?AVL樹的插入和刪除操作和二叉查找樹幾乎一模一樣,唯一區別就是插入操作刪除操作之後會有一個平衡操作。所以在這裏我們只需要講清楚如何進行平衡操作結合前面講解的二叉查找樹的插入刪除操作就可以明白AVL樹的插入刪除操作了。另外AVL樹的遍歷操作和二叉查找樹一模一樣。
AVL樹的平衡旋轉操作總共有四種情況:每次都是k1處失衡
補充:
(1)與左孩子節點單旋轉(以根為基準)
(2)與右孩子節點單旋轉(以根為基準)
在這裏我們對單旋轉進行一下總結,雖然圖中給的是三個明確的節點,但事實上只有兩個節點(紅色圈起來的節點)進行相應的旋轉,發生了相應的狀態的改變,另一個節點k3是沒有狀態改變的。
(3)雙旋轉,先根的左孩子和其右孩子旋轉,然後根最後與左孩子節點單旋轉
(4)雙旋轉,先根的右孩子和其左孩子旋轉,然後根最後與右孩子節點單旋轉
總結:之前說單旋轉只涉及到兩個節點的狀態發生變化,而雙旋轉則是三個節點狀態都發生改變。其實雙旋轉的過程也是兩個單旋轉的過程,而且這兩個單旋轉也就是上面所說的兩個單旋轉的情況,比如說:情況(4),是k2和k3節點先進行了(2)旋轉,轉化為類似(1)情況,然後在k1和k3進行(1)旋轉。
3、AVL樹的相關操作的代碼實現(Java)
1 package Tree; 2 /* 3 * avl樹是一種平衡二叉查找樹,了解了avl樹的相關操作,將會有利於對排序和樹的知識的理解 4 */ 5 /* 6 * 創建avl樹節點,這個樹的節點有: 7 * 1、左右子樹的引用 8 * 2、樹節點存儲的值 9 * 3、該節點樹高度差 10 */ 11 class AVLTreeNode{ 12 public AVLTreeNode avlLeft;//左孩子 13 public AVLTreeNode avlRight;//右孩子 14 public int data;//節點存儲的值 15 public int height;//節點樹高度值 16 public AVLTreeNode(int data,AVLTreeNode avlLeft,AVLTreeNode avlRight){ 17 this.avlLeft=avlLeft; 18 this.data=data; 19 this.avlRight=avlRight; 20 } 21 } 22 23 public class AVLTree { 24 private static final int ALLOWED_IMBALANCE=1;//規定了AVL樹允許左右子樹高度差值的最大值 25 //用於計算當前節點的樹高度差 26 public int height(AVLTreeNode t){ 27 int mark=t==null? -1:t.height; 28 return mark; 29 } 30 31 /* 32 * 插入操作(傳入參數) 33 * m 插入的新的節點值 34 * t AVL樹的根結點 35 */ 36 public AVLTreeNode insert(int m,AVLTreeNode t){ 37 if(t==null){ 38 return t=new AVLTreeNode(m, null, null); 39 } 40 if(t.data>m){ 41 t.avlLeft=insert(m,t.avlLeft); 42 }else if(t.data<m){ 43 t.avlRight=insert(m, t.avlRight); 44 }else{ 45 ;//防止出現添加相同數字的現象 46 } 47 return balance(t); 48 } 49 50 /* 51 * AVL樹的刪除操作 52 * m 要刪除節點的值 53 * t AVL樹的根節點 54 */ 55 public AVLTreeNode delete(int m,AVLTreeNode t){ 56 if(t==null){ 57 return t; 58 } 59 if(t.data>m){ 60 t.avlLeft=delete(m, t.avlLeft); 61 }else if(t.data<m){ 62 t.avlRight=delete(m,t.avlRight); 63 }else if(t.avlLeft!=null&&t.avlRight!=null){//如果左右子樹非空,該如何進行刪除操作 64 //這個時候找到左子樹最大的元素或右子樹最小的元素來填充這個位置,在這裏我選擇右子樹最小的 65 t.data=findMin(t.avlRight).data; 66 //同時我們獲得右子樹刪除右子樹上多余的值(原先的最小值) 67 t.avlRight=delete(t.data,t.avlRight); 68 }else{//當右子樹為空或左子樹為空,該如何進行刪除操作,簡單,左子樹為空,刪除操作直接將右子樹的節點替代根就完事 69 t=(t.avlLeft!=null)?t.avlLeft:t.avlRight; 70 } 71 return balance(t); 72 } 73 74 /* 75 * 用來查找二叉查找樹種最小的元素 76 */ 77 public AVLTreeNode findMin(AVLTreeNode t){ 78 //根據二叉查找樹的特點,樹的最小節點一定是在最左邊的子樹上,我們只需要不停的尋找它的左子樹即可 79 while(t.avlLeft!=null){ 80 t=t.avlLeft; 81 } 82 return t; 83 } 84 85 /* 86 * 用於平衡二叉查找樹的(AVL樹核心方法) 87 */ 88 public AVLTreeNode balance(AVLTreeNode t){ 89 if(t==null) 90 return t; 91 if(height(t.avlLeft)-height(t.avlRight)>ALLOWED_IMBALANCE){ 92 if(height(t.avlLeft.avlLeft)>height(t.avlLeft.avlRight)){//第一種情況 93 t=rotateWithLeftChild(t); 94 }else{//第三種情況 95 t=doubleWithLeftChild(t); 96 } 97 }else if(height(t.avlRight)-height(t.avlLeft)>ALLOWED_IMBALANCE){ 98 if(height(t.avlRight.avlLeft)<height(t.avlRight.avlRight)){//第二種情況 99 t=rotateWithRightChild(t); 100 }else{//第四種情況 101 t=doubleWithRightChild(t); 102 } 103 }else{ 104 ;//第三種就是已經平衡,不進行任何操作 105 } 106 t.height=Math.max(height(t.avlLeft), height(t.avlRight))+1; 107 return t; 108 } 109 110 /* 111 * 平衡操作需要的旋轉操作 112 */ 113 //與左孩子節點單旋轉(以根為基準) 114 public AVLTreeNode rotateWithLeftChild(AVLTreeNode k1){ 115 AVLTreeNode k2=k1.avlLeft; 116 k1.avlLeft=k2.avlRight; 117 k2.avlRight=k1; 118 k1.height=Math.max(height(k1.avlLeft), height(k1.avlRight))+1;//計算以這個節點為根的樹的高度 119 k2.height=Math.max(height(k2.avlLeft), k1.height)+1; 120 return k2; 121 } 122 123 //與右孩子節點單旋轉(以根為基準) 124 public AVLTreeNode rotateWithRightChild(AVLTreeNode k1){ 125 AVLTreeNode k2=k1.avlRight; 126 k1.avlRight=k2.avlLeft; 127 k2.avlLeft=k1; 128 k1.height=Math.max(height(k1.avlLeft),height(k1.avlRight))+1; 129 k2.height=Math.max(height(k2.avlRight), k1.height)+1; 130 return k2; 131 } 132 133 //雙旋轉,先根的左孩子和其右孩子旋轉,然後根最後與左孩子節點單旋轉 134 public AVLTreeNode doubleWithLeftChild(AVLTreeNode k1){ 135 //首先的思想是將雙旋轉轉換為之前習慣的單旋轉的情況, 136 k1.avlLeft=rotateWithRightChild(k1.avlLeft); 137 //然後通過調用一次與左孩子節點單旋轉 138 AVLTreeNode k2=rotateWithLeftChild(k1); 139 return k2; 140 } 141 142 //雙旋轉,先根的左孩子和其右孩子旋轉,然後根最後與左孩子節點單旋轉 143 public AVLTreeNode doubleWithRightChild(AVLTreeNode k1){ 144 //首先的思想是將雙旋轉轉換為之前習慣的單旋轉的情況, 145 k1.avlRight=rotateWithLeftChild(k1.avlRight); 146 //然後通過調用一次與左孩子節點單旋轉 147 AVLTreeNode k2=rotateWithRightChild(k1); 148 return k2; 149 } 150 151 152 /* 153 * 中序遍歷的方法(遞歸) 154 */ 155 public void midTravleTree(AVLTreeNode h){ 156 if(h!=null){ 157 midTravleTree(h.avlLeft); 158 System.out.print(h.data+" "); 159 midTravleTree(h.avlRight); 160 } 161 } 162 163 /* 164 * 測試 165 */ 166 167 //public void static 168 public static void main(String[] args) { 169 AVLTree avlTree=new AVLTree(); 170 AVLTreeNode t=null; 171 t=avlTree.insert(1, t); 172 System.out.println("t.height:"+t.height);//每次插完後後進行查一次樹的高度 173 t=avlTree.insert(2, t); 174 System.out.println("t.height:"+t.height);//每次插完後後進行查一次樹的高度 175 t=avlTree.insert(3, t); 176 System.out.println("t.height:"+t.height);//每次插完後後進行查一次樹的高度 177 t=avlTree.insert(4, t); 178 System.out.println("t.height:"+t.height);//每次插完後後進行查一次樹的高度 179 t=avlTree.insert(5, t); 180 System.out.println("t.height:"+t.height);//每次插完後後進行查一次樹的高度 181 t=avlTree.insert(6, t); 182 System.out.println("t.height:"+t.height);//每次插完後後進行查一次樹的高度 183 t=avlTree.insert(7, t); 184 System.out.println("t.height:"+t.height);//每次插完後後進行查一次樹的高度 185 t=avlTree.insert(8, t); 186 System.out.println("t.height:"+t.height);//每次插完後後進行查一次樹的高度 187 //中序遍歷一下avl樹 188 avlTree.midTravleTree(t); 189 System.out.println();//換行 190 t=avlTree.delete(4, t); 191 System.out.println("t.height:"+t.height);//每次刪除後後進行查一次樹的高度 192 t=avlTree.delete(6, t); 193 System.out.println("t.height:"+t.height);//每次刪除後後進行查一次樹的高度 194 //中序遍歷一下avl樹 195 avlTree.midTravleTree(t); 196 } 197 }
運行結果:
t.height:0 t.height:1 t.height:1 t.height:2 t.height:2 t.height:2 t.height:2 t.height:3 1 2 3 4 5 6 7 8 t.height:2 t.height:2 1 2 3 5 7 8
四、說一些自己感想
AVL樹的出現,一方面為排序提供了方便,另一方面也提高了樹結構的查詢效率,查詢的時間復雜度為O(logn)。
AVL樹(平衡二叉查找樹)