在上一個專題中,我們在談論二叉查找樹的效率的時候。不同結構的二叉查找樹,查找效率有很大的不同(單支樹結構的查找效率退化成了順序查找)。如何解決這個問題呢?關鍵在於如何最大限度的減小樹的深度。正是基於這個想法,平衡二叉樹出現了。
平衡二叉樹的定義 (AVL—— 發明者爲Adel'son-Vel'skii 和 Landis)
平衡二叉查找樹,又稱 AVL樹。 它除了具備二叉查找樹的基本特徵之外,還具有一個非常重要的特點:它 的左子樹和右子樹都是平衡二叉樹,且左子樹和右子樹的深度之差的絕對值(平衡因子 ) 不超過1。 也就是說AVL樹每個節點的平衡因子只可能是-1、0和1(左子樹高度減去右子樹高度)。
平衡二叉樹的性能優勢:
很顯然,平衡二叉樹的優勢在於不會出現普通二叉查找樹的最差情況。其查找的時間複雜度爲O(logN)。
那麼如何是二叉查找樹在添加數據的同時保持平衡呢?基本思想就是:當在二叉排序樹中插入一個節點時,首先檢查是否因插入而破壞了平衡,若 破壞,則找出其中的最小不平衡二叉樹,在保持二叉排序樹特性的情況下,調整最小不平衡子樹中節點之間的關係,以達 到新的平衡。所謂最小不平衡子樹 指離插入節點最近且以平衡因子的絕對值大於1的節點作爲根的子樹。
平衡二叉樹的操作
1. 查找操作
平衡二叉樹的查找基本與二叉查找樹相同。
2. 插入操作
在平衡二叉樹中插入結點與二叉查找樹最大的不同在於要隨時保證插入後整棵二叉樹是平衡的。那麼調整不平衡樹的基本方法就是: 旋轉 。 下面我們歸納一下平衡旋轉的4中情況
1) 繞某元素左旋轉
80 90
/ \ 左旋 / \
60 90 ---- -> 80 120
/ \ / \ /
85 120 60 85 100
/
100
a) BST樹 b ) AVL樹
分析一下:在插入數據100之前,a圖的B ST樹只有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+樹 》 將詳細分析。
上面提到的紅黑樹和多路查找樹都是屬於深度有界查找樹(depth-bounded tree —DBT)
平衡二叉樹的插入操作代碼(平衡旋轉)
- package net.hr.algorithm.search;
- /**平衡因子枚舉類*/
- enum B
- alanceFactor{
- 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的左孩子爲空,則插入到結點的做孩子並修改結點的平衡因子爲LH
- if(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 else
- return 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;
- }
- }//switch
- rd.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;
- }
- }//switch
- rd.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());
- }
- }
相關問題1:N層平衡二叉樹至少多少個結點
假設F(N)表示N層平衡二叉樹的結點個數,則F[1]=1,F[2]=2。而F(N)=F(N-2)+F(N-1)+1
爲什麼呢?我們可以這樣考慮,假設現在又一個(N-2)層和(N-1)層的最少結點平衡二叉樹。要構造一棵N層的平衡二叉樹,則只需加入一個根節點,其左右子樹分別(N-2)層和(N-1)層的樹即可 。由於兩個子樹都是最少結點的,所有N層的也是最少結點的。