Java數(shù)據(jù)結(jié)構(gòu)之平衡二叉樹的原理與實(shí)現(xiàn)
平衡二叉樹(AVL樹),顧名思義,是一顆很“平衡”的樹,它的平衡是相對(duì)于排序二叉樹來說的。為了避免極端情況下二叉搜索樹節(jié)點(diǎn)分布不均勻,甚至退化為鏈表,影響查找效率,我們引入了平衡二叉樹,即讓樹的結(jié)構(gòu)看起來盡量“均勻”,左右子樹的節(jié)點(diǎn)數(shù)和層級(jí)盡量一樣多。
本文詳細(xì)介紹了平衡二叉樹的概念和實(shí)現(xiàn)原理,并且提供了Java代碼的完全實(shí)現(xiàn)。
1 平衡二叉樹的概述
為了避免極端情況下二叉搜索樹退化為鏈表,影響查找效率,我們引入了平衡二叉樹,即讓樹的結(jié)構(gòu)看起來盡量“均勻”,左右子樹的節(jié)點(diǎn)數(shù)和層級(jí)盡量一樣多。要想學(xué)習(xí)平衡二叉樹并且掌握它,必須要先掌握二叉排序樹,如果對(duì)二叉搜索樹還不太明白的,包括為什么二叉排序樹可能退化為鏈表,可以看看這篇文章:數(shù)據(jù)結(jié)構(gòu)—二叉排序樹的原理以及Java代碼的完全實(shí)現(xiàn)。
平衡二叉樹,又稱AVL樹,指的是左子樹上的所有節(jié)點(diǎn)的值都比根節(jié)點(diǎn)的值小,而右子樹上的所有節(jié)點(diǎn)的值都比根節(jié)點(diǎn)的值大,且左子樹與右子樹的高度差最大為1。因此,平衡二叉樹滿足所有二叉排序(搜索)樹的性質(zhì),是在二叉排序樹的基礎(chǔ)上發(fā)展而來的。至于AVL,則是取自兩個(gè)發(fā)明平衡二叉樹的俄羅斯科學(xué)家的名字:G. M. Adelson-Velsky和E. M. Landis。
總的來說平衡二叉樹具有如下性質(zhì):
- 它一定是一棵二叉排序樹;
- 它是一棵空樹或它的左右兩個(gè)子樹的高度差的絕對(duì)值不超過1,并且左右兩個(gè)子樹都是一棵平衡二叉樹,遞歸定義。
平衡因子BF(Balance Factor): 我們將二叉樹上節(jié)點(diǎn)的左子樹深度減去右子樹深度的值稱為平衡因子,那么平衡二叉樹上所有節(jié)點(diǎn)的平衡因子只可能是-1、0和1。只要二叉樹上有一個(gè)節(jié)點(diǎn)的平衡因子的絕對(duì)值大于1,則該二叉樹就是不平衡的。

上圖中,圖一是平衡二叉樹,圖二的59比58大,卻是58的左子樹,這是不符合二叉排序樹的定義的,圖二不是平衡二叉樹。圖3不是平衡二叉樹的原因就在于,節(jié)點(diǎn)58的左子樹高度為3,而右子樹為空,二者差大于了絕對(duì)值1,因此它也不是平衡的。而經(jīng)過適當(dāng)?shù)恼{(diào)整后的圖4,它就符合了定義,因此它是平衡二叉樹。
最小不平衡子樹: 距離插入、刪除節(jié)點(diǎn)最近的,且平衡因子的絕對(duì)值大于1的節(jié)點(diǎn)為根的子樹,我們稱為最小不平衡子樹。下圖中當(dāng)新插入節(jié)點(diǎn)37時(shí),距離它最近的平衡因子絕對(duì)值超過1的節(jié)點(diǎn)是58(即它的左子樹高度3減去右子樹高度1),所以從58開始以下的子樹為最小不平衡子樹。

2 平衡二叉樹的實(shí)現(xiàn)原理
平衡二叉樹實(shí)現(xiàn)原理的核心就是:由于在插入、刪除節(jié)點(diǎn)以后,只有那些從插入點(diǎn)到根節(jié)點(diǎn)的路徑上的節(jié)點(diǎn)的平衡可能被改變,因?yàn)橹挥羞@些節(jié)點(diǎn)的子樹可能發(fā)生變化。因此,我們需要沿著這條路徑上行到根并更新平衡信息,嘗試找出最小不平衡樹。在保持二叉排序樹特性的前提下,調(diào)整最小不平衡子樹中根節(jié)點(diǎn)和子結(jié)點(diǎn)之間的關(guān)系,進(jìn)行相應(yīng)的旋轉(zhuǎn)(rotation),使之成為新的平衡子樹。
先來看看插入的重平衡,因?yàn)榈胶竺嫖覀儠?huì)發(fā)現(xiàn)插入和刪除進(jìn)行的重平衡操作基本是一致的。
我們把需要進(jìn)行平衡(平衡因子絕對(duì)值大于1)的節(jié)點(diǎn)稱為x,由于任意節(jié)點(diǎn)最多有兩個(gè)兒子,因此出現(xiàn)高度不平衡就需要x點(diǎn)的兩棵子樹的高度差2,而這種不平衡只可能出現(xiàn)在下面四種情況中:
- 在節(jié)點(diǎn)X的左孩子節(jié)點(diǎn)的左子樹中插入元素,簡稱LL
- 在節(jié)點(diǎn)X的左孩子節(jié)點(diǎn)的右子樹中插入元素,簡稱LR
- 在節(jié)點(diǎn)X的右孩子節(jié)點(diǎn)的左子樹中插入元素,簡稱RL
- 在節(jié)點(diǎn)X的右孩子節(jié)點(diǎn)的右子樹中插入元素,簡稱RR
其中第1種情況和第4種情況是對(duì)稱的,被稱為發(fā)生“外邊”的情況,可以通過單旋轉(zhuǎn)來解決,而第2種情況和第3種情況是對(duì)稱的,被稱為發(fā)生在“內(nèi)邊”的情況,需要雙旋轉(zhuǎn)來解決。
案例:對(duì)數(shù)組中的元素{3,2,1,4,5,6,7,16,15,14,13,12,11,10,8,9}順序插入并建立一個(gè)平衡二叉樹。以這個(gè)案例為例,來講解上面4個(gè)問題的通用解決辦法和單旋轉(zhuǎn)和雙旋轉(zhuǎn)的概念。
2.1 單旋轉(zhuǎn)
首先是添加前兩個(gè)元素“3、2”的時(shí)候,可以正常的構(gòu)建平衡二叉樹,到了第3個(gè)數(shù)“1”時(shí),發(fā)現(xiàn)此時(shí)根節(jié)點(diǎn)“3”的平衡因子變成了2,此時(shí)整棵樹都成了最小不平衡子樹,因此需要調(diào)整結(jié)構(gòu)。

