十三、彻底搞懂平衡二叉树AVL

590 阅读5分钟

平衡二叉树和AVL

目标:

  • 认识平衡二叉树与AVL
  • 平衡二叉树的判断
    • 判断是否为二分搜索树
    • 计算节点的高度和平衡因子
    • 判断是否为平衡二叉树
  • 旋转
    • RR
    • LL
    • LR
    • RL
  • 删除元素

1.1 平衡二叉树与AVL

  • 平衡二叉树

    由前面对二分搜索树的研究可知,在最坏的情况下,二分搜索树会退化成链表,此时时间复杂度为 O(n)。因此我们引入平衡二叉树,它是一种结构平衡的二分搜索树。那么,在二分搜索树的基础上,平衡二叉树需要满足两个条件:

    • 它的左右两个子树的高度差的绝对值不超过1
    • 左右两个子树都是一颗平衡二叉树

    平衡二叉树的高度和节点数量之间的关系也是O(logn)。

  • AVL树

    AVL树是最早的自平衡二分搜索树结构,AVL树维护自身的平衡涉及到两个概念:

    • 节点的高度
    • 平衡因子:左子树的高度减去右子树的高度。
      • 平衡因子的取值只可能为0,-1,1。分别对应着左右子树等高,左子树比较高,右子树比较高。
      • 如果平衡因子绝对值大于等于2,那么就不是平衡二叉树。

1.2 平衡二叉树的判断

  • 判断是否为二分搜索树

    根据之前总结的二分搜索树特点,根节点大于左子树节点同时小于右子树节点,我们用中序遍历来判断一棵树是否为二分搜索树。具体如下:

        // 判断该二叉树是否是一棵二分搜索树
        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);
        }
    
  • 计算节点的高度和平衡因子

    初始设定height=1,那么节点高度的变化和平衡因子的计算,主要是在添加元素时。此时需要修改之前的add代码:

        // 获得节点node的高度值
        private int getHeight(Node node) {
            if (node == null) {
                return 0;
            }
            return node.height;
        }
    
        // 获得节点node的平衡因子
        private int getBalanceFactor(Node node) {
            if (node == null) {
                return 0;
            }
            return getHeight(node.left) - getHeight(node.right);
        }
        
    	// 向以node为根的二分搜索树中插入元素(key, value),递归算法
    	private Node add(Node node, K key, V value) {
            if (node == 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 // key.compareTo(node.key) == 0
            {
                node.value = value;
            }
    
            // 更新height
            node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
    
            // 计算平衡因子
            int balanceFactor = getBalanceFactor(node);
            if (Math.abs(balanceFactor) > 1) {
                System.out.println("unbalanced: " + balanceFactor);
            }
            return node;
        }
    

    如上代码所示,当插入元素时,节点的高度发生变化,并且当前位置节点的高度=1+左右节点高度最大值。在计算平衡因子中,主要是获取左右节点高度的差值。

  • 那么现在我们来判断是否为一棵平衡二叉树

    判断标准即为,左子树和右子树高度差的绝对值是否大于1。采用递归思想实现代码:

        // 判断该二叉树是否是一棵平衡二叉树
        public boolean isBalanced() {
            return isBalanced(root);
        }
    
        private boolean isBalanced(Node node) {
            if (node == null) {
                return true;
            }
    
            int balanceFactor = getBalanceFactor(node);
            if (Math.abs(balanceFactor) > 1) {
                return false;
            }
            return isBalanced(node.left) && isBalanced(node.right);
        }
    

1.3 旋转

当我们插入元素时,有可能破坏平衡二叉树的结构,当加入节点后,需要沿着节点向上维护平衡性。

  • 最小失衡子树

    • 在新插入的结点向上查找,以第一个平衡因子的绝对值超过1的结点为根的子树称为最小不平衡子树
    • 平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现,左旋与右旋。
  • 左旋RR

    插入的元素在不平衡的节点的右子树的右子树

    • 节点的右孩子替代此节点位置
    • 右孩子的左子树变为该节点的右子树
    • 节点本身变为右孩子的左子树

    image-20200915145459296image-20200915145605297

    如上图所示:

    节点的右孩子替代此节点位置 —— 节点 66 的右孩子是节点 75 ,将节点 75 代替节点 66 的位置;

    右孩子的左子树变为该节点的右子树 —— 节点 75 的左子树为节点 70,将节点 70 挪到节点 66 的右子树位置;

    节点本身变为右孩子的左子树 —— 节点 66 变为了节点 75 的左子树。

    具体实现代码如下:

        // 对节点y进行向左旋转操作,返回旋转后新的根节点x
        //    y                             x
        //  /  \                          /   \
        // T1   x      向左旋转 (y)       y     z
        //     / \   - - - - - - - ->   / \   / \
        //   T2  z                     T1 T2 T3 T4
        //      / \
        //     T3 T4
        private Node leftRotate(Node y){
            Node x = y.right;
            Node T2 = x.left;
    
            // 向左旋转过程
            x.left = y;
            y.right = T2;
    
            // 更新height
            y.height = 1 + Math.max(getHeight(y.left), getHeight(y.right));
            x.height = 1 + Math.max(getHeight(x.left), getHeight(x.right));
    
            return x;
        }
    
  • 右旋LL

    插入的元素在不平衡的节点的左子树的左子树

    • 节点的左孩子替代此节点
    • 节点的左孩子的右子树变为节点的左子树
    • 节点本身变为左孩子的右子树

    image-20200915145911373image-20200915150213339

    来直接看代码:

        // 对节点y进行向右旋转操作,返回旋转后新的根节点x
        //        y                              x
        //       / \                           /   \
        //      x   T4     向右旋转 (y)        z     y
        //     / \       - - - - - - - ->    / \   / \
        //    z   T3                       T1  T2 T3 T4
        //   / \
        // T1   T2
        private Node rightRotate(Node y) {
    
            Node x = y.left;
            Node T3 = x.right;
    
            // 向右旋转过程
            x.right = y;
            y.left = T3;
    
            // 更新height
            y.height = 1 + Math.max(getHeight(y.left), getHeight(y.right));
            x.height = 1 + Math.max(getHeight(x.left), getHeight(x.right));
    
            return x;
        }
    
  • LR

    插入的元素在不平衡的节点的左子树的右子树

    image-20200915155733003image-20200915155748009image-20200915155758799

    此时的解决办法就是,先进行左旋转,将树结构调整为LL结构,然后再进行右旋转,实现AVL。

  • RL

    插入的元素在不平衡的节点的右子树的左子树

    image-20200915155848920image-20200915155900735image-20200915155909623

    此时的解决办法就是,先进行右旋转,然后再进行左旋转。

    综合上述四种情况,实现汇总代码如下:

        // 平衡维护
        // LL
        if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) { // 此时说明是在左子树的左子树添加元素,使用右旋转
        	return  rightRotate(node);
        }
        // RR
        if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) { // 此时说明是在右子树的右子树添加元素,使用左旋转
        	return  leftRotate(node);
        }
        // LR
        if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) { // 此时说明是在左子树的右子树添加元素,先使用左旋转,再使用右旋转
            node.left = leftRotate(node.left);
            return rightRotate(node);
        }
        // RL
        if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) { // 此时说明是在右子树的左子树添加元素,使用右旋转,再使用左旋转
            node.right = rightRotate(node.right);
            return leftRotate(node);
        }