平衡二叉樹(AVL樹)
技術標籤:演算法與資料結構平衡二叉樹avl資料結構java二叉排序樹
文章目錄
一、平衡二叉樹概述
1.1 什麼是平衡二叉樹
平衡二叉樹也叫 AVL 樹。平衡二叉樹是具有以下特點的二叉查詢樹:它是一棵空樹
或它的左右兩個子樹的高度差的絕對值不超過 1, 並且左右兩個子樹都是一棵平衡二叉樹。
圖一 平衡二叉樹 | 圖二 非平衡二叉樹 |
1.2 為什麼要有平衡二叉樹
在上一篇文章 二叉排序樹 中對二叉排序樹做了介紹。
我們知道二叉排序樹是結合了陣列和連結串列的優點的一種資料結構,它在增刪節點效率還不錯的同時還有具有很好的查詢效率。
但是對於下面這樣的二叉排序樹:
它的左子樹為空,從形式上看更像是一個連結串列。雖然它在插入、刪除速度上依然很快,但是查詢的速度卻和連結串列一樣甚至更慢(因為還需要判斷有無左子樹)。這樣的二叉排序樹顯然喪失了最大的優點。
我們可以對它改造,將其改造成平衡二叉樹:
可以看到,在不改變二叉樹元素的前提下,新的二叉樹的查詢速度得到了有效提升。
這就是平衡二叉樹的意義。
二、平衡二叉樹的操作
2.1 左旋轉
2.1.1 需要左旋轉的情況
當根節點的右子樹高度減去左子樹高度大於 1 時,需要左旋二叉樹。
對於下面這樣的二叉樹:
我們可以看出來,它的左子樹的高度比右子樹的高度少 2。
我們可以通過將其左旋轉使其變成下面這樣的平衡二叉樹:
2.1.2 左旋轉步驟
左旋轉的基本步驟如下:
- 首先建立一個新節點 ,該節點的值設為根節點的值;
- 新節點的左指標指向根節點的左子樹;
- 新節點的右指標指向根節點的右子節點的左子樹;
- 將根節點的值設定為根節點的右子節點的值;
- 根節點的左指標指向新節點;
- 根節點的右指標指向其右子節點的右子樹。
左旋轉過程圖解如下:
2.1.3 左旋轉程式碼實現
左旋轉的程式碼實現如下,該函式是節點類的成員函式:
// 以當前節點為根節點將二叉樹左旋
public void leftRotate(){
// 1. 新建一個節點,值為根節點的值
AVLNode newNode = new AVLNode(this.value);
// 2. 新節點的左指標指向根節點的左子樹
newNode.left = this.left;
// 3. 新節點的右指標指向根節點的右子樹的左子樹
newNode.right = this.right.left;
// 4. 將根節點的值改為其右子節點的值
this.value = this.right.value;
// 5. 根節點的左指標指向新節點
this.left = newNode;
// 6. 根節點的右指標指向其右子節點的右子節點
this.right = this.right.right;
}
2.2 右旋轉
2.2.1 需要右旋轉的情況
當根節點的左子樹高度減去右子樹高度大於 1 時,需要右旋二叉樹。
如下圖圖左的二叉排序樹,其根節點左子樹的高度比右子樹大 2,將其右旋轉之後,新的二叉樹的搜尋效率有效提升。
2.2.2 右旋轉步驟
將二叉樹右旋轉基本步驟如下:
- 首先建立一個新節點,新節點的值設為根節點的值;
- 新節點的右指標指向根節點的右子樹;
- 新節點的左指標指向根節點的左子節點的右子樹;
- 將根節點的值設為其左子節點的值;
- 根節點的左指標指向其左子節點的左子樹;
- 根節點的右指標指向新節點。
二叉樹右旋轉過程與左旋轉類似,可以參照左旋轉的步驟圖來理解。
2.2.3 右旋轉程式碼實現
右旋轉的程式碼實現如下,該函式是節點類的成員函式:
// 以當前節點為根節點將二叉樹右旋
public void rightRotate(){
// 1. 新建一個節點,值為根節點的值
AVLNode newNode = new AVLNode(this.value);
// 2. 將新節點的右指標指向根節點的右子樹
newNode.right = this.right;
// 3. 將新節點的左指標指向根節點的左子樹的右子樹
newNode.left = this.left.right;
// 4. 將根節點的值改為其左子節點的值
this.value = this.left.value;
// 5. 將根節點的左指標指向其左子節點的左子樹
this.left = this.left.left;
// 6. 將根節點的右指標指向新節點
this.right = newNode;
}
2.3 雙旋轉
2.3.1 需要雙旋轉的情況
前面的兩個二叉樹進行單旋轉(即一次旋轉)就可以將非平衡二叉樹轉成平衡二叉樹。但是在某些情況下,單旋轉並不能完成平衡二叉樹的轉換。
上圖就是典型的無法通過單旋轉變成 AVL 樹的情況。圖左的二叉排序樹經過一次右旋轉之後,得到的依然是一個非平衡二叉樹。
我們可以看到出現這種問題的原因是:原二叉樹在右旋的過程中,新的節點的左指標指向了根節點的左子節點的右子樹,而這個左子節點的右子樹的高度高於它的左子樹,故單旋轉之後二叉樹沒有變成 AVL 樹。
如何解決這個問題呢?
我們可以在對原二叉樹右旋之前,將其左子樹(以 7 為根節點的樹)進行一次左旋即可。
也就是說,針對這種情況的二叉排序樹,需要進行雙旋轉才會變成 AVL 樹。
2.3.2 雙旋轉步驟
雙旋轉是基於單旋轉(左旋、右旋)的,其只是對單旋轉的程式碼呼叫,通過程式碼來理解雙旋轉即可。
雙旋轉程式碼的核心思想在於:在建立二叉樹的過程中,每次新增新的節點,都要判斷一下該二叉樹是否需要旋轉。
- 如果二叉樹需要左旋,則先判斷根節點的右子節點的左子樹高度是否大於右子樹的高度,如果大於則先對根節點的右子樹進行右旋,最後再對原二叉樹進行左旋;
- 如果二叉樹需要右旋,則先判斷根節點的左子節點的右子樹高度是否大於左子樹的高度,如果大於則先對根節點的左子樹進行左旋,最後再對原二叉樹進行右旋。
2.3.3 雙旋轉程式碼實現
雙旋轉完整程式碼實現如下:
/**
* @Description 平衡二叉樹
*/
public class No2_AVLTree {
public static void main(String[] args) {
int[] arr = {10,9,11,6,5,7,8};
AVLTree tree = new AVLTree();
for (int i=0; i<arr.length; i++){ // 建立平衡二叉樹
tree.addNode(new AVLNode(arr[i]));
}
tree.infixOrder(); // 中序遍歷
System.out.println("樹的高度為:" + tree.root.getHeight());
System.out.println("左子樹高度為:" + tree.root.getLeftHeight());
System.out.println("右子樹高度為:" + tree.root.getRightHeight());
}
}
/**
* AVL —— 平衡二叉樹
*/
class AVLTree{
AVLNode root; // 根節點
// 中序遍歷
public void infixOrder(){
if (root != null){
root.infixOrder();
}else{
System.out.println("二叉樹為空!");
}
}
// 新增節點到二叉樹中
public void addNode(AVLNode node){
if (root == null){
root = node;
}else{
root.addNode(node);
// 每新增一個節點,就判斷一下是否需要旋轉
if (root.getRightHeight() - root.getLeftHeight() > 1){ // 如果右子樹減去左子樹高度大於 1
if (root.right.getLeftHeight() > root.right.getRightHeight()){ // 判斷是否要雙旋轉
root.right.rightRotate(); // 根節點的右子樹右旋
}
root.leftRotate(); // 左旋
}
if (root.getLeftHeight() - root.getRightHeight() > 1){ // 如果左子樹減去右子樹高度大於 1
if (root.left.getRightHeight() > root.left.getLeftHeight()){ // 判斷是否要雙旋轉
root.left.leftRotate(); // 根節點的左子樹左旋
}
root.rightRotate();
}
}
}
}
/**
* 節點類
*/
class AVLNode{
int value; // 節點的值
AVLNode left; // 指向左子節點
AVLNode right; // 指向右子節點
public AVLNode(int value){
this.value = value;
}
// 新增結點
public void addNode(AVLNode node){
if (node.value < this.value){
if (this.left != null){
this.left.addNode(node);
}else{
this.left = node;
}
}else{
if (this.right != null){
this.right.addNode(node);
}else{
this.right = node;
}
}
}
// 中序遍歷:左->根->右
public void infixOrder(){
if (this.left != null){ // 先左子節點
this.left.infixOrder();
}
System.out.println(this); // 再根節點
if (this.right != null){
this.right.infixOrder(); // 最後右子節點
}
}
// 求以當前節點為根節點的樹的高度
public int getHeight(){
return Math.max(left == null ? 0 : left.getHeight(),
right == null ? 0 : right.getHeight()) + 1;
}
// 獲取以當前節點為根節點的左子樹高度
public int getLeftHeight(){
if (this.left != null){
return this.left.getHeight();
}
return 0;
}
// 獲取以當前節點為根節點的右子樹高度
public int getRightHeight(){
if (this.right != null){
return this.right.getHeight();
}
return 0;
}
// 以當前節點為根節點將二叉樹左旋
public void leftRotate(){
// 1. 新建一個節點,值為根節點的值
AVLNode newNode = new AVLNode(this.value);
// 2. 新節點的左指標指向根節點的左子樹
newNode.left = this.left;
// 3. 新節點的右指標指向根節點的右子樹的左子樹
newNode.right = this.right.left;
// 4. 將根節點的值改為其右子節點的值
this.value = this.right.value;
// 5. 根節點的左指標指向新節點
this.left = newNode;
// 6. 根節點的右指標指向其右子樹的右子節點
this.right = this.right.right;
}
// 以當前節點為根節點將二叉樹右旋
public void rightRotate(){
// 1. 新建一個節點,值為根節點的值
AVLNode newNode = new AVLNode(this.value);
// 2. 將新節點的右指標指向根節點的右子樹
newNode.right = this.right;
// 3. 將新節點的左指標指向根節點的左子樹的右子樹
newNode.left = this.left.right;
// 4. 將根節點的值改為其左子節點的值
this.value = this.left.value;
// 5. 將根節點的左指標指向其左子節點的左子樹
this.left = this.left.left;
// 6. 將根節點的右指標指向新節點
this.right = newNode;
}
@Override
public String toString() {
return "AVLNode{" +
"value=" + value +
'}';
}
}