理解 AVL 树

理解 AVL 树

参考资料:强烈推荐通过 visualgo 中的例子来感受 AVL 树在各种操作下的再平衡过程。二叉树的再平衡的过程非常解压
树结构的代码天然适合递归的写法。

基本了解

首先我们要了解二叉搜索树(BST, Binary Search Tree)

public class AVLTreeNode {  
    int value;  
    AVLTreeNode left, right;  
}

其实乍一看,树跟链表这种结构起始没什么区别,在链表中,只要有链表的头节点,我们就能最对这个链表进行增删改查,
二叉树也是如此,我们只要知道树的根节点就可以遍历整个树,
但是为了实现 左子树所有节点值小于当前节点值,右子树所有节点值大于当前节点值。和 左右子树高度差不超过 1,我们要做很多操作。

AVL 树的代码

AVL 树节点 MyAVLTreeNode,这里为了方便打印其结构,实现了 BTPrinterAdapter,具体请看 在控制台输出二叉树的结构

 public class MyAVLTreeNode<K extends Comparable<K>, V> implements BTPrinterAdapter<MyAVLTreeNode<K, V>> {  
  
    public K key;  
    public V val;  
    // 用于评估平衡度,每次都通过递归的方式去算太耗时,直接用一个字段保存,然后每一级递归的时候更新会快很多  
    public int height;  
    public MyAVLTreeNode<K, V> left, right;  
  
    public MyAVLTreeNode(K key, V val) {  
        this.key = key;  
        this.val = val;  
        this.height = 1;  
        this.left = null;  
        this.right = null;  
    }  
  
    @Override  
    public String getNodeString() {  
        return key.toString() + ":" + val.toString();  
    }  
  
    @Override  
    public MyAVLTreeNode<K, V> getLeftNode() {  
        return left;  
    }  
  
    @Override  
    public MyAVLTreeNode<K, V> getRightNode() {  
        return right;  
    }  
}

AVL 树本身 MyAVLTree

public class MyAVLTree<K extends Comparable<K>, V> {  
  
    private MyAVLTreeNode<K, V> root;  
  
    public MyAVLTreeNode<K, V> search(K key) {  
        if (root == null) {  
            return null;  
        }  
        MyAVLTreeNode<K, V> target = null;  
        MyAVLTreeNode<K, V> nowRoot = root;  
        // 其实也可以写成递归,但是没必要,就用 for 循环就够了  
        while (nowRoot != null) {  
            int flag = key.compareTo(nowRoot.key);  
            if (flag == 0) {  
                target = nowRoot;  
                break;  
            }else if (flag < 0) {  
                // 从左子树往下找  
                nowRoot = nowRoot.left;  
            } else if (key.compareTo(nowRoot.key) > 0) {  
                // 从右子树往下找  
                nowRoot = nowRoot.right;  
            }  
        }  
        return target;  
    }  
  
  
    public MyAVLTreeNode<K, V> insert(K key, V value) {  
        if (key != null && value != null) {  
            // 需要更新根节点  
            root = insert(root, key, value);  
        }  
        return root;  
    }  
  
  
    public MyAVLTreeNode<K, V> insert(MyAVLTreeNode<K, V> nowRoot, K key, V value) {  
        if (nowRoot == null) {  
            return new MyAVLTreeNode(key, value);  
        }  
  
        int compare = key.compareTo(nowRoot.key);  
        if (compare < 0) {  
            nowRoot.left = insert(nowRoot.left, key, value);  
        } else if (compare > 0) {  
            nowRoot.right = insert(nowRoot.right, key, value);  
        } else {  
            nowRoot.val = value;  
            return nowRoot;  
        }  
  
        // 更新树高  
        nowRoot.height = getHeightFromLeftAndRight(nowRoot);  
        // 查看平衡度  
        int balance = getBalance(nowRoot);  
        // 根据平衡度来调整树  
        nowRoot = reBalance(nowRoot, balance);  
  
        return nowRoot;  
    }  
  
  
    // 删除节点,  
    public MyAVLTreeNode<K, V> delete(K key) {  
        if (key != null) {  
            root = delete(root, key);  
        }  
        return root;  
    }  
  