上圖中的情況情況符合條件1——LL,因此所采用單旋轉(zhuǎn)來重平衡。此時(shí),我們需要右旋(順時(shí)針旋轉(zhuǎn))。旋轉(zhuǎn)的目的實(shí)際上就是為了降低深度,保持平衡。

節(jié)點(diǎn)3經(jīng)過右旋后,節(jié)點(diǎn)2變成了根節(jié)點(diǎn),節(jié)點(diǎn)3變成了2的右子樹,此時(shí)樹節(jié)點(diǎn)1的深度降低了一級(jí),整顆樹重新回到了平衡。我們把通過一次旋轉(zhuǎn)即可修復(fù)平衡的操作叫做單旋轉(zhuǎn)。
平衡因子BF絕對(duì)值大于1的節(jié)點(diǎn)X稱為失衡點(diǎn),修復(fù)一棵被破壞的AVL樹時(shí),找到失衡點(diǎn)是很重要的,查找失衡點(diǎn)就是從新插入、刪除的節(jié)點(diǎn)的位置向上回溯至根節(jié)點(diǎn)的過程。
然后我們?cè)僭黾庸?jié)點(diǎn)4,平衡因子沒有超出限定范圍。增加節(jié)點(diǎn)5時(shí),節(jié)點(diǎn)3的BF值為-2,說明又要旋轉(zhuǎn)了。

上圖中的情況情況符合條件4——RR,需要采用單旋轉(zhuǎn)來重平衡。此時(shí),我們需要左旋(逆時(shí)針旋轉(zhuǎn))。

左旋之后,如上圖右,樹的深度降低了一級(jí),此時(shí)整棵樹又達(dá)到了平衡狀態(tài)。繼續(xù),增加節(jié)點(diǎn)6時(shí),發(fā)現(xiàn)根節(jié)點(diǎn)2的BF值變成了-2,所以我們對(duì)根節(jié)點(diǎn)進(jìn)行了左旋。

左旋的結(jié)果使得節(jié)點(diǎn)2成為節(jié)點(diǎn)4的左孩子,原本處于2和4之間的節(jié)點(diǎn)3是4的左子樹,由于旋轉(zhuǎn)后需要滿足二叉排序樹特性,因此它成了節(jié)點(diǎn)2的右子樹,因?yàn)樵撟訕涞拿恳粋€(gè)關(guān)鍵字都在2-4之間,因此這個(gè)變換是成立的。
現(xiàn)在我們來嘗試總結(jié)出發(fā)生情況1和4時(shí)的通用解法。
首先,情況1和4可以提煉出一個(gè)通用模型:

模型中,左邊如果要發(fā)生不平衡的情況1,那么左子樹1的深度肯定比右子樹1的深度深2層;右邊如果要發(fā)生不平衡的情況4,那么左子樹1的深度肯定比右子樹1的深度淺2層。
針對(duì)上面情況1和情況4,我們分別使用右旋和左旋,來降低或者升高這兩顆子樹的深度:

如上圖,情況1右旋之后,k2成為根節(jié)點(diǎn),k1成為k2的右子節(jié)點(diǎn),k2的右子樹2成為k1的左子樹;情況4左旋之后,k2成為根節(jié)點(diǎn),k1成為k2的左子節(jié)點(diǎn),k2的左子樹2成為k1的右子樹。樹重新達(dá)到了平衡狀態(tài),這就是解決情況1和情況4的通解,并且我們可以發(fā)現(xiàn)它們是對(duì)稱的。
下面增加節(jié)點(diǎn)7,這導(dǎo)致節(jié)點(diǎn)5的BF變成了-2,且符合情況4,需要左旋,根據(jù)上面的通解,采用下面的左旋方法讓樹重新成為平衡二叉樹:

2.2 雙旋轉(zhuǎn)
上面的單旋轉(zhuǎn)對(duì)于情況2和3是沒有用的,因?yàn)榇藭r(shí)樹結(jié)構(gòu)太深,單旋轉(zhuǎn)并不會(huì)減低它的深度。此時(shí)需要使用雙旋轉(zhuǎn)。
當(dāng)增加節(jié)點(diǎn)16時(shí),結(jié)構(gòu)無變化,再增加節(jié)點(diǎn)15,此時(shí)節(jié)點(diǎn)7的BF變成了-2。此時(shí)符合情況3:在節(jié)點(diǎn)X的右孩子節(jié)點(diǎn)的左子樹中插入元素,簡稱RL。如下圖:

此時(shí)簡單的左旋無法解決問題:節(jié)點(diǎn)15成了16的右孩子,這是不符合二叉排序樹的特性的,此時(shí)不能簡單的左旋。如下圖:

對(duì)于這種情況,我們對(duì)于關(guān)鍵節(jié)點(diǎn)7、16、15先建立一個(gè)更廣泛的模型:

其中7-k1、16-k2、15-k3,并且節(jié)點(diǎn)7完全還可以擁有左子樹,節(jié)點(diǎn)16可以擁有右子樹,而節(jié)點(diǎn)15則可以擁有左右子樹。
要想發(fā)生上面k1的BF為-2的情況,需要左子樹2或右子樹2其中一顆子樹的深度比左子樹1深兩層,或者他們都是空子樹,但是我們不知道是具體是什么情況,不過這沒關(guān)系,在這里我們要求出一個(gè)對(duì)這個(gè)問題通解!
此時(shí)為了平衡高度,我們不能將k1當(dāng)作根節(jié)點(diǎn)了,但是左旋——把k2當(dāng)作根節(jié)點(diǎn)也不能解決問題(上面已經(jīng)證實(shí)了),唯一的選擇就是:將k3當(dāng)作新的根節(jié)點(diǎn),并且先使得k2右旋成為k3的右子樹,然后k1左旋成為k3的左子樹,并且左子樹2成為k1的右子樹,右子樹2成為k2的左子樹,這是完全成立的,這就是情況3的通解。 最終,右-左雙旋結(jié)果如下:

