AVL定義:
AVL的命名是由2個其發明者的名字組成的,G.M.Adelson-Velsky和E.M.Landis,兩個俄羅斯人。AVL既有平衡二叉樹的特性,又有二分搜索樹的特性。
平衡二叉樹:對于任意一個節點,左子樹和右子樹的高度差不能超過1。
二分搜索樹:
1、二分搜索樹的每個節點的值,大于其左子樹的所有節點的值;小于其右子樹的所有節點的值;
2、每一顆子樹也是二分搜索樹。
AVL結構:
因為AVL是二分搜索樹,所以可以直接使用其結構,這里泛型使用了映射(Map),是為了符合更多的數據類型。然后新增了變量height,來表示當前節點的高度,其中葉子節點的高度為1。如下圖:
public class AVLTree<K extends Comparable<K>, V>{
private class Node{
//當前節點的值
public K key;
public V value;
//左節點
public Node left;
//右節點
public Node right;
// 節點高度,葉子結點為1
public int height;
public Node(K key, V value){
this.key = key;
this.value = value;
left = null;
right = null;
height = 1;
}
}
private Node root;
private int size;
獲取節點的高度和平衡因子:
平衡因子:左孩子的高度減去右孩子的高度。
1、獲取當前節點的高度:
// 獲取節點node的高度
private int getHeight(Node node){
if (node == null) return 0;
return node.height;
}
2、獲取當前節點的平衡因子:
// 獲取節點node的平衡因子,平衡因子不在[-1,1]區間,說明不是平衡二叉樹
private int getBalanceFactory(Node node){
if (node == null) return 0;
return getHeight(node.left) - getHeight(node.right);
}
判斷該二叉樹是否是一顆二分搜索樹:
思路:因為二分搜索樹的中序遍歷會使元素自然排序,于是我們可以利用這個特性。對當前的二叉樹進行中序遍歷,把值存入一個集合List中,然后遍歷這個List,看前一個元素是否小于后一個元素,如果有一個不滿足返回false,直到全部元素遍歷完才返回true。
// 判斷該二叉樹是否是一顆二分搜索樹
public boolean isBST(){
// 二分搜索樹的一個特性:中序遍歷的結果是自然排序的
ArrayList<K> keys = new ArrayList<>();
inOrder(root, keys);
for (int i = 1; i < keys.size(); i++) {
if (keys.get(i - 1).compareTo(keys.get(i)) > 0)
return false;
}
return true;
}
// 中序遍歷
private void inOrder(Node node, ArrayList<K> keys) {
if (node == null) return;
inOrder(node.left, keys);
keys.add(node.key);
inOrder(node.right, keys);
}
判斷該二叉樹是否是一顆平衡二叉樹:
思路:遍歷判斷當前節點的平衡因子balanceFactory是否 > 1,遞歸的終止條件為:1、執行到最后一個NULL元素,此時平衡因子為0,返回true;2、平衡因子>1,返回false。遞歸執行,只有左右孩子同時滿足平衡二叉樹的特性時才為true。
// 判斷該二叉樹是否是一顆平衡二叉樹,看平衡因子
public boolean isBalanced(){
return isBalanced(root);
}
// 判斷node為根的二叉樹是否是一顆平衡二叉樹,遞歸算法
private boolean isBalanced(Node node) {
if (node == null) return true;
int balanceFactory = getBalanceFactory(node);
if (Math.abs(balanceFactory) > 1)
return false;
// 左右子樹遞歸都為true才行
return isBalanced(node.left) && isBalanced(node.right);
}
LL與AVL的右旋轉:
在執行了添加元素或者刪除元素之后,由于會對節點高度進行修改,很可能破壞AVL的平衡性。于是需要左旋轉或者右旋轉操作來使AVL重新獲得平衡。
右旋轉的時機:插入的元素在不平衡節點的左側的左側(LL)。
我們先看右旋轉,如下圖:
首先從新增或者刪除的元素往上,找到第一個平衡因子=2的節點,如上圖的 y,然后取出 x 的右子樹 T3,然后使 y 的左子樹 x 的右孩子指向 y,然后讓 y 的左子樹指向 T3。此時 x 成為了新的AVL的根節點,并且新的AVL即符合平衡二叉樹的特性又符合二分搜索樹的特性。
右旋轉代碼實現:
// 對節點y進行向右旋轉操作,返回旋轉后新的根節點x
// y x
// / \ / \
// x T4 向右旋轉 (y) z y
// / \ - - - - - - - -> / \ / \
// z T3 T1 T2 T3 T4
// / \
// T1 T2
private Node rightRotate(Node y){
// 取出y 的左孩子x的右孩子T3
Node x = y.left;
Node T3 = x.right;
x.right = y;
y.left = T3;
// 更新height
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
RR與AVL的左旋轉:
左旋轉和右旋轉剛好相反,于是實現機制是一樣的。
左旋轉的時機: 插入的元素在不平衡節點的右側的右側(RR)。
左旋轉的代碼實現:
// 對節點y進行向左旋轉操作,返回旋轉后新的根節點x
// y x
// / \ / \
// T4 x 向左旋轉 (y) y z
// / \ - - - - - - - -> / \ / \
// T3 z T4 T3 T1 T2
// / \
// T1 T2
private Node leftRotate(Node y){
// 取出y 的右孩子x的左孩子T3
Node x = y.right;
Node T3 = x.left;
x.left = y;
y.right = T3;
// 更新height
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
LR:
插入的元素在不平衡節點的左側的右側(LR)。
此時則需要讓不平衡節點 y 的左孩子 x 進行左旋轉,然后讓不平衡節點 y 的左子樹指向左旋轉之后的新的二叉樹的根節點 z。如下圖:
此時,已經儼然成為了LL的情況,于是 y 節點右旋轉就解決了啦。
RL:
插入的元素在不平衡節點的右側的左側(RL)。
此時則需要讓不平衡節點 y 的右孩子 x 進行右旋轉,然后讓不平衡節點 y 的右子樹指向右旋轉之后的新的二叉樹的根節點 z。如下圖:
此時,已經儼然成為了RR的情況,于是 y 節點左旋轉就解決了啦。
添加元素:
遞歸操作,遞歸的終止條件:當前節點為NULL,表示到達最后一個根節點NULL,則將新節點放入該位置。然后比較所給的key與當前node節點的key的大小,如果小于,則新增的節點放在左子樹,于是往左子樹遞歸;反之,往右子樹遞歸;當二者相等時,則更新value值。然后更新node的height,計算node的平衡因子,如果平衡因子的絕對值 <= 1,則表示符合AVL特性,直接返回node;否則,就是已經破壞了AVL的平衡性,需要根據不同情況來進行左右旋轉。
public void add(K key, V value) {
root = add(root, key, value);
}
// 向 node 為根的二分搜索樹中添加新的元素(key, value)
// 返回插入新節點后的二分搜索樹的根
private Node add(Node node, K key, V value) {
//遞歸終止條件
if (node == null) {
//表示到達最后一個根節點null,則將新節點放入該位置
size++;
return new Node(key, value);
}
if (key.compareTo(node.key) < 0){
//遞歸步驟,往往遞歸終止條件的參數是和遞歸步驟的參數對應的
//將插入新節點后的二分搜索樹的根掛在當前樹上
node.left = add(node.left, key, value);
}else if(key.compareTo(node.key) > 0){
node.right = add(node.right, key, value);
}else {
// 更新value值
node.value = value;
}
// 更新 height,左右孩子中大的height + 1
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
// 計算平衡因子
int balanceFactory = getBalanceFactory(node);
if (Math.abs(balanceFactory) <= 1) return node;
// 不符合平衡二叉樹條件,需要平衡維護
if (balanceFactory > 1){
if (getBalanceFactory(node.left) >= 0){
// 插入的元素在不平衡節點的左側的左側(LL),采用右旋轉
return rightRotate(node);
}else{
// 插入的元素在不平衡節點的左側的右側(LR),采用先左旋轉再右旋轉
node.left = leftRotate(node.left);
return rightRotate(node);
}
}
if (balanceFactory < -1){
if (getBalanceFactory(node.right) <= 0){
// 插入的元素在不平衡節點的右側的右側(RR),采用左旋轉
return leftRotate(node);
}else {
// 插入的元素在不平衡節點的右側的左側(RL),采用右旋轉再左旋轉
node.right = rightRotate(node.right);
return leftRotate(node);
}
}
return node;
}
刪除元素:
思路:依舊是遞歸執行,遞歸的終止條件:1、走到最后沒有找到 key;2、找到key,即node.key.compareTo(key) == 0,此時也要分情況考慮,當左子樹為空,則把右子樹的根節點作為新二叉樹的根節點,然后將node的右子樹置為NULL,讓GC回收;同理,右子樹為空時,則把左子樹的根節點作為新二叉樹的根節點,然后將node的左子樹置為NULL;當左右子樹都不為空時,則找出右子樹的最小值 successor 用來頂替待刪除節點的位置(后繼策略)。最后還是需要對上述操作之后返回的retNode進行平衡因子的判斷,如果失去了平衡性,則需要根據不同情況進行左右旋轉。
public V remove(K key) {
Node node = getNode(root, key);
if (node != null) {
root = remove(root, key);
return node.value;
}
return null;
}
// 刪除以node為根的二分搜索樹中的鍵為key的節點
// 返回刪除節點后的新的二分搜索樹
private Node remove(Node node, K key) {
// 遞歸終止條件: 1、走到最后沒有找到key ; 2、找到key
if (node == null) return node;
Node retNode;
if (node.key.compareTo(key) == 0){
if (node.left == null){
// 待刪除的節點左子樹為空的情況
Node rightNode = node.right;
node.right = null;
size--;
retNode = rightNode;
} else if (node.right == null){
// 待刪除的節點右子樹為空的情況
Node leftNode = node.left;
node.left = null;
size--;
retNode = leftNode;
}else {
// 左右子樹都不為空,則找出右子樹的最小值,即采用e 的后繼。然后用這個節點頂替待刪除節點的位置
Node successor = minimum(node.right);
//由于removeMin 并未對刪除元素之后進行平衡檢查,所以要么加上,要不不用;
// 我們這里采用不用,直接使用remove,因為successor就是node.right子樹的最小值
// successor.right = removeMin(node.right);
successor.right = remove(node.right, successor.key);
successor.left = node.left;
node.left = null;
node.right = null;
//注意:這里不需要size--,因為removeMin(node.right) 已經操作了size--
retNode = successor;
}
}else if (key.compareTo(node.key) < 0){
node.left = remove(node.left, key);
retNode = node;
}else {
node.right = remove(node.right, key);
retNode = node;
}
if (retNode == null) return retNode;
// 更新 height,左右孩子中大的height + 1
retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
// 計算平衡因子
int balanceFactory = getBalanceFactory(retNode);
// 不符合平衡二叉樹條件,需要平衡維護
if (balanceFactory > 1){
if (getBalanceFactory(retNode.left) >= 0){
// 插入的元素在不平衡節點的左側的左側(LL),采用右旋轉
return rightRotate(retNode);
}else{
// 插入的元素在不平衡節點的左側的右側(LR),采用先左旋轉再右旋轉
retNode.left = leftRotate(retNode.left);
return rightRotate(retNode);
}
}
if (balanceFactory < -1){
if (getBalanceFactory(retNode.right) <= 0){
// 插入的元素在不平衡節點的右側的右側(RR),采用左旋轉
return leftRotate(retNode);
}else {
// 插入的元素在不平衡節點的右側的左側(RL),采用右旋轉再左旋轉
retNode.right = rightRotate(retNode.right);
return leftRotate(retNode);
}
}
return retNode;
}
小結:
至此,AVL的基本操作已經介紹完了,由于它不會像二分搜索樹那樣退化成鏈表,所以它的添加元素和刪除元素的時間復雜度都是O(log n)。