數據結構與算法(二):基於數組的實現ArrayList源碼完全分析數組
數據結構與算法(三):基於鏈表的實現LinkedList源碼完全分析數據結構
數據結構與算法(四):基於哈希表實現HashMap核心源碼完全分析ide
數據結構與算法(五):LinkedHashMap核心源碼完全分析性能
數據結構與算法(七):赫夫曼樹this
在上一篇中咱們提到過二叉排序樹構造可能出現的性能問題,好比咱們將數據:2,4,6,8構造一顆二叉排序樹,構造出來以下: 設計
這確定不是咱們所但願構造出來的,由於這樣一棵樹查找的時候效率是及其低下的,說白了就至關於數組同樣挨個遍歷比較。3d
那咱們該怎麼解決這個問題呢?這時候就須要咱們學習一下二叉平衡樹的概念了,本系列設計的二叉平衡樹主要包含AVL樹以及紅黑樹,本篇主要講解AVL樹。
下面咱們瞭解一下AVL樹。
一棵AVL樹是其每一個結點的平衡因子絕對值最多相差1的二叉查找樹。
平衡因子?這是什麼鳥,別急,繼續向下看。
平衡因子就是二叉排序樹中每一個結點的左子樹和右子樹的高度差。
這裏須要注意有的博客或者書籍會將平衡因子定義爲右子樹與左子樹的高度差,本篇咱們定義爲左子樹與右子樹的高度差,不要搞混。
好比下圖中:
根結點45的平衡因子爲-1 (左子樹高度2,右子樹高度3)
50結點的平衡因子爲-2 (左子樹高度0,右子樹高度2)
40結點的平衡因子爲0 (左子樹高度1,右子樹高度1)
根據定義這顆二叉排序樹中有結點的平衡因子超過1,因此不是一顆AVL樹。
因此AVL樹能夠表述以下:一棵AVL樹是其每一個結點的左右子樹高度差絕對值最多相差1的二叉查找樹。
最小不平衡二叉樹定義爲:距離插入結點最近,且平衡因子絕對值大於2的結點爲根結點的子樹,稱爲最小不平衡二叉排序樹。
好比下圖:
在插入80結點以前是一顆標準的AVL樹,在插入80結點以後就不是了,咱們查找一下最小不平衡二叉排序樹,從距離80結點最近的結點開始,67結點平衡因子爲-1,50結點平衡因子爲-2,到這裏就找到了,因此以50爲根結點的子樹就是最小不平衡二叉排序樹。
明白了以上概念後咱們就須要再瞭解一下左旋與右旋的概念了,這裏左旋右旋對於剛接觸的同窗來講有點難度,可是對於理解AVL樹,紅黑樹是必須掌握的概念,十分重要,不要怕,跟着個人思路我就不信講不明白。
左旋與右旋就是爲了解決不平衡問題而產生的,咱們構建一顆AVL樹的過程會出現結點平衡因子絕對值大於1的狀況,這時就能夠經過左旋或者右旋操做來達到平衡的目的。
接下來咱們瞭解一下左旋右旋的具體操做。
上圖就是一個標準的X結點的左旋流程。
在第一步圖示僅僅將X結點進行左旋,成爲Y結點的一個子節點。
可是此時出現一個問題,就是Y結點有了三個子節點,這連最基礎的二叉樹都不是了,因此須要進行第二部操做。
在第二部操做的時候,咱們將B結點設置爲X結點的右孩子,這裏能夠仔細想一下,B結點一開始爲X結點的右子樹Y的左孩子,那麼其確定比X結點大,比Y結點小,因此這裏設置爲X結點的右孩子是沒有問題的。
上圖中Y結點有左子樹B,若是沒有左子樹B,那麼第二部也就不須要操做了,這裏很容易理解,都沒有還操做什麼鬼。
到這裏一個標準的左旋流程就完成了。
在構建AVL樹的過程當中咱們到底怎麼使用左旋操做呢?這裏咱們先舉一個例子,以下圖:
在上圖中咱們插入結點5的時候就出現不平衡了,3結點的平衡因子爲-2,這時候咱們能夠將結點3進行左旋,如右圖,這樣就從新達到平衡狀態了。
1 /** 2 * 左旋操做 3 * @param t 4 */ 5 private void left_rotate(AVL<E>.Node<E> t) { 6 if (t != null) { 7 Node tr = t.right; 8 //將t結點的右孩子的左結點設置爲t結點的右孩子 9 t.right = tr.left; 10 if (tr.left != null) { 11 //重置其父節點 12 tr.left.parent = t; 13 } 14 //t結點旋轉下來,其右孩子至關於替換t結點的位置 15 //因此這裏一樣須要調整其右孩子的父節點爲t結點的父節點 16 tr.parent = t.parent; 17 //整棵樹只有根結點沒有父節點,這裏檢測咱們旋轉的是否爲根結點 18 //若是是則須要重置root結點 19 if (t.parent == null) { 20 root = tr; 21 } else { 22 //若是t結點位於其父節點的左子樹,則旋轉上去的右結點則 23 //位於父節點的左子樹,反之同樣 24 if (t.parent.left == t) { 25 t.parent.left = tr; 26 } else if (t.parent.right == t) { 27 t.parent.right = tr; 28 } 29 } 30 //將t結點設置爲其右子樹的左結點 31 tr.left = t; 32 //重置t結點的父節點 33 t.parent = tr; 34 } 35 }
代碼基本上都加上了備註,對比左旋流程仔細分析一下,這裏須要注意一下,旋轉完後結點的父節點都須要重置。
好了,對於左旋操做,相信你已經有必定了解了,若是還有不明白的地方能夠本身仔細想一下,實在想不明白能夠關注我公衆號聯繫本人單獨交流。
接下來咱們看看右旋是怎麼回事。
上圖就是對Y結點進行右旋操做的流程,有了左旋操做的基礎這裏應該很好理解了。
第一步一樣僅僅將Y結點右旋,成爲X的一個結點,一樣這裏會出現問題X有了三個結點。
第二步,若是一開始Y左子樹存在右結點,上圖中也就是B結點,則將其設置爲Y的右孩子。
到這裏一個標準的右旋流程就完成了。
咱們看一個右旋的例子,如圖:
在咱們插入結點1的時候就會出現不平衡現象,結點5的平衡因子變爲2,這裏咱們將結點5進行右旋,變爲右圖就又變爲一顆AVL樹了。
1 /** 2 * 右旋操做 3 * @param t 4 */ 5 private void right_rotate(AVL<E>.Node<E> t) { 6 if (t != null) { 7 Node<E> tl = t.left; 8 t.left =tl.right; 9 if (tl.right != null) { 10 tl.right.parent = t; 11 } 12 13 tl.parent = t.parent; 14 if (t.parent == null) { 15 root = tl; 16 } else { 17 if (t.parent.left == t) { 18 t.parent.left = tl; 19 } else if (t.parent.right == t) { 20 t.parent.right = tl; 21 } 22 } 23 tl.right = t; 24 t.parent = tl; 25 } 26 }
對於右旋操做代碼實現,沒有加任何註釋,但願你本身沉下心來逐行分析一下,有了左旋代碼基礎,這裏並不難。
好了,以上就是左旋與右旋的操做,這部分必定要搞明白,AVL樹與紅黑樹的構建過程出現不平衡狀況主要經過左旋與右旋來使其從新達到平衡狀態。
上面咱們瞭解了左旋與右旋的概念,也經過具體案例明白到底怎麼經過左旋或者右旋來使二叉排序樹從新達到AVL樹的要求,可是這裏要明白有些狀況並非僅僅靠一次左旋或者右旋就能實現平衡的目的,這是就須要左旋右旋一塊兒使用來使其達到平衡的目的。
那麼到底怎麼區分是使用左旋或者右旋或者左旋右旋一塊兒使用才能使樹從新達到平衡呢?
這裏咱們就須要仔細分狀況來處理了,咱們在構建AVL樹插入某一個元素候若是出現不平衡現象確定是左子樹或者右子樹出現了不平衡現象,這裏有點繞,不過也很好理解,某一結點平衡因子絕對值超過1了,確定是左子樹太高或者右子樹太高產生的,這裏,咱們採用分治的思想來解決,分治思想是算法思想的一種,就是把一個複雜的問題分紅兩個或更多的相同或類似的子問題,直到最後子問題能夠簡單的直接求解,原問題的解即子問題的解的合併。
這裏咱們怎麼使用分治的思想呢?首先出現不平衡只有兩種可能,某一結左子樹或者右子樹太高致使的,咱們能夠先考慮左子樹太高該怎麼處理,而後考慮右子樹太高怎麼處理,固然這裏只是粗略的分爲兩大解決問題的方向,往下還會繼續分析不一樣狀況,接下來咱們將會仔細分析。
左平衡操做,即結點t的不平衡是由於左子樹過深形成的,這時咱們須要對t左子樹分狀況進行解決。
左平衡操做狀況分類
一、若是新的結點插入後t的左孩子的平衡因子爲1,也就是插入到t左孩子的左側,則直接對結點t進行右旋操做便可
二、若是新的結點插入後t的左孩子的平衡因子爲-1,也就是插入到t左孩子的右側,則須要進行分狀況討論
狀況a:當t的左孩子的右子樹根節點的平衡因子爲-1,這時須要進行兩步操做,先以tl進行左旋,在以t進行右旋。
通過上述過程,最終又達到了平衡狀態。
狀況b:當p的左孩子的右子樹根節點的平衡因子爲1,這時須要進行兩步操做,先以tl進行左旋,在以t進行右旋。
狀況c:當p的左孩子的右子樹根節點的平衡因子爲0,這時須要進行兩步操做,先以tl進行左旋,在以t進行右旋。
到這裏細心的同窗確定有一個疑問,狀況a,b,c不都是先以tl左旋,再以t右旋嗎?爲何還要拆分出來?
首先觀察a,b,c三種狀況,旋轉以前是葉子結點的,在兩次旋轉以後依然是葉子結點,也就是說其平衡因子旋轉先後無變化,均是0。
可是再觀察一下t,tl,tlr這三個節點旋轉先後的平衡因子,不一樣狀況下先後是不同的,因此這裏須要區分一下,具體旋轉後t,tl,tlr的平衡因子以下:
狀況a:
t.balance = 0;
tlr.balance = 0;
tl.balance = 1;
狀況b:
t.balance = -1;
tl.balance =0;
tlr.balance = 0;
狀況c:
t.balance = 0;
tl.balance = 0;
tlr.balance = 0;
以上就是左平衡操做的全部狀況,接下來看下左平衡具體代碼:
1 /** 2 * 左平衡操做 3 * @param t 4 */ 5 private void leftBalance(AVL<E>.Node<E> t) { 6 Node<E> tl = t.left; 7 switch (tl.balance) { 8 case LH: 9 right_rotate(t); 10 tl.balance = EH; 11 t.balance = EH; 12 break; 13 case RH: 14 Node<E> tlr = tl.right; 15 switch (tlr.balance) { 16 case RH: 17 t.balance = EH; 18 tlr.balance = EH; 19 tl.balance = LH; 20 break; 21 case LH: 22 t.balance = RH; 23 tl.balance =EH; 24 tlr.balance = EH; 25 break; 26 case EH: 27 t.balance = EH; 28 tl.balance = EH; 29 tlr.balance =EH; 30 break; 31 //統一旋轉 32 default: 33 break; 34 } 35 //統一先以tl左旋,在以t右旋 36 left_rotate(t.left); 37 right_rotate(t); 38 break; 39 default: 40 break; 41 } 42 }
好了,左平衡操做全部狀況講解以及具體代碼實現,主要就是分治思想,加以細分而後逐個狀況逐個解決的套路。
右平衡操做,即結點t的不平衡是由於右子樹過深形成的,這時咱們須要對t右子樹分狀況進行解決。
右平衡操做狀況分類
一、若是新的結點插入後t的右孩子的平衡因子爲1,也就是插入到t左孩子的右側,則直接對結點t進行左旋操做便可
二、若是新的結點插入後t的右孩子的平衡因子爲-1,也就是插入到t右孩子的左側,則須要進行分狀況討論
狀況a:當t的右孩子的左子樹根節點的平衡因子爲1,這時須要進行兩步操做,先以tr進行右旋,在以t進行左旋。
狀況b:當p的右孩子的左子樹根節點的平衡因子爲-1,這時須要進行兩步操做,先以tr進行右旋,在以t進行左旋。
狀況c:當p的右孩子的左子樹根節點的平衡因子爲0,這時須要進行兩步操做,先以tr進行右旋,在以t進行左旋。
一樣,a,b,c三種狀況旋轉先後葉子結點依然是葉子結點,變化的
只是t,tr,trl結點的平衡因子,而且三種狀況trl最後平衡因子均爲0.
右平衡代碼實現:
1 /** 2 * 右平衡操做 3 * @param t 4 */ 5 private void rightBalance(AVL<E>.Node<E> t) { 6 Node<E> tr = t.right; 7 switch (tr.balance) { 8 case RH: 9 left_rotate(t); 10 t.balance = EH; 11 tr.balance = EH; 12 break; 13 case LH: 14 Node<E> trl = tr.left; 15 switch (trl.balance) { 16 case LH: 17 t.balance = EH; 18 tr.balance = RH; 19 break; 20 case RH: 21 t.balance = LH; 22 tr.balance = EH; 23 break; 24 case EH: 25 t.balance = EH; 26 tr.balance = EH; 27 break; 28 29 } 30 trl.balance = EH; 31 right_rotate(t.right); 32 left_rotate(t); 33 break; 34 default: 35 break; 36 } 37 }
到此,左平衡與右平衡操做也就講解完了,主要思想是採用的分治思想,大問題化爲小問題,而後逐個解決,到這裏,若是能所有理解,那麼AVL樹的最核心部分就徹底理解了,對於紅黑樹來講上面也是很核心的部分。
這部分咱們主要了解下怎麼建立AVL樹,也就是添加元素方法的總體邏輯。
先看下每一個結點類所包含的信息:
1public class Node<E extends Comparable<E>>{ 2 E element; // data 3 int balance = 0; // 每一個結點的平衡因子 4 Node<E> left; 5 Node<E> right; 6 Node<E> parent; 7 public Node(E element, Node<E> parent) { 8 this.element = element; 9 this.parent = parent; 10 } 11 12 @Override 13 public String toString() { 14 // TODO Auto-generated method stub 15 return element + "BF: " + balance; 16 } 17 18 public E getElement() { 19 return element; 20 } 21 22 public void setElement(E element) { 23 this.element = element; 24 } 25 26 public int getBalance() { 27 return balance; 28 } 29 30 public void setBalance(int balance) { 31 this.balance = balance; 32 } 33 34 public Node<E> getLeft() { 35 return left; 36 } 37 38 public void setLeft(Node<E> left) { 39 this.left = left; 40 } 41 42 public Node<E> getRight() { 43 return right; 44 } 45 46 public void setRight(Node<E> right) { 47 this.right = right; 48 } 49 50 public Node<E> getParent() { 51 return parent; 52 } 53 54 public void setParent(Node<E> parent) { 55 this.parent = parent; 56 } 57 }
最主要的是每一個結點類添加了一個balance屬性,也就是記錄本身的平衡因子,在插入元素的時候須要動態的調整。
咱們看下插入元素方法的Java實現:
1 /** 2 * 添加元素方法 3 * @param 4 */ 5 public boolean addElement(E element) { 6 Node<E> t = root; 7 //t檢查root是否爲空,若是爲空則表示AVL樹尚未建立, 8 //則須要建立根結點便可 9 if (t == null) { 10 root = new Node<E>(element, null); 11 size = 1; 12 root.balance = 0; 13 return true; 14 } else { 15 int cmp = 0; 16 Node<E> parent; 17 Comparable<? super E> e = (Comparable<? super E>)element; 18 //查找父類的過程,邏輯和講解二叉排序樹時查找父類是同樣的 19 do { 20 parent = t; 21 cmp = e.compareTo(t.element); 22 if (cmp < 0) { 23 t= t.left; 24 } else if (cmp > 0) { 25 t= t.right; 26 } else { 27 return false; 28 } 29 } while (t != null); 30 //建立結點,並掛載到父節點上 31 Node<E> child = new Node<E>(element, parent); 32 if (cmp < 0) { 33 parent.left = child; 34 } else { 35 parent.right = child; 36 } 37 //節點已經插入, 38 // 插入元素後 檢查平衡性,回溯查找 39 while (parent != null) { 40 cmp = e.compareTo(parent.element); 41 //元素在左邊插入 42 if (cmp < 0) { 43 parent.balance++; 44 } else{ //元素在右邊插入 45 parent.balance --; 46 } 47 //插入以後父節點balance正好徹底平衡,則不會出現平衡問題 48 if (parent.balance == 0) { 49 break; 50 } 51 //查找最小不平衡二叉樹 52 if (Math.abs(parent.balance) == 2) { 53 //出現平衡問題 54 fix(parent); 55 break; 56 } else { 57 parent = parent.parent; 58 } 59 } 60 size++; 61 return true; 62 } 63 }
其大致流程主要分爲兩大部分,前半部分和二叉排序樹插入元素的邏輯同樣,主要是查找父節點,將其掛載到父節點上,然後半部分就是AVL樹特有的了,也就是查找最小不平衡二叉樹而後對其修復,修復也就是經過左旋右旋操做使其達到平衡狀態,咱們看下fix方法主要邏輯:
1 /** 2 * 發現最小不平衡樹,對其進行修復 3 * @param parent 4 */ 5 private void fix(AVL<E>.Node<E> parent) { 6 if (parent.balance == 2) { 7 leftBalance(parent); 8 } 9 if (parent.balance == -2) { 10 rightBalance(parent); 11 } 12 }
很簡單,就是判斷左邊與右邊哪邊不平衡,進而進行左平衡或者右平衡操做,至於左平衡右平衡上面已經詳細講解過,不在過多說明。
好了,以上就是構建一顆AVL樹的過程講解,若是有不懂得地方能夠靜下心來本身好好分析一下。
本篇主要講解了AVL的概念以及經過最基礎的左旋,右旋使其保持樹中每個結點的平衡因子值保證在「-1,0,1」中,這樣構建出來的樹具備很好的查找特性。
AVL樹相對於紅黑樹來講是一顆嚴格的平衡二叉樹,平衡條件很是嚴格(樹高差只有1),只要插入或刪除不知足上面的條件就要經過旋轉來保持平衡。因爲旋轉是很是耗費時間的。AVL樹適合用於插入刪除次數比較少,但查找多的狀況。
在平衡二叉樹中應用比較多的是紅黑樹,紅黑樹對高度差要求沒有AVL那麼嚴格,用以保持平衡的左旋右旋操做次數比較少,用於搜索時,插入刪除次數多的狀況下一般用紅黑樹來取代AVL樹。TreeMap的實現以及JDK1.8之後的HashMap中都有紅黑樹的具體應用。
下一篇我可能先寫圖的概念以及圖一些經典算法,放心紅黑樹我確定會寫的,關於AVL樹與紅黑樹的差別在寫完紅黑樹在作詳細比較,以上簡單提一下。
好了,本篇就到此爲止了,但願對你有用。