    public MyAVLTreeNode<K, V> delete(MyAVLTreeNode<K, V> nowRoot, K key) {  
        if (nowRoot == null) {  
            return null;  
        }  
  
        int compare = key.compareTo(nowRoot.key);  
        if (compare < 0) {  
            nowRoot.left = delete(nowRoot.left, key);  
        } else if (compare > 0) {  
            nowRoot.right = delete(nowRoot.right, key);  
        } else {  
            // nowRoot 就是要删除的节点  
            if (nowRoot.left == null && nowRoot.right == null) {  
                // 如果这个节点没有子节点,则直接删除就行了  
                // 直接返回 null 表示删除当前节点  
                return null;  
            } else if (nowRoot.left != null && nowRoot.right == null) {  
                // 如果左不为空,右为空,左节点直接向上替代  
                return nowRoot.left;  
            } else if (nowRoot.right != null && nowRoot.left == null) {  
                // 如果右不为空,左为空,右节点直接向上替代  
                return nowRoot.right;  
            } else {  
                // 二者都不为空  
                // 从右节点找出最小的把 key 和 value 替换过来  
                MyAVLTreeNode<K, V> minNode = findMinNode(nowRoot.right);  
                nowRoot.val = minNode.val;  
                nowRoot.key = minNode.key;  
                // 因为 minNode 没有子节点,所以,删除 minNode  的时候不会再次进入这个循环  
                nowRoot.right = delete(nowRoot.right, minNode.key);  
            }  
        }  
  
        nowRoot.height = getHeightFromLeftAndRight(nowRoot);  
        int balance = getBalance(nowRoot);  
        nowRoot = reBalance(nowRoot, balance);  
  
        return nowRoot;  
    }  
  
  
    private int getHeight(MyAVLTreeNode<K, V> root) {  
        return root == null ? 0 : root.height;  
    }  
  
    private int getHeightFromLeftAndRight(MyAVLTreeNode<K, V> root) {  
        int leftHeight = getHeight(root.left);  
        int rightHeight = getHeight(root.right);  
        return Math.max(leftHeight, rightHeight) + 1;  
    }  
  
    private int getBalance(MyAVLTreeNode<K, V> root) {  
        int leftHeight = getHeight(root.left);  
        int rightHeight = getHeight(root.right);  
        return leftHeight - rightHeight;  
    }  
  
  
    private enum BalanceType {  
        LL,  
        LR,  
        RR,  
        RL  
    }  
  
  
    /**  
     * 核心方法,AVL 树再平衡  
     */  
    private MyAVLTreeNode<K, V> reBalance(MyAVLTreeNode<K, V> nowRoot, int balance) {  
        if (balance < -1 || balance > 1) {  
            BalanceType type = null;  
            if (balance > 1) {  
                // 因为左边树高至少是 2,所以 nowRoot.left 一定是不为 null 的  
                int leftBalance = getBalance(nowRoot.left);  
                if (leftBalance < 0) {  
                    // 需要旋转两次  
                    type = BalanceType.LR;  
                } else {  
                    // 只用旋转一次  
                    type = BalanceType.LL;  
                }  
            } else if (balance < -1) {  
                int rightBalance = getBalance(nowRoot.right);  
                if (rightBalance > 0) {  
                    // 需要旋转两次  
                    type = BalanceType.RL;  
                } else {  
                    // 只用旋转一次  
                    type = BalanceType.RR;  
                }  
            }  
  
            if (type != null) {  
                switch (type) {  
                    case LL:  
                        return rightRotate(nowRoot);  
                    case LR:  
                        // 旋转之后,需要将旋转后的结果更新到原来的树上  
                        nowRoot.left = leftRotate(nowRoot.left);  
                        return rightRotate(nowRoot);  
                    case RR:  
                        return leftRotate(nowRoot);  
                    case RL:  
                        // 旋转之后,需要将旋转后的结果更新到原来的树上  
                        nowRoot.right = rightRotate(nowRoot.right);  
                        return leftRotate(nowRoot);  
                }  
            }  
        }  
  
        return nowRoot;  
    }  
  