我們可以看到,無論是具體發(fā)生了什么情況(左子樹2或右子樹2其中一顆子樹的深度比左子樹1深兩層,或者他們都是空子樹),左-右雙旋轉(zhuǎn)換為上右圖的形狀之后,左子樹2或右子樹2都會(huì)被削減一層深度,而左子樹1會(huì)被增加一層深度,這棵樹始終都是一顆平衡二叉樹。
實(shí)際上,右-左雙旋,分開旋轉(zhuǎn)的過程模型如下:

回到案例,案例中左子樹2、右子樹2、左子樹1、右子樹1都是空樹,使用右-左雙旋之后,樹結(jié)構(gòu)如下圖,該樹得以重新平衡:

接著插入14,情況與剛才類似,節(jié)點(diǎn)6的BF是-2,此時(shí)符合RL的情況(在節(jié)點(diǎn)6的右孩子節(jié)點(diǎn)15的左子樹7中插入元素),如下圖左,此時(shí)繼續(xù)右-左雙旋后,整棵樹又回到了平衡狀態(tài),如下圖右:

繼續(xù)插入13,此時(shí)根節(jié)點(diǎn)4的BF變成了-2,符合情況4,此時(shí)使用一次單左旋即可解決問題:

繼續(xù)插入12之后,向上回溯到節(jié)點(diǎn)14時(shí),發(fā)現(xiàn)節(jié)點(diǎn)14的BF為2,此時(shí)符合情況1,需要右旋恢復(fù)平衡:

繼續(xù)插入11之后,向上回溯到節(jié)點(diǎn)15時(shí),發(fā)現(xiàn)節(jié)點(diǎn)15的BF為2,此時(shí)符合情況1,需要右旋恢復(fù)平衡:

繼續(xù)插入10之后,向上回溯到節(jié)點(diǎn)12時(shí),發(fā)現(xiàn)節(jié)點(diǎn)12的BF為2,此時(shí)符合情況1,需要右旋恢復(fù)平衡:

插入8之后,向上回溯到根節(jié)點(diǎn)也沒有發(fā)現(xiàn)最小不平衡樹,因此不需要旋轉(zhuǎn)。最后插入9之后,我們發(fā)現(xiàn)出現(xiàn)了情況2,此時(shí)我們有情況1和情況4對(duì)稱的經(jīng)驗(yàn),自然也知道需要右-左雙旋的的對(duì)稱操作——左-右雙旋來重新平衡。
先來看左-右雙旋模型:

它和右-左雙旋模型就是對(duì)稱操作,將k3當(dāng)作新的根節(jié)點(diǎn),并且先使得k2左旋成為k3的左子樹,然后k1右旋成為k3的右子樹,并且左子樹2成為k2的右子樹,右子樹2成為k1的左子樹,這是完全成立的,這就是情況2的通解。
左-右雙旋之后,重新形成了平衡二叉樹:

實(shí)際上,左-右雙旋,分開旋轉(zhuǎn)的過程模型如下:

節(jié)點(diǎn)添加完畢,最終形成了一顆平衡二叉樹:

