資料結構與演算法(十六):平衡二叉樹
一、什麼是平衡二叉樹
1.概述
平衡二叉樹(AVL樹)是一種帶有平衡條件的二叉搜尋樹。它的特性如下:
- AVL樹的左右兩個子樹的高度差的絕對值不超過1
- AVL樹的左右兩個子樹都是一棵平衡二叉樹
舉個例子,如上圖所示:
- 第一棵樹左樹高2,右樹高1,差值為1,是一顆AVL樹;
- 第二棵樹左樹高2,右樹高2,差值為0,是一顆AVL樹;
- 第三棵樹左樹高3,右樹高1,差值為2,不是一顆AVL樹;
紅黑樹就是一直AVL樹。
2.為什麼需要平衡二叉樹
當我們使用二叉排序樹的時候,當連續插入順序的節點的時候就會出現問題。比如,我們插入{1,2,3,4,5}這樣一個數組:
可見該樹左樹節點全為空,比起樹更像單鏈表,這也導致了該樹的插入和查詢速度明顯的下降,查詢速度甚至因為每次多處一個比較左樹的操作導致還不如單鏈表。為了避免這種情況,我們引入的AVL樹。
二、AVL樹左旋轉
1.思路分析
AVL為了避免左右樹高度差超過1,在可能導致這種情況的插入或者刪除操作時會進行旋轉。
我們舉個例子,現在有數列{4,3,6,5,7},當插入8後,現在的得到的排序樹如下圖:
明顯不再是一個AVL樹,所以需要進行左旋轉:
-
我們以當前根節點值再建立一個新節點
newNode
-
讓新節點的左子節點指向根節點的左子節點
newNode.left = root.left
-
讓新節點的右子節點指向根節點的右子節點的左子節點
newNode.right = root.right.left
-
把根節點的值換成右子節點的值
root.val = root.right.val
-
把根節點的右子節點指向其右子節點的右子節點
root.right = root.right.right
-
讓根節點的左子節點指向新節點(根節點的右子節點成為了新的根節點)
root.left = newNode
我們調整一下圖片樣式,就可以直觀的看到左旋轉後樹的樣子:
網上看到一個非常形象直觀的動圖:
不難理解:左旋的目的是降低左子樹的高度
2.程式碼實現
由於AVL樹是基於BST改進的一種資料結構,所以這裡的AVL樹類繼承了BST的方法和程式碼,使用同一個節點類,這裡具體的程式碼可以參考之前的文章。
我們先建立一個繼承BST的AVL樹類:
/** * @Author:CreateSequence * @Date:2020-07-23 19:01 * @Description:平衡二叉樹 * 由於是在二叉排序樹的基礎上改進,這裡直接繼承了二叉排序樹類 */ public class AVLTree extends BinarySortTree{ public AVLTree(BinarySortTreeNode root) { super(root); } }
由於旋轉的條件是左右子樹高度差大於1,所以我們需要有幾個方法來判斷樹的高度:
/**
* 獲取當前節點的右子樹高度
* @param node
* @return
*/
public int getRightHeight(BinarySortTreeNode node) {
if (node.right == null) {
return 0;
}
return getHeight(node.right);
}
/**
* 獲取當前節點的左子樹高度
* @param node
* @return
*/
public int getLeftHeight(BinarySortTreeNode node){
if (node.left == null) {
return 0;
}
return getHeight(node.left);
}
/**
* 獲取以當前節點為根節點的樹高度
* @param node
* @return
*/
public int getHeight(BinarySortTreeNode node) {
//判斷當前節點的左/右節點是否為空,是返回0,否則遍歷返回當前節點的左右樹最高值
return Math.max(node.left == null ? 0 : getHeight(node.left), node.right == null ? 0 : getHeight(node.right)) + 1;
}
接著我們需要一個讓樹左旋的程式碼,步驟同思路分析:
/**
* 排序樹左旋轉
*/
private void leftRotate() {
// 1.建立新節點,與根節點值相同
BinarySortTreeNode node = new BinarySortTreeNode(root.val);
//2.讓新節點左子節點指向根節點左子節點
node.left = root.left;
//3.讓新節點的右子節點指向根節點的右子節點的左子節點
node.right = root.right.left;
//4.讓根節點的值變為其右子節點的值
root.val = root.right.val;
//5.把根節點的右子節點指向其右子節點的右子節點
root.right = root.right.right;
//6.讓根節點的左子節點指向新節點
root.left = node;
}
然後我們再原先舊的新增方法上進行改進:
當新增完一個節點後,我們判斷左右子樹的高度差是否大於1,如果是就進行左旋
/**
* 重寫二叉排序樹的節點新增方法,當新增完節點後左子樹與右子樹高度差大於1時,讓樹進行左旋轉,若情況相反則進行右旋轉
* @param node
*/
@Override
public void add(BinarySortTreeNode node) {
super.add(node);
//新增完節點後,判斷左子樹與右子樹高度差是否大於1
int disparity = getRightHeight(root) - getLeftHeight(root);
if (disparity > 1) {
System.out.println("高度差:" + disparity + ",左旋轉!");
//左子樹與右子樹高度差大於1就左旋
leftRotate();
}
}
注意:截止目前,僅僅只對左子樹高度較高的情況作了處理!
三、AVL樹的雙旋轉
左旋轉是為了降低左子樹的高度,但是如果是右子樹高度過高,我們就需要右旋,事實上,一個完整的AVL樹,應當是能夠雙旋的。
右旋的步驟與左旋基本一致,但是方向不同:
-
我們以當前根節點值再建立一個新節點
newNode
-
讓新節點的右子節點指向根節點的右子節點
newNode.right = root.right
-
讓新節點的左子節點指向根節點的左子節點的右子節點
newNode.left = root.left.right
-
把根節點的值換成左子節點的值
root.val = root.left.val
-
把根節點的左子節點指向其左子節點的左子節點
root.left = root.left.left
-
讓根節點的右子節點指向新節點(根節點的左子節點成為了新的根節點)
root.right = newNode
實現程式碼:
/**
* 排序樹右旋轉
*/
private void rightRotate() {
// 1.建立新節點,與根節點值相同
BinarySortTreeNode node = new BinarySortTreeNode(root.val);
//2.讓新節點右子節點指向根節點右子節點
node.right = root.right;
//3.讓新節點的左子節點指向根節點的左子節點的右子節點
node.left = root.left.right;
//4.讓根節點的值變為其左子節點的值
root.val = root.left.val;
//5.把根節點的左子節點指向其左子節點的左子節點
root.left = root.left.left;
//6.讓根節點的右子節點指向新節點
root.right = node;
}
現在為排序樹的add方法新增右旋的情況:
/**
* 重寫二叉排序樹的節點新增方法,當新增完節點後左子樹與右子樹高度差大於1時,讓樹進行左旋轉,若情況相反則進行右旋轉
* @param node
*/
@Override
public void add(BinarySortTreeNode node) {
super.add(node);
//新增完節點後,判斷左右樹高度差是否大於1
int disparity = getRightHeight(root) - getLeftHeight(root);
if (disparity > 1) {
System.out.println("高度差:" + disparity + ",左旋轉!");
//左子樹與右子樹高度差大於1就左旋
leftRotate();
}else if (- disparity > 1){
//右子樹與左子樹高度差小於1就左旋
rightRotate();
}
}