    // 始终记住三个参数的关系,即 y > T2 > x 这样写左旋或者右旋就不会乱  
    // 我们修改也就是这三个节点,更新的也只有这三个节点  
    private MyAVLTreeNode<K, V> rightRotate(MyAVLTreeNode<K, V> y) {  
        MyAVLTreeNode<K, V> x = y.left;  
        MyAVLTreeNode<K, V> T2 = x.right;  
  
        x.right = y;  
        y.left = T2;  
  
        // 这里我们只用更新 x 和 y 的树高,为什么,因为我们并没有修改 T2 的左右子节点  
        // 更新 height 的时候,一定要注意,要先更新下级节点的高度,再更新上级节点的高度,因为上级节点的高度取决于下级节点的高度  
        y.height = getHeightFromLeftAndRight(y);  
        x.height = getHeightFromLeftAndRight(x);  
  
        return x;  
    }  
  
    private MyAVLTreeNode<K, V> leftRotate(MyAVLTreeNode<K, V> x) {  
        MyAVLTreeNode<K, V> y = x.right;  
        MyAVLTreeNode<K, V> T2 = y.left;  
  
        y.left = x;  
        x.right = T2;  
  
  
        x.height = getHeightFromLeftAndRight(x);  
        y.height = getHeightFromLeftAndRight(y);  
  
        return y;  
    }  
  
  
    /**  
     * 查找最小的节点,只用从最左边往下找即可。  
     */  
    public MyAVLTreeNode<K, V> findMinNode(MyAVLTreeNode<K, V> nowRoot) {  
        if (nowRoot == null) {  
            return null;  
        }  
        MyAVLTreeNode<K, V> minimalNode = nowRoot;  
        while (nowRoot != null && nowRoot.left != null) {  
            minimalNode = nowRoot.left;  
            nowRoot = minimalNode;  
        }  
        return minimalNode;  
    }  
  
  
    public MyAVLTreeNode<K, V> getRoot() {  
        return root;  
    }  
  
}

测试代码

 
public static void main(String[] args) {  
	MyAVLTree<Integer, String> tree = new MyAVLTree<Integer, String>();  
	tree.insert(1, "a");  
	BTPrinter.printTree(tree.getRoot());  

	tree.insert(2, "b");  
	BTPrinter.printTree(tree.getRoot());  

	tree.insert(3, "c");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(4, "d");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(5, "e");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(6, "f");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(7, "g");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-1, "h");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-2, "i");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-3, "j");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-5, "k");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-6, "l");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-7, "m");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-9, "n");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-10, "o");  
	BTPrinter.printTree(tree.getRoot());  
	tree.insert(-11, "p");  
	BTPrinter.printTree(tree.getRoot());  


	System.out.println("---------------delete-----------------");  

	tree.delete(5);  
	BTPrinter.printTree(tree.getRoot());  
	tree.delete(6);  
	BTPrinter.printTree(tree.getRoot());  
	tree.delete(4);  
	BTPrinter.printTree(tree.getRoot());  
	tree.delete(-9);  
	BTPrinter.printTree(tree.getRoot());  
	tree.delete(-1);  
	BTPrinter.printTree(tree.getRoot());  

	System.out.println("---------------search-----------------");  

	MyAVLTreeNode<Integer, String> target = tree.search(-11);  
	System.out.println(target.key + ":" + target.val);  

}  
  

输出

---
1:a
--------
1:a     
   |_   
     2:b
-------------
     2:b     
   _|   |_   
1:a       3:c
------------------
     2:b          
   _|   |_        
1:a       3:c     
             |_   
               4:d
-----------------------
     2:b               
   _|   |______        
1:a            4:d     
             _|   |_   
          3:c       5:e
----------------------------
               4:d          
        ______|   |_        
     2:b            5:e     
   _|   |_             |_   