2.3 總結(jié)
插入節(jié)點(diǎn)的不平衡的情況只有四種:
- 在節(jié)點(diǎn)X的左孩子節(jié)點(diǎn)的左子樹中插入元素,簡稱LL
- 在節(jié)點(diǎn)X的左孩子節(jié)點(diǎn)的右子樹中插入元素,簡稱LR
- 在節(jié)點(diǎn)X的右孩子節(jié)點(diǎn)的左子樹中插入元素,簡稱RL
- 在節(jié)點(diǎn)X的右孩子節(jié)點(diǎn)的右子樹中插入元素,簡稱RR
其中1采用單右旋、4采用單左旋即可解決問題。2和3比較復(fù)雜,2需要采用左-右雙旋、3需要采用右-左雙旋。
1和4、2和3是對(duì)稱的情況,現(xiàn)在綜合起來看,所謂的旋轉(zhuǎn)似乎也不那么復(fù)雜,并且我們已經(jīng)求出了這幾種問題的通解,該通解對(duì)于節(jié)點(diǎn)的刪除是同樣適用的,不必再考慮各種特殊情況,非常方便,下面來看看具體的代碼實(shí)現(xiàn)!
3 平衡二叉樹的構(gòu)建
3.1 類架構(gòu)
首先節(jié)點(diǎn)對(duì)象還是需要一個(gè)數(shù)據(jù)域和兩個(gè)引用域,相比于二叉排序樹,還要多一個(gè)節(jié)點(diǎn)高度的字段,這樣方便計(jì)算平衡因子,并且提供返回節(jié)點(diǎn)高度的方法。 另外還需要一個(gè)比較器的引用,因?yàn)樾枰獙?duì)元素進(jìn)行排序,自然需要比較元素的大小,如果外部傳遞了比較器,那么就使用用戶指定的比較器進(jìn)行比較,否則,數(shù)據(jù)類型E必須是Comparable接口的子類,否則因?yàn)椴荒鼙容^而報(bào)錯(cuò)。
另外,還需要提供中序遍歷的方法,該遍歷方法對(duì)于二叉排序樹的結(jié)果將會(huì)順序展示。
public class AvlTree<E> {
/**
* 外部保存根節(jié)點(diǎn)的引用
*/
private BinaryTreeNode<E> root;
/**
* 自定義比較器
*/
private Comparator<? super E> cmp;
/**
* 樹節(jié)點(diǎn)的數(shù)量
*/
private int size;
/**
* 內(nèi)部節(jié)點(diǎn)對(duì)象
*
* @param <E> 數(shù)據(jù)類型
*/
public static class BinaryTreeNode<E> {
//數(shù)據(jù)域
E data;
//左子節(jié)點(diǎn)
BinaryTreeNode<E> left;
//右子節(jié)點(diǎn)
BinaryTreeNode<E> right;
//節(jié)點(diǎn)高度 從0開始,從下往上;null節(jié)點(diǎn)高度返回-1
int height;
public BinaryTreeNode(E data) {
this.data = data;
}
@Override
public String toString() {
return data.toString();
}
}
/**
* 指定比較器
*
* @param cmp 比較器
*/
public AvlTree(Comparator<? super E> cmp) {
this.cmp = cmp;
}
/**
* 空構(gòu)造器
*/
public AvlTree() {
}
/**
* 是否是空樹
*
* @return true 是 ;false 否
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 返回節(jié)點(diǎn)數(shù)
*
* @return 節(jié)點(diǎn)數(shù)
*/
public int size() {
return size;
}
/**
* 對(duì)元素進(jìn)行比較大小的方法,如果傳遞了自定義比較器,則使用自定義比較器,否則則需要數(shù)據(jù)類型實(shí)現(xiàn)Comparable接口
*
* @param e1 被比較的第一個(gè)對(duì)象
* @param e2 被比較的第二個(gè)對(duì)象
* @return 0 相等 ;小于0 e1 < e2 ;大于0 e1 > e2
*/
private int compare(E e1, E e2) {
if (cmp != null) {
return cmp.compare(e1, e2);
} else {
return ((Comparable<E>) e1).compareTo(e2);
}
}
/**
* 保存遍歷出來的節(jié)點(diǎn)數(shù)據(jù)
*/
List<BinaryTreeNode<E>> str = new ArrayList<>();
/**
* 中序遍歷,提供給外部使用的api
*
* @return 遍歷的數(shù)據(jù)
*/
public String toInorderTraversalString() {
//如果是空樹,直接返回空
if (isEmpty()) {
return null;
}
//從根節(jié)點(diǎn)開始遞歸
inorderTraversal(root);
//獲取遍歷結(jié)果
String s = str.toString();
str.clear();
return s;
}
/**
* 中序遍歷 內(nèi)部使用的遞歸遍歷方法,借用了棧的結(jié)構(gòu)
*
* @param root 節(jié)點(diǎn),從根節(jié)點(diǎn)開始
*/
private void inorderTraversal(BinaryTreeNode<E> root) {
BinaryTreeNode<E> left = getLeft(root);
if (left != null) {
//如果左子節(jié)點(diǎn)不為null,則繼續(xù)遞歸遍歷該左子節(jié)點(diǎn)
inorderTraversal(left);
}
//添加數(shù)據(jù)節(jié)點(diǎn)
str.add(root);
//獲取節(jié)點(diǎn)的右子節(jié)點(diǎn)
BinaryTreeNode<E> right = getRight(root);
if (right != null) {
//如果右子節(jié)點(diǎn)不為null,則繼續(xù)遞歸遍歷該右子節(jié)點(diǎn)
inorderTraversal(right);
}
}
/**
* 獲取左子節(jié)點(diǎn)
*
* @param parent 父節(jié)點(diǎn)引用
* @return 左子節(jié)點(diǎn)或者null--表示沒有左子節(jié)點(diǎn)
*/
public BinaryTreeNode<E> getLeft(BinaryTreeNode<E> parent) {
return parent == null ? null : parent.left;
}
/**
* 獲取右子節(jié)點(diǎn)
*
* @param parent 父節(jié)點(diǎn)引用
* @return 右子節(jié)點(diǎn)或者null--表示沒有右子節(jié)點(diǎn)
*/
public BinaryTreeNode<E> getRight(BinaryTreeNode<E> parent) {
return parent == null ? null : parent.right;
}
/**
* 獲取根節(jié)點(diǎn)
*
* @return 根節(jié)點(diǎn) ;或者null--表示空樹
*/
public BinaryTreeNode<E> getRoot() {
return root;
}
/**
* 獲取height
*
* @param node 節(jié)點(diǎn)
* @return 高度或者-1 表示節(jié)點(diǎn)為null
*/
private int getHeight(BinaryTreeNode<E> node) {
return node == null ? -1 : node.height;
}
}3.2 查找的方法
平衡二叉樹就是一顆二叉排序樹,其查找方法可以復(fù)用二叉排序樹的查找方法,很簡單:
- 若根節(jié)點(diǎn)的關(guān)鍵字值等于查找的關(guān)鍵字,成功,返回true;
- 否則,若小于根節(jié)點(diǎn)的關(guān)鍵字值,遞歸查左子樹;
- 若大于根節(jié)點(diǎn)的關(guān)鍵字值,遞歸查右子樹;
- 最終查找到葉子節(jié)點(diǎn)還是沒有數(shù)據(jù),那么查找失敗,則返回false
/**
* 查找,開放給外部使用的api
* @param e 要查找的元素
* @return false 不存在 true 存在
*/
public boolean contains(E e) {
return contains(e, root);
}
/**
* 查找,內(nèi)部調(diào)用的方法,從根節(jié)點(diǎn)開始查找
*
* @param e 要查找的元素
* @param root 節(jié)點(diǎn)
* @return false 不存在 true 存在
*/
private boolean contains(E e, BinaryTreeNode<E> root) {
/*null校驗(yàn)*/
if (root == null) {
return false;
}
/*調(diào)用比較的方法*/
int i = compare(e, root.data);
/*如果大于0,則說明e>root.date 繼續(xù)查詢右子樹*/
if (i > 0) {
return contains(e, root.right);
/*如果小于0,則說明e<root.date 繼續(xù)查詢左子樹*/
} else if (i < 0) {
return contains(e, root.left);
} else {
/*如果等于0,則說明e=root.date 即查詢成功*/
return true;
}
}3.3 檢查是否平衡的方法
很簡單,只需要遞歸的查看所有節(jié)點(diǎn),判斷是否存在的節(jié)點(diǎn)的左右子節(jié)點(diǎn)高度差絕對(duì)值是否大于1的情況就能判斷了,如果存在,那么返回false表示不是平衡二叉樹,不存在就返回true表示是平衡二叉樹。
/**
* 保存是否平衡的標(biāo)志
*/
private boolean balance = true;
/**
* 檢查是否是平衡二叉樹的方法,當(dāng)然也可以debug看,如果你不嫌麻煩……
*
* @return true 是 ;false 否
*/
public boolean checkBalance() {
checkBalance(root);
boolean balanceNow=balance;
balance=true;
return balanceNow;
}
/**
* 遞歸檢查是否平衡,實(shí)際上這里采用了后序遍歷,即左子節(jié)點(diǎn)-右子節(jié)點(diǎn)-根節(jié)點(diǎn)的方法遞歸遍歷檢查
*
* @param root 根節(jié)點(diǎn)
* @return 節(jié)點(diǎn)的高度
*/
private int checkBalance(BinaryTreeNode<E> root) {
if (root == null) {
return -1;
}
//返回左子樹的高度
int hl = checkBalance(root.left);
//返回右子樹的高度
int hr = checkBalance(root.right);
//如果root的左右子樹高度差絕對(duì)值大于1,或者checkBalance和getHeight方法獲取的左/右子樹高度不一致,那么算作不平衡
if (Math.abs(getHeight(root.left) - getHeight(root.right)) > 1 ||
getHeight(root.left) != hl || getHeight(root.right) != hr) {
balance = false;
}
return getHeight(root);
}3.4 插入的方法
平衡二叉樹和二叉排序樹的最大區(qū)別就是在插入和刪除的時(shí)候了。我們已經(jīng)討論過插入之后的4種出現(xiàn)平衡問題的特殊情況,這里不再贅述,下面看代碼具體如何實(shí)現(xiàn):
/**
* 插入,開放給外部使用的api
*
* @param e 要插入的元素
*/
public void insert(E e) {
//返回root,但此時(shí)新的節(jié)點(diǎn)可能已經(jīng)被插入進(jìn)去了
root = insert(e, root);
}
/**
* 插入,開放給外部使用的api
*
* @param es 要插入的元素的數(shù)組,注意,數(shù)組元素的順序存儲(chǔ)的位置將會(huì)影響二叉排序樹的生成
*/
public void insert(E[] es) {
//返回root,但此時(shí)新的節(jié)點(diǎn)可能已經(jīng)被插入進(jìn)去了
for (E e : es) {
root = insert(e, root);
}
}
/**
* 插入,內(nèi)部調(diào)用的方法,先從根節(jié)點(diǎn)開始遞歸查找要插入的位置,然后插入
* 大部分代碼都和排序二叉樹的相似,區(qū)別就是在插入之后,會(huì)調(diào)用嘗試重平衡的方法rebalance
*
* @param e 要插入的數(shù)據(jù)
* @param root 節(jié)點(diǎn)
* @return 原節(jié)點(diǎn)重平衡之后的節(jié)點(diǎn)或者新插入的節(jié)點(diǎn)
*/
private BinaryTreeNode<E> insert(E e, BinaryTreeNode<E> root) {
/*沒有查找到,那么直接構(gòu)建新的節(jié)點(diǎn)返回,將會(huì)在上一層方法中被賦值給其父節(jié)點(diǎn)的某個(gè)引用,這個(gè)插入的位置肯定是該遍歷路徑上的最后一點(diǎn)
* 即插入的元素節(jié)點(diǎn)肯定是屬于葉子節(jié)點(diǎn)*/
if (root == null) {
size++;
return new BinaryTreeNode<>(e);
}
/*調(diào)用比較的方法*/
int i = compare(e, root.data);
/*如果大于0,則說明e>root.date 繼續(xù)查詢右子樹*/
if (i > 0) {
//重新賦值
root.right = insert(e, root.right);
/*如果小于0,則說明e<root.date 繼續(xù)查詢左子樹*/
} else if (i < 0) {
//重新賦值
root.left = insert(e, root.left);
} else {
/*如果等于0,則說明e=root.date 即存在節(jié)點(diǎn) 什么都不做*/
}
/*insert遞歸插入之后,在返回時(shí),會(huì)調(diào)用重新平衡并且設(shè)置高度的方法 嘗試重平衡root根節(jié)點(diǎn) 而不是像排序二叉樹一樣簡單的返回root
*從新插入節(jié)點(diǎn)的父節(jié)點(diǎn)一直向上回溯直到根節(jié)點(diǎn),嘗試尋找最小不平衡樹,找到之后會(huì)進(jìn)行平衡,返回返回平衡之后的樹,.*/
return rebalance(root);
}
/**
* 重平衡的方法
* 1) 在節(jié)點(diǎn)X的左孩子節(jié)點(diǎn)的左子樹中插入元素,簡稱LL 右旋
* 2) 在節(jié)點(diǎn)X的左孩子節(jié)點(diǎn)的右子樹中插入元素,簡稱LR 左-右雙旋
* 3) 在節(jié)點(diǎn)X的右孩子節(jié)點(diǎn)的左子樹中插入元素,簡稱RL 左旋
* 4) 在節(jié)點(diǎn)X的右孩子節(jié)點(diǎn)的右子樹中插入元素,簡稱RR 右-左雙旋
*
* @param root 樹的根節(jié)點(diǎn),無論是否是最小不平衡樹,都是走這個(gè)方法
* @return 平衡之后的樹的根節(jié)點(diǎn)
*/
private BinaryTreeNode<E> rebalance(BinaryTreeNode<E> root) {
/*1、如果節(jié)點(diǎn)為null,直接返回null*/
if (root == null) {
return null;
}
/*2、開始旋轉(zhuǎn)*/
/*2.1、如果左子樹的高度減去右子樹的高度值大于1,說明左子樹的高度大于右子樹的高度至少2層,可能是情況1、2 繼續(xù)判斷*/
if (getHeight(root.left) - getHeight(root.right) > 1) {
/*如果左子節(jié)點(diǎn)的左子節(jié)點(diǎn)高度大于左子節(jié)點(diǎn)的右子節(jié)點(diǎn)高度,那說明是情況1,否則是情況2*/
if (getHeight(root.left.left) >= getHeight(root.left.right)) {
/*2.1.1、右旋*/
root = rotateRight(root);
} else {
/*2.1.2、左-右雙旋*/
root = rotateLeftAndRight(root);
}
/*2.2、如果右子樹的高度減去左子樹的高度值大于1,說明右子樹的高度大于左子樹的高度至少2層,可能是情況3、4 繼續(xù)判斷*/
} else if (getHeight(root.right) - getHeight(root.left) > 1) {
/*如果右子節(jié)點(diǎn)的右子節(jié)點(diǎn)高度大于右子節(jié)點(diǎn)的左子節(jié)點(diǎn)高度,那說明是情況4,否則是情況3*/
if (getHeight(root.right.right) >= getHeight(root.right.left)) {
/*2.2.1、左旋*/
root = rotateLeft(root);
} else {
/*2.2.2、右-左雙旋*/
root = rotateRightAndLeft(root);
}
}
/*3、到這一步,說明旋轉(zhuǎn)完畢,或者不需要旋轉(zhuǎn),但是都需要重新計(jì)算高度,高度為左/右子樹高度最大值+1*/
root.height = Math.max(getHeight(root.left), getHeight(root.right)) + 1;
return root;
}
/**
* 右旋
* 通解:右旋之后,k2成為根節(jié)點(diǎn),k1成為k2的右子節(jié)點(diǎn),k2的右子樹2成為k1的左子樹
*
* @param k1 需要旋轉(zhuǎn)的最小不平衡樹根節(jié)點(diǎn)
* @return k2 旋轉(zhuǎn)后的最小不平衡樹根節(jié)點(diǎn), 已經(jīng)轉(zhuǎn)換為平衡二叉樹
*/
private BinaryTreeNode<E> rotateRight(BinaryTreeNode<E> k1) {
//獲取k2,k2是k1的左子節(jié)點(diǎn)
BinaryTreeNode<E> k2 = k1.left;
//k2的右子樹成為k1的左子樹
k1.left = k2.right;
//k1成為k2的右子節(jié)點(diǎn)
k2.right = k1;
//k1的高度等于k1的左或者右子樹的高度的最大值+1;
k1.height = Math.max(getHeight(k1.left), getHeight(k1.right)) + 1;
//k2的高度等于k2的左子節(jié)點(diǎn)和k1的高度(此時(shí)k1就是k2的右子節(jié)點(diǎn))的最大值+1
k2.height = Math.max(getHeight(k2.left), k1.height) + 1;
//返回k2,k2成為根節(jié)點(diǎn)
return k2;
}
/**
* 左旋 很簡單,實(shí)際上就是右旋的鏡像
* 通解:左旋之后,k2成為根節(jié)點(diǎn),k1成為k2的左子節(jié)點(diǎn),k2的左子樹2成為k1的右子樹
*
* @param k1 需要旋轉(zhuǎn)的最小不平衡樹根節(jié)點(diǎn)
* @return k2 旋轉(zhuǎn)后的最小不平衡樹根節(jié)點(diǎn), 已經(jīng)轉(zhuǎn)換為平衡二叉樹
*/
private BinaryTreeNode<E> rotateLeft(BinaryTreeNode<E> k1) {
//獲取k2,k2是k1的右子節(jié)點(diǎn)
BinaryTreeNode<E> k2 = k1.right;
//k2的左子樹成為k1的右子樹
k1.right = k2.left;
//k1成為k2的左子節(jié)點(diǎn)
k2.left = k1;
//k1的高度等于k1的左或者右子樹的高度的最大值+1;
k1.height = Math.max(getHeight(k1.left), getHeight(k1.right)) + 1;
//k2的高度等于k2的右子節(jié)點(diǎn)和k1的高度(此時(shí)k1就是k2的左子節(jié)點(diǎn))的最大值+1
k2.height = Math.max(getHeight(k2.right), k1.height) + 1;
//返回k2,k2成為根節(jié)點(diǎn)
return k2;
}
/**
* 右-左雙旋
* 通解:將k3當(dāng)作新的根節(jié)點(diǎn),并且先使得k2右旋成為k3的右子樹,然后k1左旋成為k3的左子樹,并且左子樹2成為k1的右子樹,右子樹2成為k2的左子樹
*
* @param k1 需要旋轉(zhuǎn)的最小不平衡樹根節(jié)點(diǎn)
* @return 旋轉(zhuǎn)后的最小不平衡樹根節(jié)點(diǎn), 已經(jīng)轉(zhuǎn)換為平衡二叉樹
*/
private BinaryTreeNode<E> rotateRightAndLeft(BinaryTreeNode<E> k1) {
/*1、先對(duì)k1的右子節(jié)點(diǎn)k2進(jìn)行右旋,返回右旋之后的根節(jié)點(diǎn)k3,然后使得成為k3成為的k1的左子樹*/
k1.right = rotateRight(k1.right);
/*2、然后對(duì)k1進(jìn)行左旋,成為k3的左子樹,返回的根節(jié)點(diǎn)就是k3,即返回旋轉(zhuǎn)之后的根節(jié)點(diǎn)*/
return rotateLeft(k1);
}
/**
* 左-右雙旋 很簡單,實(shí)際上就是右-左雙旋的鏡像
* 通解: 將k3當(dāng)作新的根節(jié)點(diǎn),并且先使得k2左旋成為k3的左子樹,然后k1右旋成為k3的右子樹,并且左子樹2成為k2的右子樹,右子樹2成為k1的左子樹
*
* @param k1 需要旋轉(zhuǎn)的最小不平衡樹根節(jié)點(diǎn)
* @return 旋轉(zhuǎn)后的最小不平衡樹根節(jié)點(diǎn), 已經(jīng)轉(zhuǎn)換為平衡二叉樹
*/
private BinaryTreeNode<E> rotateLeftAndRight(BinaryTreeNode<E> k1) {
/*1、先對(duì)k1的左子節(jié)點(diǎn)k2進(jìn)行左旋,返回左旋之后的根節(jié)點(diǎn)k3,然后使得成為k3成為的k1的左子樹*/
k1.left = rotateLeft(k1.left);
/*2、然后對(duì)k1進(jìn)行右旋,成為k3的右子樹,返回的根節(jié)點(diǎn)就是k3,即返回旋轉(zhuǎn)之后的根節(jié)點(diǎn)*/
return rotateRight(k1);
}3.4.1 測試
AvlTree<Integer> avlTree = new AvlTree<>();
Integer[] es = new Integer[]{3, 2, 1, 4, 5, 6, 7, 16, 15, 14, 13, 12, 11, 10, 8, 9};
//批量插入
avlTree.insert(es);
//中序遍歷輸出
System.out.println(avlTree.toInorderTraversalString());
//檢查是否平衡
System.out.println(avlTree.checkBalance());
//數(shù)量
System.out.println(avlTree.size());
在insert之后打上斷點(diǎn),Debug,可以看到avlTree的數(shù)據(jù)結(jié)構(gòu)和在實(shí)現(xiàn)原理中最終的結(jié)構(gòu)是一致的。


