樹篇2-平衡二叉查詢樹之AVL樹
一、AVL樹定義
在資料結構中,AVL樹是最先發明的自平衡二叉查詢樹。在AVL樹中任何節點的兩個子樹的高度差的絕對值不能超過一,所以它也被稱為高度平衡樹。查詢、插入和刪除在平均和最壞情況下都是O(log n)。增加和刪除可能需要通過一次或多次旋轉來使得AVL樹保持平衡。
二、AVL樹的特性
1.任意一個根節點的左孩子小於根節點,右孩子大於根節點。
2.每個結點的左右子樹的高度之差的絕對值不能超過1。
三、如何處理插入節點或刪除節點引起的不平衡問題
在寫這篇部落格之前,看了許多關於AVL樹的資料。有的將處理不平衡問題歸納為左左、左右、右左、右右處理不平衡問題,在此基礎上又進行劃分,看的我頭暈目眩。經過大量的查詢,一種最簡單有效、好理解、容易接受的方法是將上面的四種情況歸納為兩種,與右有關的旋轉統一歸納到順時針旋轉,與左有關的旋轉統一歸納到逆時針旋轉。
四、插入操作
AVL樹的插入操作首先會按照普通搜尋二叉樹的插入操作進行,當插入一個數據後,我們會沿著插入資料時所經過的的節點回溯,回溯的過程中會判回溯路徑中的每個節點的左子支高度與右子支高度之差的絕對值是否超過1,如果超過1我們就進行調整,調整的目的是使得該節點滿足AVL樹的定義。調整的情況可以分為以下四旋轉操作,旋轉操作可以降低樹的高度,同時不改變搜尋二叉樹的性質(即任何一個節點左子支中的全部節點小於該節點,右子支的全部節點大於該節點)。
五、節點不平衡解決策略
情況1 左左(LL)
節點N左子支比右子支高度大2,且插入的節點位於N的左孩子節點NL的左子支上
情況2 右右(RR)
節點N右子支比左子支高度大2,且插入的節點位於節點N右孩子節點NR的右子支上
情況3 左右(LR)
節點N左子支比右子支高度大2,且插入的節點位於節點N左孩子節點NL的右子支上
情況4 右左(RL)
節點N左子支比右子支高度大2,且插入的節點位於節點N左孩子節點NL的右子支上
六、AVL刪除操作
AVL樹的刪除操作和插入操作一樣,首先會按照普通搜尋二叉樹的刪除操作進行,當刪除一個數據後,和插入操作一樣,我們通常採取的策略是沿著刪除資料時所經過的的節點回溯,回溯的過程中會判斷該節點的左子支高度與右子支高度之差的絕對值是否超過1(或者說大2),如果超過1,我們就進行調整,調整的目的是使得該節點滿足AVL樹的定義。調整的情況可以分為四種,和插入過程完全一樣,這裡不在贅述。
七、完整程式碼實現
import com.sun.org.apache.regexp.internal.REUtil;
import java.util.Comparator;
/**
* AVL樹:自平衡二叉搜尋樹
*/
public class AVLTree<E> {
/**
* AVL樹節點
*/
private class AVLTreeNode<E> {
int height; // 節點高度
E element; // 值域
AVLTreeNode<E> parent; // 父節點
AVLTreeNode<E> leftChild; // 左孩子
AVLTreeNode<E> rightChild; // 右孩子
// 構造方法
public AVLTreeNode(int height, E element, AVLTreeNode<E> parent, AVLTreeNode<E> leftChild, AVLTreeNode<E> rightChild) {
this.height = height;
this.element = element;
this.parent = parent;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
@Override
public String toString() {
return "AVLTreeNode{" +
"height=" + height +
", element=" + element +
'}';
}
}
//建立一個偽根節點,該節點的右子支才是真正的AVL樹的根
//使用偽根節點節點的目的是,對插入和刪除操作遞迴的形式能夠統一
private AVLTreeNode<E> rootIndex; // 指向根節點的偽根節點(目的:方面操作)
private int size; // 節點個數
private Comparator<? super E> comparator; //節點大小比較器
/**
* 預設比較器
*/
private class Cmp<T> implements Comparator<T> {
@Override
public int compare(T e1, T e2) {
return ((Comparable)e1).compareTo(e2);
}
}
// 不帶引數的構造方法
public AVLTree() {
this.comparator = new Cmp<>();
this.rootIndex = new AVLTreeNode<>(-1, null, null, null, null);
}
// 帶比較器構造方法
public AVLTree(Comparator<? super E> comparator) {
if (comparator == null) // 如果比較器為空,丟擲非法引數異常
throw new IllegalArgumentException();
this.comparator = comparator;
this.rootIndex = new AVLTreeNode<>(-1, null, null, null, null);
}
// 返回元素個數
public int getSize() {
return size;
}
// 插入元素
public void insert(E e) {
if (e == null)
throw new IllegalArgumentException("插入節點不能為空!");
insert(rootIndex.rightChild, e);
}
// 插入節點內部方法
private void insert(AVLTreeNode<E> node, E e) {
// 這個if,在整個生命週期中只執行一次
if (node == null) { //偽根節點的右孩子為空,即真實的根節點為空。將插入的節點設定為根節點
rootIndex.rightChild = new AVLTreeNode<>(1, e, rootIndex, null, null);
size++;
return;
}
if (comparator.compare(e, node.element) < 0) { // 待插入元素小於當前節點
if (node.leftChild != null) {
insert(node.leftChild, e);
int leftHeight = getNodeHeight(node.leftChild);
int rightHeight = getNodeHeight(node.rightChild);
if (leftHeight - rightHeight == 2) {
if (comparator.compare(e, node.leftChild.element) < 0) {
clockwiseRotate(node);
} else {
antiClockwiseRotate(node.leftChild);
clockwiseRotate(node);
}
}
} else { // 建立節點,將新節點作為node節點左孩子
size++;
node.leftChild = new AVLTreeNode<>(1, e, node, null, null);
}
} else if (comparator.compare(e, node.element) > 0) { // 待插入元素大於當前節點
if (node.rightChild != null) {
insert(node.rightChild, e);
int leftHeight = getNodeHeight(node.leftChild);
int rightHeight = getNodeHeight(node.rightChild);
if (rightHeight - leftHeight == 2) {
if (comparator.compare(e, node.rightChild.element) > 0) {
antiClockwiseRotate(node);
} else {
clockwiseRotate(node.rightChild);
antiClockwiseRotate(node);
}
}
} else {
size++;
node.rightChild = new AVLTreeNode<>(1, e, node, null, null);
}
} else if (comparator.compare(e, node.element) == 0){ // 待插入元素等於當前節點
//元素已存在,我們用新的元素更新舊
node.element = e;
}
node.height = Math.max(getNodeHeight(node.leftChild), getNodeHeight(node.rightChild)) + 1;
}
/**
* 順時針旋轉(右旋)
*
* @param node 表示要旋轉的軸節點
*/
private void clockwiseRotate(AVLTreeNode<E> node) {
AVLTreeNode<E> nodeParent = node.parent; // 軸節點的父節點
AVLTreeNode<E> nodeLeftChild = node.leftChild; // 軸節點的左孩子
// 第一步:父節點不動,順時針旋轉軸節點;
// 將老軸節點的左孩子作為新的軸節點,並將新軸節點的父親指向老軸節點的父親
if (nodeParent.leftChild == node) // node節點為父節點左孩子,則將旋轉後的軸節點(nodeLeftChild)作為父節點左孩子
nodeParent.leftChild = nodeLeftChild; // 將node的左孩子作為node父節點左孩子
else // node節點不是父節點左孩子,則將旋轉後的軸節點(nodeLeftChild)作為父節點右孩子
nodeParent.rightChild = nodeLeftChild; // 將node的左孩子作為node父節點的右孩子
nodeLeftChild.parent = nodeParent; // 將node的左孩子父節點指標指向新的父節點
// 第二步:將新軸節點的右孩子過繼給老軸節點作為它的左孩子
node.leftChild = nodeLeftChild.rightChild; // node左孩子的右孩子過繼給node作為左孩子
if (nodeLeftChild.rightChild != null) // 如果node左孩子的右孩子存在,將它的父節點指向node
nodeLeftChild.rightChild.parent = node;
// 第三步:將新軸節點指向老軸節點
nodeLeftChild.rightChild = node; // 將新的軸節點的右孩子指向老軸節點
node.parent = nodeLeftChild; // 將老軸節點的父親指向新軸節點
// 旋轉後要更新被替代的老軸節點的高度和新軸節點的高度(計算規則:其節點左右子樹的最大高度+1)
node.height = Math.max(getNodeHeight(node.leftChild), getNodeHeight(node.rightChild)) + 1;
nodeLeftChild.height = Math.max(getNodeHeight(nodeLeftChild.leftChild), getNodeHeight(nodeLeftChild.rightChild)) + 1;
}
/**
* 逆時針旋轉(左旋)
*
* @param node 表示要旋轉的軸節點
*/
private void antiClockwiseRotate(AVLTreeNode<E> node) {
AVLTreeNode<E> nodeParent = node.parent; // 老軸節點父節點
AVLTreeNode<E> nodeRightChild = node.rightChild; // 老軸節點的右孩子,即要替代老軸節點的新軸節點
// 第一步:父節點不動,逆時針旋轉軸節點;
// 將老軸節點的右孩子作為新的軸節點,並將新軸節點的父親指向老軸節點的父親
if (nodeParent.leftChild == node) { // 如果老軸節點是其父親的左孩子,將新軸節點也作為它的左孩子
nodeParent.leftChild = nodeRightChild;
} else
nodeParent.rightChild = nodeRightChild;
nodeRightChild.parent = nodeParent;
// 第二步:將新軸節點的左孩子過繼給老軸節點作為它的右孩子
node.rightChild = nodeRightChild.leftChild; // node右孩子的左孩子過繼給node作為右孩子
if (nodeRightChild.leftChild != null) // 如果node右孩子的左孩子存在,將它的父節點指向node
nodeRightChild.leftChild.parent = node;
// 第三步:將新軸節點指向老軸節點
nodeRightChild.leftChild = node; // 將新的軸節點的左孩子指向老軸節點
node.parent = nodeRightChild; // 將老軸節點的父親指向新軸節點
// 旋轉後要更新被替代的老軸節點的高度和新軸節點的高度(計算規則:其節點左右子樹的最大高度+1)
node.height = Math.max(getNodeHeight(node.leftChild), getNodeHeight(node.rightChild)) + 1;
nodeRightChild.height = Math.max(getNodeHeight(nodeRightChild.leftChild), getNodeHeight(nodeRightChild.rightChild)) + 1;
}
// 返回節點的高度
private int getNodeHeight(AVLTreeNode<E> node) {
if (node == null)
return 0;
else
return node.height;
}
// 逐層遍歷
public void orderTraverse() {
if (rootIndex != null)
orderTraverse(rootIndex.rightChild);
}
private void orderTraverse(AVLTreeNode<E> node) {
if (node != null) {
System.out.print(node.element + "-" + node.height + "\t\t");
if (node.leftChild != null)
System.out.print( "(L)" + node.leftChild.element + "\t\t");
else
System.out.print("(L)Null\t\t");
if (node.rightChild != null)
System.out.print("(R)" + node.rightChild.element + "\t\t");
else
System.out.print("(R)Null\t\t");
System.out.println();
orderTraverse(node.leftChild);
orderTraverse(node.rightChild);
}
}
// 移除指定節點
public boolean removeNode(E e) {
return removeNode(rootIndex.rightChild, e);
}
private boolean removeNode(AVLTreeNode<E> node, E e) {
if (node == null) // 沒有找到待刪除節點(遞迴結束條件)
return false;
if (comparator.compare(e, node.element) < 0) { // 待刪除元素小於當前節點
boolean flog = removeNode(node.leftChild, e); // 向左節點繼續找,繼續遞迴
if (flog == false) // 沒找到結束遞迴返回
return false;
// 調整樹,使它滿足AVL樹條件
int leftHeight = getNodeHeight(node.leftChild);
int rightHeight = getNodeHeight(node.rightChild);
if (rightHeight - leftHeight == 2) { // 高度差為2表示此節點為失衡節點,需要旋轉平衡
// 失衡節點node,由於node節點右子樹元素過多導致失衡,調節右子樹,以完成平衡化處理
// 大於代表node的右子樹的右子樹節點深度大導致的失衡,直接逆時針旋轉即可解決
// 小於代表node的右子樹的左子樹節點深度大導致的失衡,先以node的右子節點順時針旋轉,再以node逆時針旋轉
if (getNodeHeight(node.rightChild.rightChild) > getNodeHeight(node.rightChild.leftChild)) {
antiClockwiseRotate(node);
} else {
clockwiseRotate(node.rightChild);
antiClockwiseRotate(node);
}
}
} else if (comparator.compare(e, node.element) > 0) { // 待刪除元素大於於當前節點
boolean flog = removeNode(node.rightChild, e); // 向右節點繼續找,繼續遞迴
if (flog == false) // 沒找到結束遞迴返回
return false;
// 調整樹,使它滿足AVL樹條件
int leftHeight = getNodeHeight(node.leftChild);
int rightHeight = getNodeHeight(node.rightChild);
if (leftHeight - rightHeight == 2) {
if (getNodeHeight(node.leftChild.leftChild) > getNodeHeight(node.leftChild.rightChild)) {
clockwiseRotate(node);
} else {
antiClockwiseRotate(node.leftChild);
clockwiseRotate(node);
}
}
} else { // 待刪除節點等於當前節點(找到要刪除的節點)
AVLTreeNode<E> nodeParent = node.parent;
// 分情況討論:
// 1.左子支為空,可直接刪除,在這一層一定不需要旋轉
if (node.leftChild == null) {
size--;
if (nodeParent.leftChild == node) {
nodeParent.leftChild = node.rightChild;
if (nodeParent.leftChild != null)
nodeParent.leftChild.parent = nodeParent;
} else {
nodeParent.rightChild = node.rightChild;
if (nodeParent.rightChild != null)
nodeParent.rightChild.parent = nodeParent;
}
}
// 2.右子支為空,可直接刪除,在這一層一定不需要旋轉
else if (node.rightChild == null) {
size--;
if (nodeParent.leftChild == node) {
nodeParent.leftChild = node.leftChild;
if (nodeParent.leftChild != null)
nodeParent.leftChild.parent = nodeParent;
} else {
nodeParent.rightChild = node.leftChild;
if (nodeParent.rightChild != null)
nodeParent.rightChild.parent = nodeParent;
}
}
// 3.左右子支都存在,找到待刪除的節點,用後繼節點代替,然後刪除後繼節點
// 刪除後繼節點策略:找到待刪除節點的後繼節點,將後繼節點的值替代刪除節點的值,以達到刪除該值的目的
// 隨後,再使用removeNode方法移除後繼節點。這種策略很簡單,比我上一篇部落格中尋找節點的方式簡單多了。
else {
E nextNodeElement = treeMinNode(node.rightChild); // 找node節點的後繼節點
node.element = nextNodeElement; // 將後繼節點值替代刪除的節點值
removeNode(node.rightChild, nextNodeElement); // 刪除後繼節點
int leftHeight = getNodeHeight(node.leftChild);
int rightHeight = getNodeHeight(node.rightChild);
if (leftHeight - rightHeight == 2) {
if (getNodeHeight(node.leftChild.leftChild) > getNodeHeight(node.leftChild.rightChild)) {
clockwiseRotate(node);
} else {
antiClockwiseRotate(node.leftChild);
clockwiseRotate(node);
}
}
}
}
// 更新節點的高度
node.height = Math.max(getNodeHeight(node.leftChild), getNodeHeight(node.rightChild)) + 1;
return true;
}
// 求某個節點作為根時,該子樹的最小值
private E treeMinNode(AVLTreeNode<E> node) {
while (node.leftChild != null)
node = node.leftChild;
return node.element;
}
}
測試程式碼:
public class AVLTreeDemo {
public static void main(String[] args) {
AVLTree<Integer> tree = new AVLTree<>();
tree.insert(150);
tree.insert(90);
tree.insert(250);
tree.insert(200);
tree.insert(300);
tree.insert(210);
tree.insert(205);
tree.insert(80);
tree.orderTraverse();
System.out.println("Number of element in the tree: " + tree.getSize());
System.out.println();
tree.removeNode(150);
tree.orderTraverse();
System.out.println("Number of element in the tree: " + tree.getSize());
System.out.println();
tree.removeNode(200);
tree.removeNode(250);
tree.orderTraverse();
System.out.println("Number of element in the tree: " + tree.getSize());
System.out.println();
}
}
執行結果:
200-4 (L)90 (R)250
90-2 (L)80 (R)150
80-1 (L)Null (R)Null
150-1 (L)Null (R)Null
250-3 (L)210 (R)300
210-2 (L)205 (R)Null
205-1 (L)Null (R)Null
300-1 (L)Null (R)Null
Number of element in the tree: 8
200-4 (L)90 (R)250
90-2 (L)80 (R)Null
80-1 (L)Null (R)Null
250-3 (L)210 (R)300
210-2 (L)205 (R)Null
205-1 (L)Null (R)Null
300-1 (L)Null (R)Null
Number of element in the tree: 7
205-3 (L)90 (R)300
90-2 (L)80 (R)Null
80-1 (L)Null (R)Null
300-2 (L)210 (R)Null
210-1 (L)Null (R)Null
Number of element in the tree: 5