1:a       3:c            6:f
---------------------------------
               4:d               
        ______|   |______        
     2:b                 6:f     
   _|   |_             _|   |_   
1:a       3:c       5:e       7:g
---------------------------------------
                     4:d               
              ______|   |______        
           2:b                 6:f     
         _|   |_             _|   |_   
      1:a       3:c       5:e       7:g
    _|                                 
-1:h                                   
---------------------------------------------
                           4:d               
                    ______|   |______        
                 2:b                 6:f     
          ______|   |_             _|   |_   
      -1:h            3:c       5:e       7:g
    _|    |_                                 
-2:i        1:a                              
---------------------------------------------------
                                 4:d               
                ________________|   |______        
            -1:h                           6:f     
          _|    |______                  _|   |_   
      -2:i             2:b            5:e       7:g
    _|               _|   |_                       
-3:j              1:a       3:c                    
---------------------------------------------------------
                                       4:d               
                      ________________|   |______        
                  -1:h                           6:f     
          _______|    |______                  _|   |_   
      -3:j                   2:b            5:e       7:g
    _|    |_               _|   |_                       
-5:k        -2:i        1:a       3:c                    
---------------------------------------------------------------
                        -1:h                                   
                _______|    |________________                  
            -3:j                             4:d               
          _|    |_                    ______|   |______        
      -5:k        -2:i             2:b                 6:f     
    _|                           _|   |_             _|   |_   
-6:l                          1:a       3:c       5:e       7:g
---------------------------------------------------------------------
                              -1:h                                   
                      _______|    |________________                  
                  -3:j                             4:d               
          _______|    |_                    ______|   |______        
      -6:l              -2:i             2:b                 6:f     
    _|    |_                           _|   |_             _|   |_   
-7:m        -5:k                    1:a       3:c       5:e       7:g
---------------------------------------------------------------------------
                                    -1:h                                   
                ___________________|    |________________                  
            -6:l                                         4:d               
          _|    |_______                          ______|   |______        
      -7:m              -3:j                   2:b                 6:f     
    _|                _|    |_               _|   |_             _|   |_   
-9:n              -5:k        -2:i        1:a       3:c       5:e       7:g
----------------------------------------------------------------------------------
                                           -1:h                                   
                       ___________________|    |________________                  
                   -6:l                                         4:d               
           _______|    |_______                          ______|   |______        
       -9:n                    -3:j                   2:b                 6:f     
     _|    |_                _|    |_               _|   |_             _|   |_   
-10:o        -7:m        -5:k        -2:i        1:a       3:c       5:e       7:g
-----------------------------------------------------------------------------------------
                                                  -1:h                                   
                              ___________________|    |________________                  
                          -6:l                                         4:d               
                  _______|    |_______                          ______|   |______        
              -9:n                    -3:j                   2:b                 6:f     
            _|    |_                _|    |_               _|   |_             _|   |_   
       -10:o        -7:m        -5:k        -2:i        1:a       3:c       5:e       7:g
     _|                                                                                  
-11:p                                                                                    
---------------delete-----------------
------------------------------------------------------------------------------------
                                                  -1:h                              
                              ___________________|    |________________             
                          -6:l                                         4:d          
                  _______|    |_______                          ______|   |_        
              -9:n                    -3:j                   2:b            6:f     
            _|    |_                _|    |_               _|   |_             |_   
       -10:o        -7:m        -5:k        -2:i        1:a       3:c            7:g
     _|                                                                             
-11:p                                                                               
-------------------------------------------------------------------------------
                                                  -1:h                         
                              ___________________|    |________________        
                          -6:l                                         4:d     
                  _______|    |_______                          ______|   |_   
              -9:n                    -3:j                   2:b            7:g
            _|    |_                _|    |_               _|   |_             
       -10:o        -7:m        -5:k        -2:i        1:a       3:c          
     _|                                                                        