3.5 查找最大值和最小值
很簡單,最左邊的節(jié)點(diǎn)一定是最小的,最右邊的節(jié)點(diǎn)一定是最大的。因此查找最小的節(jié)點(diǎn)只需要向左遞歸查找,查找最大的節(jié)點(diǎn)只需要向右遞歸查找。
/**
* 查找最小的節(jié)點(diǎn)
*
* @param root 根節(jié)點(diǎn)
* @return 最小的節(jié)點(diǎn)
*/
private BinaryTreeNode<E> findMin(BinaryTreeNode<E> root) {
if (root == null) {
return null;
/*如果該節(jié)點(diǎn)沒有左右子節(jié)點(diǎn),那么該節(jié)點(diǎn)就是最小的節(jié)點(diǎn),返回*/
} else if (root.left == null) {
return root;
}
/*如果該節(jié)點(diǎn)存在左子節(jié)點(diǎn),那么繼續(xù)向左遞歸查找*/
return findMin(root.left);
}
/**
* 查找最大的節(jié)點(diǎn)
*
* @param root 根節(jié)點(diǎn)
* @return 最大的節(jié)點(diǎn)
*/
private BinaryTreeNode<E> findMax(BinaryTreeNode<E> root) {
if (root == null) {
return null;
/*如果該節(jié)點(diǎn)沒有右子節(jié)點(diǎn),那么該節(jié)點(diǎn)就是最大的節(jié)點(diǎn),返回*/
} else if (root.right == null) {
return root;
}
/*如果該節(jié)點(diǎn)存在右子節(jié)點(diǎn),那么繼續(xù)向右遞歸查找*/
return findMax(root.right);
}
3.6 刪除的方法
平衡二叉樹節(jié)點(diǎn)的刪除同樣可能導(dǎo)致產(chǎn)生不平衡的狀態(tài),因此同樣在二叉排序樹的刪除代碼的基礎(chǔ)上,刪除元素之后需要在刪除節(jié)點(diǎn)之上進(jìn)行回溯直到根節(jié)點(diǎn),嘗試找出最小不平衡樹來進(jìn)行重平衡。其平衡的方法是和插入的時(shí)候是一樣的。
/**
* 刪除,開放給外部使用的api
*
* @param e 要?jiǎng)h除的元素
*/
public void delete(E e) {
//返回root,但此時(shí)可能有一個(gè)節(jié)點(diǎn)已經(jīng)被刪除了
root = delete(e, root);
}
/**
* 刪除,內(nèi)部調(diào)用的方法,刪除分為三種情況: 1、該節(jié)點(diǎn)沒有子節(jié)點(diǎn) 2、該字節(jié)僅有一個(gè)子節(jié)點(diǎn) 3、該節(jié)點(diǎn)具有兩個(gè)子節(jié)點(diǎn)
*
* @param e 要?jiǎng)h除的數(shù)據(jù)
* @param root (子)樹根節(jié)點(diǎn)
* @return 該根節(jié)點(diǎn)重平衡之后的節(jié)點(diǎn)
*/
private BinaryTreeNode<E> delete(E e, BinaryTreeNode<E> root) {
/*沒有查找到,那么什么都不做*/
if (root == null) {
return null;
}
/*調(diào)用比較的方法*/
int i = compare(e, root.data);
/*如果大于0,則說明e>root.date 繼續(xù)查詢右子樹*/
if (i > 0) {
//重新賦值
root.right = delete(e, root.right);
/*如果小于0,則說明e<root.date 繼續(xù)查詢左子樹*/
} else if (i < 0) {
//重新賦值
root.left = delete(e, root.left);
} else {
/*如果等于0,則說明e=root.date 即查詢成功 開始執(zhí)行刪除*/
/*如果兩個(gè)子節(jié)點(diǎn)都不為null*/
if (root.left != null && root.right != null) {
/*遞歸查找最小的節(jié)點(diǎn),然后遞歸刪除*/
root.data = findMin(root.right).data;
root.right = delete(root.data, root.right);
} else {
/*如果一個(gè)子節(jié)點(diǎn)不為null,則返回該子節(jié)點(diǎn);或者兩個(gè)子節(jié)點(diǎn)都為null,則返回null
* 此時(shí)該root節(jié)點(diǎn)已經(jīng)被"繞過了"*/
root = (root.left != null) ? root.left : root.right;
--size;
}
}
/*和二叉排序樹直接返回節(jié)點(diǎn)不同的是,刪除操作完成之后將會(huì)調(diào)用該方法,從被刪除節(jié)點(diǎn)回溯至根節(jié)點(diǎn),對(duì)節(jié)點(diǎn)進(jìn)行重平衡,然后才返回平衡后的節(jié)點(diǎn)*/
return rebalance(root);
}
3.6.1 測試
針對(duì)上面插入的平衡二叉樹進(jìn)行刪除:
System.out.println("======>首先刪除5 此時(shí)沒有影響,不需要重平衡");
avlTree.delete(5);
//檢查是否平衡
System.out.println(avlTree.checkBalance());
//中序遍歷輸出
System.out.println(avlTree.toInorderTraversalString());
System.out.println("======>再次刪除6 此時(shí)節(jié)點(diǎn)4的BF為2 需要右旋重平衡");
avlTree.delete(6);
//檢查是否平衡
System.out.println(avlTree.checkBalance());
//中序遍歷輸出
System.out.println(avlTree.toInorderTraversalString());
System.out.println("======>再次刪除11 由于該節(jié)點(diǎn)擁有左右子樹,實(shí)際上刪除的是該節(jié)點(diǎn)的右子樹的最小節(jié)點(diǎn)12,然后將12的值賦值給11的節(jié)點(diǎn),并導(dǎo)致節(jié)點(diǎn)12的原父節(jié)點(diǎn)11不平衡,需要重平衡");
avlTree.delete(11);
//檢查是否平衡
System.out.println(avlTree.checkBalance());
//中序遍歷輸出
System.out.println(avlTree.toInorderTraversalString());
System.out.println("======>再次刪除7 由于該節(jié)點(diǎn)擁有左右子樹,實(shí)際上刪除的是該節(jié)點(diǎn)的右子樹的最小節(jié)點(diǎn)8,然后將8的值賦值給7的節(jié)點(diǎn),并導(dǎo)致節(jié)點(diǎn)8的原父節(jié)點(diǎn)9不平衡,需要重平衡");
avlTree.delete(7);
//檢查是否平衡
System.out.println(avlTree.checkBalance());
//中序遍歷輸出
System.out.println(avlTree.toInorderTraversalString());
System.out.println("======>再次刪除9、12 此時(shí)不需要重平衡");
avlTree.delete(9);
avlTree.delete(12);
//檢查是否平衡
System.out.println(avlTree.checkBalance());
//中序遍歷輸出
System.out.println(avlTree.toInorderTraversalString());
System.out.println("======>最后刪除8 由于該節(jié)點(diǎn)擁有左右子樹,實(shí)際上刪除的是該節(jié)點(diǎn)的右子樹的最小節(jié)點(diǎn)10節(jié)點(diǎn),然后將10的值賦值給8的節(jié)點(diǎn),并導(dǎo)致節(jié)點(diǎn)10的原父節(jié)點(diǎn)13不平衡,需要重平衡");
avlTree.delete(8);
//檢查是否平衡
System.out.println(avlTree.checkBalance());
//中序遍歷輸出
System.out.println(avlTree.size());
System.out.println(avlTree.toInorderTraversalString());
在進(jìn)行上面的一系列刪除之后,樹結(jié)構(gòu)會(huì)變成如下形狀:

4 平衡二叉樹的總結(jié)
平衡二叉樹是基于二叉排序樹的,但是由于其必須保持平衡的特性,因此其編碼難度比二叉排序樹的編碼難度更高,不過如果我們搞懂了其旋轉(zhuǎn)的原理,那么實(shí)現(xiàn)起來還是比較簡單的。
如果我們需要查找的集合本身沒有順序,在頻繁查找的同時(shí)也需要經(jīng)常的插入和刪除操作,顯然我們需要構(gòu)建一棵二叉排序樹,但是不平衡的二叉排序樹,極端情況下可能退化為鏈表,查找效率是非常低的,因此我們需要在構(gòu)建時(shí),就讓這棵二叉排序樹是動(dòng)態(tài)的轉(zhuǎn)換為平衡二叉樹,此時(shí)我們的查找時(shí)間復(fù)雜度就為O(logn),而插入和刪除也為O(logn)。這顯然是比較理想的一種動(dòng)態(tài)查找表算法。
本文介紹了平衡二叉樹的原理以及Java代碼的完全實(shí)現(xiàn),要想搞懂平衡二叉樹需要先搞懂二叉排序樹:二叉排序樹的詳解以及Java代碼的完全實(shí)現(xiàn)。而搞懂平衡二叉樹又是搞懂紅黑樹的基礎(chǔ),后續(xù)文章我們將會(huì)介紹紅黑樹的概念以及Java代碼的完全實(shí)現(xiàn)!
以上就是Java數(shù)據(jù)結(jié)構(gòu)之平衡二叉樹的原理與實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Java平衡二叉樹的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
簡單學(xué)習(xí)Java API 設(shè)計(jì)實(shí)踐
API(Application Programming Interface,應(yīng)用程序編程接口)是一些預(yù)先定義的函數(shù),目的是提供應(yīng)用程序與開發(fā)人員基于某軟件或硬件的以訪問一組例程的能力,而又無需訪問源碼,或理解內(nèi)部工作機(jī)制的細(xì)節(jié)。需要的可以了解一下2019-06-06
Java字節(jié)緩存流的構(gòu)造方法之文件IO流
這篇文章主要介紹了Java字節(jié)緩存流的構(gòu)造方法之文件IO流,同時(shí)也介紹了字符流中的一些相關(guān)的內(nèi)容,并且通過大量的案例供大家理解。最后通過一些經(jīng)典的案例幫助大家對(duì)前面所學(xué)的知識(shí)做了一個(gè)綜合的應(yīng)用,需要的朋友可以參考一下2022-04-04
mybatis批量添加,批量更新之前如何判斷是否已經(jīng)存在
這篇文章主要介紹了mybatis批量添加,批量更新之前如何判斷是否已經(jīng)存在,具有很好的參考價(jià)值,希望對(duì)的有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
Java中ArrayList和LinkedList的區(qū)別
ArrayList和LinkedList在這個(gè)方法上存在一定的性能差異,本文就介紹了Java中ArrayList和LinkedList的區(qū)別,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
Java 創(chuàng)建兩個(gè)線程模擬對(duì)話并交替輸出實(shí)現(xiàn)解析
這篇文章主要介紹了Java 創(chuàng)建兩個(gè)線程模擬對(duì)話并交替輸出實(shí)現(xiàn)解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10
Java23種設(shè)計(jì)模式中的單例模式你了解嗎
這篇文章主要為大家詳細(xì)介紹了Java23種設(shè)計(jì)模式中的單例模式,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-02-02
SpringCloud創(chuàng)建多模塊項(xiàng)目的實(shí)現(xiàn)示例
,Spring Cloud作為一個(gè)強(qiáng)大的微服務(wù)框架,提供了豐富的功能和組件,本文主要介紹了SpringCloud創(chuàng)建多模塊項(xiàng)目的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02

