前言:以下是轉自Heart.X.Raid的部落格:http://hxraid.iteye.com/blog/609949 。可能連結丟失,所以在此進行全文轉載,忘原作者諒解。
在上一個專題中,我們在談論二叉尋找樹的效率的時候。不同結構的二叉尋找樹,尋找效率有很大的不同(單支樹結構的尋找效率退化成了順序尋找)。如何解決這個問題呢?關鍵在於如何最大限度的減小樹的深度。正是基於這個想法,平衡二叉樹出現了。
平衡二叉樹的定義
(AVL—— 發明者為Adel'son-Vel'skii 和 Landis)
平衡二叉尋找樹,又稱AVL樹。它除了具備二叉尋找樹的基本特徵之外,還具有一個非常重要的特點:它的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的深度之差的絕對值(平衡因子)不超過1。也就是說AVL樹每個節點的平衡因子只可能是-1、0和1(左子樹高度減去右子樹高度)。
那麼如何是二叉尋找樹在添加資料的同時保持平衡呢?基本思想就是:當在二叉排序樹中插入一個節點時,首先檢查是否因插入而破壞了平衡,若破壞,則找出其中的最小不平衡二叉樹,在保持二叉排序樹特性的情況下,調整最小不平衡子樹中節點之間的關係,以達到新的平衡。所謂最小不平衡子樹指離插入節點最近且以平衡因子的絕對值大於1的節點作為根的子樹。
平衡二叉樹的操作
1. 尋找操作
平衡二叉樹的尋找基本與二叉尋找樹相同。
2. 插入操作
在平衡二叉樹中插入結點與二叉尋找樹最大的不同在於要隨時保證插入後整棵二叉樹是平衡的。那麼調整不平衡樹的基本方法就是:旋轉。下面我們歸納一下平衡旋轉的4中情況
1) 繞某元素左旋轉
80 90
/ \
左旋 / \
60 90 -----> 80 120
/ \ / \ /
85 120 60 85 100
/
100
a) BST樹 b ) AVL樹
分析一下:在插入資料100之前,a圖的BST樹只有80節點的平衡因子是-1(左高-右高),但整棵樹還是平衡的。加入100之後,80節點的平衡因子就成為了-2,此時平衡被破壞。需要左旋轉成b圖。
當樹中節點X的右孩子的右孩子上插入新元素,且平衡因子從-1變成-2後,就需要繞節點X進行左旋轉。
2) 繞某元素右旋轉
100 85
/ \ 右旋 / \
85 120
-------> 60 100
/ \ \ / \
60 90 80 90 120
\
80
a) B ST樹 b) AVL樹
當樹中節點X的左孩子的左孩子上插入新元素,且平衡因子從1變成2後,就需要繞節點X進行右旋轉。
3) 繞某元素的左子節點左旋轉,接著再繞該元素自己右旋轉。此情況下就是左旋與右旋 的結合,具體操作時可以分解成這兩種操作,只是圍繞點不一樣而已。
100 100 90
/ \
左旋 / \ 右旋 / \
80 120 ------> 90 120 ------> 80 100
/ \ / / \ \
60 90 80 60 85 120
/ / \
85 60 85
當樹中節點X的左孩子的右孩子上插入新元素,且平衡因子從1變成2後,就需要先繞X的左子節點Y左旋轉,接著再繞X右旋轉
4) 繞某元素的右子節點右旋轉,接著再繞該元素自己左旋轉。此情況下就是 右旋與左旋 的結合,具體操作時可以分解成這兩種操作,只是圍繞點不一樣而已。
80 80 85
/ \
右旋 / \
左旋 / \
60 100 ------> 60 85 -------> 80 100
/ \ \ / / \
85 120 100 60 90 120
\ / \
90 90 120
當樹中節點X的右孩子的左孩子上插入新元素,且平衡因子從-1變成-2後,就需要先繞X的右子節點Y右旋轉,接著再繞X左旋轉
平衡二叉樹效能分析
平衡二叉樹的效能優勢:
很顯然,平衡二叉樹的優勢在於不會出現普通二叉尋找樹的最差情況。其尋找的時間複雜度為O(logN)。
平衡二叉樹的缺陷:
(1) 很遺憾的是,為了保證高度平衡,動態插入和刪除的代價也隨之增加。因此,我們在下一專題中講講《紅/黑樹狀結構》這種更加高效的尋找結構。
(2) 所有二叉尋找樹結構的尋找代價都與樹高是緊密相關的,能否通過減少樹高來進一步降低尋找代價呢。我們可以通過多路尋找樹的結構來做到這一點,在後面專題中我們將通過《多路尋找樹/B-樹/B+樹》來介紹。
(3) 在大資料量尋找環境下(比如說系統磁碟裡的檔案目錄,資料庫中的記錄查詢等),所有的二叉尋找樹結構(BST、AVL、RBT)都不合適。如此大規模的資料量(幾G資料),全部組織成平衡二叉樹放在記憶體中是不可能做到的。那麼把這棵樹放在磁碟中吧。問題就來了:假如構造的平衡二叉樹深度有1W層。那麼從根節點出發到葉子節點很可能就需要1W次的硬碟IO讀寫。大家都知道,硬碟的機械組件讀寫資料的速度遠遠趕不上純電子媒體的記憶體。尋找效率在IO讀寫過程中將會付出巨大的代價。在大規模資料查詢這樣一個實際應用背景下,平衡二叉樹的效率就很成問題了。對這一問題的解決:我們也會在《多路尋找樹/B-樹/B+樹》
將詳細分析。
package net.hr.algorithm.search;/**平衡因子枚舉類*/enum BalanceFactor{LH("左子樹高"),EH("左右等高"),RH("右子樹高");private String illustration="";private BalanceFactor(String s){this.illustration=s;}public String toString(){return this.illustration;}}/** * 平衡二叉樹結點 */class AVLNode<E extends Comparable<E>>{/**結點關鍵字*/E key=null;/**結點的平衡因子*/BalanceFactor bFactor=BalanceFactor.EH;/**結點的直接父親*/AVLNode<E> parent=null;/**結點的左右孩子*/AVLNode<E> lchild,rchild=null;AVLNode(E k){this.key=k;}/** * 格式輸出結點 */public String toString(){//String fomateStr="";//if(this.lchild==null)String lchildStr=(this.lchild==null)?"null":this.lchild.key.toString();String rchildStr=(this.rchild==null)?"null":this.rchild.key.toString();return this.key+"[lchild="+lchildStr+",rchild="+rchildStr+"]";}}/** * 平衡二叉尋找樹 * @author heartraid */public class AVL<E extends Comparable<E>> {/**樹根*/private AVLNode<E> root=null;/**當前樹是否變高*/public boolean isTaller=false;public AVL(){}public boolean insert(E key){System.out.print("插入["+key+"]:");if(key==null) return false;if(root==null){System.out.println("插入到樹根。");root=new AVLNode<E>(key);return true;}else{System.out.print("搜尋路徑[");return insertAVL(key,root);}}private boolean insertAVL(E key,AVLNode<E> node){System.out.print(node.key+" —>");// 樹中存在相同的key,不需要插入if(node.key.compareTo(key)==0){System.out.println("]. 搜尋有相同關鍵字,插入失敗");isTaller=false;return false;}else{//左子樹搜尋if(node.key.compareTo(key)>0){//當前node的左孩子為空白,則插入到結點的做孩子並修改結點的平衡因子為LHif(node.lchild==null){System.out.println("]. 插入到"+node.key+"的左孩子");AVLNode<E> newNode=new AVLNode<E>(key);node.lchild=newNode; //設定左孩子結點newNode.parent=node; //設定父親結點isTaller=true; //樹長高了}//左孩子不為空白,則繼續搜尋下去else{insertAVL(key,node.lchild);}//當前如果樹長高了,說明是因為左孩子的添加改變了平衡因子(左高)。if(isTaller){System.out.print(" 樹變化了,"+node.key+"的平衡因子變化");switch(node.bFactor){ //原來結點平衡因子是LH(bf=1),則左高以後bf=2,因此需要做左平衡旋轉case LH: {System.out.println("[LH=1 ——> LH=2]. 出現了不平衡現象[左比右高2]");System.out.println(" ★ 以"+node.key+"為根將樹進行左平衡處理");leftBalance(node);isTaller=false; break;}//原來結點平衡因子是EH(bf=0),則左高了以後bf=1,不需要平衡處理。case EH:{System.out.println("[EH=0 ——> LH=1]. 沒有不平衡現象");node.bFactor=BalanceFactor.LH;isTaller=true;break;}//原來結點平衡因子是RH(bf=-1),則左高以後bf=0,不需要平衡處理。case RH:{System.out.println("[RH=-1 ——> EH=0]. 沒有不平衡現象");node.bFactor=BalanceFactor.EH;isTaller=false;break;}}//end switch}//end if}//end if//右子樹搜尋else{if(node.rchild==null){System.out.println("]. 插入到"+node.key+"的右孩子");AVLNode<E> newNode=new AVLNode<E>(key);node.rchild=newNode; //設定右孩子結點newNode.parent=node; //設定父親結點isTaller=true; //樹長高了}else{insertAVL(key,node.rchild);}//當前如果樹長高了,說明是因為右孩子的添加改變了平衡因子(右高)。if(isTaller){System.out.print(" 樹變化了,"+node.key+"的平衡因子變化");switch(node.bFactor){ //原來結點平衡因子是LH(bf=1),則右高以後bf=0,不需要平衡處理。case LH: {System.out.println("[LH=1 ——> EH=0]. 沒有不平衡現象");node.bFactor=BalanceFactor.EH;isTaller=false;break;}//原來結點平衡因子是EH(bf=0),則右高了以後bf=-1,不需要平衡處理。case EH:{System.out.println("[EH=0 ——> RH=-1]. 沒有不平衡現象");node.bFactor=BalanceFactor.RH;isTaller=true;break;}//原來結點平衡因子是RH(bf=-1),則右高以後bf=0,因此需要做右平衡旋轉。case RH:{System.out.println("[RH=-1 ——> RH=-2]. 出現了不平衡現象[左比右矮2]");rightBalance(node);isTaller=false; break;}}//end switch}//end if(isTaller)}//end elsereturn true;}//end else}/** * 左平衡旋轉處理 * 先對node的左子樹進行單左旋處理,在對node樹進行單右旋處理 * * 100 100 90 * / \ 左旋 / \ 右旋 / \ * 80 120 ------> 90 120 ------> 80 100 * / \ / / \ \ * 60 90 80 60 85 120 * / / \ * 85 60 85 * * @param node 需要做處理的子樹的根結點 */private void leftBalance(AVLNode<E> node){// node.parent指向新的孩子結點AVLNode<E> lc=node.lchild;//lc指向node的左孩子結點switch(lc.bFactor){case LH:{ //新結點插入在node的左孩子的左子樹上,則需要單右旋處理System.out.println(" ┖ 對"+node.key+"進行單右旋轉處理");node.bFactor=lc.bFactor=BalanceFactor.EH;rRotate(node);break;}case RH:{ //新結點插入在node的左孩子的右子樹上,需要雙旋處理System.out.println(" ┖ 對"+node.key+"的左子樹進行單左旋轉處理,再對其本身樹進行單右迴圈處理");AVLNode<E> rd=lc.rchild; //rd指向node左孩子的右子樹根switch(rd.bFactor){ //修改node與其左孩子的平衡因子case LH:{node.bFactor=BalanceFactor.RH;lc.bFactor=BalanceFactor.EH;break;}case EH:{node.bFactor=lc.bFactor=BalanceFactor.EH;break;}case RH:{node.bFactor=BalanceFactor.EH;lc.bFactor=BalanceFactor.LH;break;}}//switchrd.bFactor=BalanceFactor.EH;lRotate(node.lchild);rRotate(node);break;}}}/** * 右平衡旋轉處理 * * 80 80 85 * / \ 右 旋 / \ 左 旋 / \ * 60 100 ------> 60 85 -------> 80 100 * / \ \ / / \ * 85 120 100 60 90 120 * \ / \ * 90 90 120 * * @param node */private void rightBalance(AVLNode<E> node){AVLNode<E> lc=node.rchild;//lc指向node的右孩子結點switch(lc.bFactor){case RH:{ //新結點插入在node的右孩子的右子樹上,則需要單左旋處理node.bFactor=lc.bFactor=BalanceFactor.EH;lRotate(node);break;}case LH:{ //新結點插入在node的右孩子的左子樹上,需要雙旋處理AVLNode<E> rd=lc.lchild; //rd指向node右孩子的左子樹根switch(rd.bFactor){ //修改node與其右孩子的平衡因子case LH:{node.bFactor=BalanceFactor.EH;lc.bFactor=BalanceFactor.RH;break;}case EH:{node.bFactor=lc.bFactor=BalanceFactor.EH;break;}case RH:{node.bFactor=BalanceFactor.LH;lc.bFactor=BalanceFactor.EH;break;}}//switchrd.bFactor=BalanceFactor.EH;rRotate(node.rchild);lRotate(node);break;}}}/** * 對以node為根的子樹進行單右旋處理,處理後node.parent指向新的樹根,即旋轉之前 * node的左孩子結點 * 100<-node.parent 80<-node.parent * / / \ * 80 ———> 60 100 * / \ / * 60 85 85 */private void rRotate(AVLNode<E> node){AVLNode<E> lc=node.lchild;//lc指向node的左孩子結點node.lchild=lc.rchild;lc.rchild=node;if(node.parent==null){root=lc;}else if(node.parent.lchild.key.compareTo(node.key)==0)node.parent.lchild=lc;else node.parent.rchild=lc;}/** * 對以node為根的子樹進行單左旋處理,處理後node.parent指向新的樹根,即旋轉之前 * node的右孩子結點 * 100<-node.parent 110<-node.parent * \ / \ * 110 ————> 100 120 * / \ \ * 105 120 105 */private void lRotate(AVLNode<E> node){AVLNode<E> rc=node.rchild;//lc指向node的右孩子結點node.rchild=rc.lchild;rc.lchild=node;if(node.parent==null){root=rc;}else if(node.parent.lchild.key.compareTo(node.key)==0) node.parent.lchild=rc;else node.parent.rchild=rc;}/** * 得到BST根節點 * @return BST根節點f */ public AVLNode<E> getRoot(){ return this.root; } /** * 遞迴前序走訪樹 */public void preOrderTraverse(AVLNode<E> node){if(node!=null){System.out.println(node);preOrderTraverse(node.lchild);preOrderTraverse(node.rchild);}}/** * 測試 * @param args */public static void main(String[] args) {AVL<Integer> avl=new AVL<Integer>();avl.insert(new Integer(80));avl.insert(new Integer(60));avl.insert(new Integer(90));avl.insert(new Integer(85));avl.insert(new Integer(120));avl.insert(new Integer(100));System.out.println("前序走訪AVL:");avl.preOrderTraverse(avl.getRoot());}}