-11:p                                                                          
--------------------------------------------------------------------------
                                                  -1:h                    
                              ___________________|    |______             
                          -6:l                               2:b          
                  _______|    |_______                     _|   |______   
              -9:n                    -3:j              1:a            7:g
            _|    |_                _|    |_                         _|   
       -10:o        -7:m        -5:k        -2:i                  3:c     
     _|                                                                   
-11:p                                                                     
--------------------------------------------------------------------
                                            -1:h                    
                        ___________________|    |______             
                    -6:l                               2:b          
            _______|    |_______                     _|   |______   
       -10:o                    -3:j              1:a            7:g
     _|     |_                _|    |_                         _|   
-11:p         -7:m        -5:k        -2:i                  3:c     
--------------------------------------------------------------
                                            1:a               
                        ___________________|   |______        
                    -6:l                              3:c     
            _______|    |_______                    _|   |_   
       -10:o                    -3:j             2:b       7:g
     _|     |_                _|    |_                        
-11:p         -7:m        -5:k        -2:i                    
---------------search-----------------
-11:p

非常漂亮的二叉树的结构
假设有一根线竖着从左往右扫这个图形(这个其实就是中序遍历 94. 二叉树的中序遍历),你会发现,碰到的数刚好就是从小到大排序的。这也是二叉搜索树的特性。二叉树的递归遍历#中序遍历

编码过程中的注意点

理解二叉树的代码

其实对于二叉树来说,查找的代码是非常简单的,类似于二分查找,真正难以理解的,其实是插入和删除的时候,保证任意节点左右子树高度差不超过 1 的实现过程。
从上面的代码中我们可以看到,实现主要是通过旋转来实现的。

理解查询的代码

类似于二分查找,

理解插入的过程

往 AVL 树中插入节点的流程是:

  1. 首先递归到需要插入的位置,创建节点
  2. 返回并更新当前树的左子树或者右子树
  3. 更新当前树的根节点的树高
  4. 计算当前树的根节点的平衡度
  5. 根据平衡度进行再平衡
  6. 递归返回到上一层,重复 2-5 步
    因为我们是从 root 节点开始往下寻找位置插入的,所以当递归返回的时候,会检查每一级的平衡因子,因此左旋或者右旋的次数,确实会有点多,这也是后面为什么后面会开发出红黑树(请看 理解红黑树),并成为主流

理解删除的过程

从 AVL 树中删除节点的流程是:

  1. 找到要删除的节点
    • 如果这个节点没有子节点,直接删除
    • 如果这个节点只有一个子节点,那直接让那个唯一的子节点代替自己
    • 如果两个子节点都存在,则
      • 从右子节点中找到最小的节点,替代当前节点
      • 从右子树中删除这个最小节点
  2. 返回并更新当前树的左子树或者右子树
  3. 更新当前树的根节点的树高
  4. 计算当前树的根节点的平衡度
  5. 根据平衡度进行再平衡
  6. 递归返回到上一层,重复 2-5 步
    可以看到跟插入的时候除了第一步不一样,后面几步都一样,现在我们就来看看 AVL 树的核心操作,再平衡

理解再平衡的过程 - 重点

再平衡的作用是保持左子树跟右子树的高度差不超过 1,

  1. 首先判断当前节点左右子树的高度差是否大于 1
  2. 如果是,则需要进一步判断是哪种类型,总共有四种类型,LL、LR、RR、RL、
  3. 根据类型,对当前子树进行旋转
  4. 返回旋转之后的根节点并返回。

1. LL 型(左左)

如果插入或者删除之后,左子树 B 的高度比右子树高出 1 以上,而且左子树的左边 C 比右边高,就是 LL 型,需要对 A 进行一次右旋转

     A                      B
    /                      / \
   B       =>             C   A
  /
 C

2. RR 型(右右)

如果插入或者删除之后,右子树 B 的高度比左子树高出 1 以上,而且右子树的右边 C 比左边高,就是 RR 型,需要对 A 进行一次左旋转

   A                          B
    \                        / \
     B      =>              A   C
      \
       C

3. LR 型(左右)

如果插入或者删除之后,左子树 B 的高度比右子树高出 1 以上,而且左子树 B 的右边 C 比左边高,就是 LR 型,需要先对左子树 B 进行一次左旋转,然后再对当前节点 A 进行一次右旋转

     A                       A                      C
    /                       /                      / \
   B      =>  左旋(B)      C      => 右旋(A)       B   A
    \                     /                      
     C                   B

4. RL 型(右左)

如果插入或者删除之后,右子树 B 的高度比左子树高出 1 以上,而且右子树的左边 C 比右边高,就是 RL 型,需要先对右子树 B 进行一次右旋转,然后再对当前节点 A 进行一次右旋转

     A                       A                       C
      \                       \                     / \
       B       => 右旋(B)      C      => 左旋(A)    A   B
      /                         \                  
     C                           B

实际过程

上面这四种情况,只是概念模型,ABC 除了图中的左右子节点外,也会有别的节点
而且旋转,是节点树的整体位置的变换,而不是两个单独的节点之间的变换,后面我们会看到
具体的过程,强烈推荐通过 visualgo 中的例子来感受 AVL 树在各种操作下的再平衡过程

假设一开始树形,
图 1:
800

突然加了一个节点,此时 x 是平衡的,但是 y 不平衡
图 2:
800
而且此时是 LL,需要一次右旋,结果如下
图 3:
800

再加一个节点,此时 x 不平衡,此时的状态是 RR,需要一次左旋
图 4:
800
左旋之后的结果
图 5:
800

假设从图 1 变成了图 6:
图 6:
800
此时就是 LR,需要先进进行一次左旋
800
可以看到变成图 2 了,再进行一次右旋,就变成图 3 了,从图 6 到图 3 ,可以看到 LR 先左旋再右旋的变换过程其实就是把节点搬到别的地方,在保证左子树和右子树的大小的情况下,尽量保证让整个树的高度最低。

对左旋函数和右旋函数的理解

参考上面的图形和代码,应该能对左旋右旋操作有一个明确的理解了。

我们在编写函数的时候,不管是左旋还是右旋,都存在三种类型的节点,他们的关系始终是 y > T2 > x ,始终记住这个关系,写左旋或者右旋就不会乱
因为 y 是最大的节点,所以在写左旋的时候,根节点(左旋函数的参数)的是 y ,x 是 y 的左子树,同时 T2 是 x 的右子树
而写右旋的时候,x 是跟节点(右旋函数的参数),y 是 x 的右子树,T2 是 y 的左子树

旋转的作用是什么?
我们会发现,旋转之前的关系是 y>T2>x,旋转之后的关系依然是 y>T2>x,但是树的高度却降低了,
这就是旋转最神奇的地方,我们减少了子树的高度,但是我们的组织元素的方式依然是符合规则的

这个规则我们一定要保证,因为只有保证了规则,才能进行快速的二分查找,而我们进行各种旋转,都是为了降低树高,减少查找时分叉的次数

其实 T2 不是我们要修改的节点,我们也没有修改 T2,只是因为我们左旋或者右旋的时候,我们会改变 x 和 y 的指针,那之前指向的树我们不能丢掉啊,这个我们不能丢掉的子树,就是 T2 子树,

这个命名 T2 是源于很多 AVL 教科书中对这种“中间子树”的惯称(有时还有 T1, T3)。

其实严格来说,我们修改了 x 和 y 的左指针和有指针而已,(通过简单修改指针就能调整树高,而且保证左中右的节点的顺序,确实非常地巧妙
我们修改了 x 和 y 的子节点,我们就得更新 x 和 y 的树高。我们没有修改 T2,所以不更新它
更新 height 的时候,一定要注意,要先更新下级节点的高度,再更新上级节点的高度,因为上级节点的高度取决于下级节点的高度
右旋之后,x 是新的根节点,y 是 x 的子节点,因此先更新 y 的高度,再更新 x 的高度,
左旋之后,y 是新的根节点,x 是 y 的子节点,因此先更新 x 的高度,再更新 y 的高度,