实现AVL树

一、概述

1.来源

AVL 树是一种自平衡二叉搜索树,由托尔·哈斯特罗姆在 1960 年提出并在 1962 年发表。它的名字来源于发明者的名字:Adelson-Velsky 和 Landis,他们是苏联数学家,于 1962 年发表了一篇论文,详细介绍了 AVL 树的概念和性质。

AVL 树是用于存储有序数据的一种重要数据结构,它是二叉搜索树的一种改进和扩展。它不仅能够提高搜索、插入和删除操作的效率,而且还能够确保树的深度始终保持在 O(log n) 的水平。

2.定义AVL节点

    static class AVLNode {

        int key;

        Object value;

        // 节点高度,刚创建出来默认为1(按照力扣的约定)
        int height = 1;
        AVLNode left;
        AVLNode right;
        public AVLNode(int key) {
            this.key = key;
        }

        public AVLNode(int key, Object value) {
            this.key = key;
            this.value = value;
        }

        public AVLNode(int key, Object value, AVLNode left, AVLNode right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }

二、平衡因子

在二叉搜索树中,如果插入的元素按照特定的顺序排列,可能会导致树变得非常不平衡,从而降低了搜索、插入和删除的效率。

为了解决这个问题,AVL 树通过在每个节点中维护一个平衡因子来确保树的平衡。
平衡因子是左子树的高度减去右子树的高度。
如果平衡因子的绝对值大于等于 2,则通过旋转操作来重新平衡树。

       3               2
      /      右旋      / \
     2    -------->  1   3
    /
   1

定义平衡因子(balance factor):平衡因子 = 左子树高度 - 右子树高度

平衡因子:

  • bf = 0,1,-1 时,表示左右平衡
  • bf > 1 时,表示左边太高
  • bf < -1 时,表示右边太高
节点高度及是否平衡相关方法
    /**
     * 求节点的高度
     * @param node
     * @return
     */
    public int height(AVLNode node) {
        return node == null ? 0 : node.height;
    }

    /**
     * 更新节点的高度(在新增,删除,旋转节点的时候需要执行)
     * @param node
     */
    public void updateHeight(AVLNode node) {
        node.height = Math.max(height(node.left), height(node.right)) + 1;
    }

    /**
     * 获取平衡因子:左子树的高度 - 右子树的高度
     * @param node
     * @return
     */
    public int bf(AVLNode node) {
        return height(node.left) - height(node.right);
    }

三、失衡的四种情况

判断失衡

一个节点的左右孩子,高度差超过 1,则此节点失衡,需要旋转

3.1 LL

  • L:失衡节点6的 bf > 1,即左边更高
  • L:失衡节点6的左孩子3的 bf >= 0,即左孩子这边也是左边更高或等高
        6
       / \
      3   7
     / \
    2   4
   /
  1

3.2 LR

  • L:失衡节点6的 bf > 1,即左边更高
  • R:失衡节点6的左孩子2的 bf < 0 即左孩子这边是右边更高
        6
       / \
      2   7
     / \
    1   4
       / \   
      3   5

3.3 RL

  • R:失衡节点2的 bf <-1,即右边更高
  • L:失衡节点2的右孩子5的 bf > 0,即右孩子这边左边更高
    2
   / \
  1   6
     / \
    4   7
   / \
  3   5 

3.4 RR

  • R:失衡节点2的 bf <-1,即右边更高
  • R:失衡节点2的右孩子4的 bf <= 0,即右孩子这边右边更高或等高
    2
   / \
  1   4
     / \
    3   5
         \
          6

四、旋转实现树的平衡

4.1 LL失衡

        6
       / \
      3   7
     / \
    2   4
   /
  1

对节点6进行右旋操作:
1.节点3上位:原本旋转的节点作为3的右孩子
2.上位节点3原本的右孩子4需要更换父亲为原来的旋转的节点6

        3
       / \
      2   6
     /   / \
    1   4   7
/**
 * 右旋
 * @param node 要旋转的节点
 * @return 返回新的根节点
 */
public AVLNode rightRotate(AVLNode node) {
    // 6为要旋转的节点node

    // 要上位的节点3
    AVLNode upNode = node.left; 
    // 要上位的节点3的孩子4(待换父亲的节点4)
    AVLNode toChangeParent = upNode.right;

    // ①处理后事:上位节点的右孩子换父亲 换为退位节点(旋转的节点)的左孩子
    node.left = toChangeParent;

    // ②上位:要旋转的节点6退位作为要上位节点3的右孩子
    upNode.right = node; 
    
    // 节点旋转完之后需要更新高度
    updateHeight(node);
    // 上位节点的高度也会改变
    updateHeight(upNode);

    return upNode;
}

4.2 RR失衡

    2
   / \
  1   4
     / \
    3   5
         \
          6

对节点2进行左旋操作:
1.节点4上位:旋转的节点2作为4的左孩子
2.上位节点4的左孩子3更换父亲为原来的旋转节点2

      4
     / \
    2   5
   / \    \
  1   3    6
/**
 * 左旋
 * @param node 要旋转的节点
 * @return 新的根节点
 */
public AVLNode leftRotate(AVLNode node) {
    // 待上位的节点
    AVLNode upNode = node.right;
    // 待换父亲的节点
    AVLNode toChangeParent = upNode.left;

    // ①处理上位节点的后事
    node.right = toChangeParent;

    // ②节点4上位:旋转节点作为4的左孩子
    upNode.left = node;
    
    // 节点旋转完之后需要更新高度
    updateHeight(node);
    // 上位节点的高度也会改变
    updateHeight(upNode);

    return upNode;
}

4.3 LR失衡

先将树调整为LL的失衡情况,再使用LL的调整方法

        6
       / \
      2   7
     / \
    1   4
       / \   
      3   5

1.先让左子树向左旋转(即上面4.1LL的向左旋转方法):此时树已经符合LL的失衡情况

        6
       / \
      4   7
     / \
    2   5
   / \
  1   3

2.针对LL的失衡情况,对树进行右旋:

        4
       / \
      2   6
     / \ / \
    1  3 5  7
/**
 * 先左旋失衡节点的左子树,再右旋失衡节点
 * 针对LR失衡
 * @param node
 * @return
 */
public AVLNode leftRightRotate(AVLNode node) {
    // 失衡节点node的左子树左旋
    AVLNode avlNode = leftRotate(node.left);
    // 失衡节点6的左孩子更新
    node.left = avlNode;
    // 对失衡节点进行右旋操作
    return rightRotate(node);
}

4.4 RL失衡

先将树调整为RR的失衡情况,再使用RR的调整方法

    2
   / \
  1   6
     / \
    4   7
   / \
  3   5 

1.右子树向右旋转

        2
       / \
      1   4
         / \
        3   6
           / \
          5   7

2.整棵树向左旋转

        4
       / \
      2   6
     / \  / \
    1   3 5  7
/**
 * 先右旋失衡节点的右子树,再左旋失衡节点
 * 针对RL失衡
 * @param node
 * @return
 */
public AVLNode rightLeftRotate(AVLNode node) {
    // 失衡节点2的右子树进行右旋,并更新失衡节点的右子树
    node.right = rightRotate(node.right);
    // 对失衡节点进行左旋
    return leftRotate(node);
}

五、树的平衡

检查节点是否失衡,如果失衡,则调整,否则原样返回

/**
 * 检查节点是否失衡,如果失衡,则调整,返回平衡后的节点;否则直接返回
 * @param node
 * @return
 */
public AVLNode balance(AVLNode node) {
    if (node == null) {
        return null;
    }
    // 平衡因子
    int bf = bf(node);
    // 等于发生在删除操作时,也符合LL/RR的失衡情况,直接右/左旋
    if (bf > 1 && bf(node.left) >= 0) { // LL
        //直接右旋
        return rightRotate(node);
    } else if (bf > 1 && bf(node.left) < 0) { // LR
        // 左旋再右旋
        return leftRightRotate(node);
    } else if (bf < -1 && bf(node.right) > 0) { // RL
        // 右旋再左旋
        return rightLeftRotate(node);
    } else if (bf < -1 && bf(node.right) <= 0) { // RR
        // 直接左旋
        return leftRotate(node);
    }
    return node;
}

六、新增节点

1.找到要插入的位置插入,如果没找到则更新对应key的value。
2.插入完后调整树的高度和重新平衡树

/**
 * 新增节点
 * @param key
 * @param value
 */
public void put(int key,Object value) {
    root = doPut(root, key, value);
}

/**
 * 递归插入元素
 * @param node 开始查找插入位置的节点
 * @param key  插入节点的key
 * @param value 插入节点的value
 * @return 插入完成之后的根节点
 */
public AVLNode doPut(AVLNode node,int key, Object value) {
    if (node == null) {
        return new AVLNode(key, value);
    }
    // 如果找到了 就执行更新操作 直接返回(不需要调整高度和平衡树)
    if (key == node.key) {
        node.value = value;
        return node;
    }
    // 没找到就继续找到要插入的位置
    if (key < node.key) {
        // 往左找 找到了空位,返回了创建的新节点,设置给当前节点的左孩子(因为是往左找的)
        node.left = doPut(node.left, key, value);
    } else {
        // 如果key比当前的node的key要大,往右找空位;找到后拿到了创建的新节点,设置给当前的右孩子。
        node.right = doPut(node.right, key, value);
    }
    // 插入了新的元素之后,高度需要更新,更新了高度,可能会失衡,需要调整树的平衡
    updateHeight(node);
    return balance(node);
}

七、删除节点

删除节点4:

     *                                    2
     *                                 /    \
     *            7                  1       4
     *          /   \                         \
     *        4      8                         8
     *      /   \                            /   \
     *     2     5                         7      9
     *    / \     \                      /
     *   1   3     6                    5
     *                                   \
     *                                    6

找到要删除的节点4:

  • 1.被删除的节点只有右孩子,将右孩子托付给被删除节点的父亲
  • 2.被删除的节点只有左孩子,将左孩子托付给被删除节点的父亲
  • 3.被删除的节点是叶子节点,符合情况1和2,将null托付给被删除节点的父亲
  • 4.被删除的节点左右孩子都有:
    • 将被删除节点的后继节点托付给被删除节点的父亲:
    • 被删除节点与后继节点相邻,将后继节点托付给被删除节点的父亲(删除4,将后继5托付给7)
    • 被删除节点与后继节点不相邻,先处理后继节点的后代,将后继节点的后代托付给后继节点的父亲,再将后继节点托付给被删除节点的父节点
    • (删除4,将后继5的后代6托付给7,再将后继5托付给4的父亲1)
  • 5.在回溯的过程更新节点的高度和平衡树
/**
 * 递归删除节点
 * @param node 查找删除节点的起点
 * @param key  要删除的key
 * @return 被删除节点后续节点
 */
public AVLNode doRemove(AVLNode node,int key) {
    // 寻找被删除的节点
    if (node == null) {
        return null;
    }
    if (key < node.key) {
        // 往左找 找到了之后,返回的是删剩下的,设置为当前节点的左孩子
        node.left = doRemove(node.left, key);
    } else if (key > node.key) {
        // 往右找
        node.right = doRemove(node.right, key);
    } else {
        // 找到了要删除的节点
        // 要删除的节点没有孩子
        if (node.left == null && node.right == null) {
            // 删除之后 没有要处理的后事 直接返回
            return null;
        } else if (node.left == null) {
            // 删除的节点只有右孩子, 返回右孩子,但是这里涉及到高度及平衡的处理,先暂存到删除节点node中,后续处理高度和平衡
            node = node.right;
        } else if (node.right == null) {
            // 删除的节点只有左孩子,返回左孩子,这里涉及高度及树平衡的处理,暂存到删除节点
            node = node.left;
        } else {
            // 删除的节点左右孩子都有
            // 找到要删除节点的后继节点(右子树中找到最小的),让该后继节点上位
            AVLNode deletedSuccessor = node.right;
            while (deletedSuccessor.left != null) {
                deletedSuccessor = deletedSuccessor.left;
            }
            //后继节点上位之前,先处理完后继节点的后事(此时该后继节点只可能存在右孩子)
            /**
             * 处理后事:把要删除节点的右子树作为删除的起点再调用doRemove(),删除的节点是该后继节点,此种情况符合只有右孩子的分支判断;
             * 删完之后就是把该后继节点的孩子给到了后继节点的父亲,即处理完了后事
             * 此时返回的是原本要删除节点的右子树,此时后继节点要上位: 即后继节点的右子树就是返回的这个处理完了后事的右子树
             */
            deletedSuccessor.right = doRemove(node.right, deletedSuccessor.key);
            deletedSuccessor.left = node.left;
            // 复制给node 用来更新高度和平衡树
            node = deletedSuccessor;
        }
    }
    // 更新树的高度
    updateHeight(node);
    // 平衡树
    return balance(node);
}

递归代码有点难理解,这里再给出二叉搜索树删除节点迭代版本的实现:
(二叉搜索树和AVL树区别只是最后需要重新对节点进行高度更新和平衡树)

public Object remove(int key) {
    // 找到被删除的节点
    BSTTreeNode pointer = root;
    BSTTreeNode deletedParent = null;
    while (pointer != null) {
        if (key < pointer.key) {
            deletedParent = pointer;
            pointer = pointer.left;
        } else if (key > pointer.key) {
            deletedParent = pointer;
            pointer = pointer.right;
        } else {
            break;
        }
    }
    // 被删除节点不存在
    if (pointer == null) {
        return null;
    }
    // 被删除节点只有左孩子
    if (pointer.right == null) {
        // 将这个唯一的孩子托付给父亲
        shift(deletedParent, pointer, pointer.left);
    }
    // 被删除的节点只有右孩子
    else if (pointer.left == null) {
        shift(deletedParent, pointer, pointer.right);
        // deletedParent.right = pointer.right;
    }
    // 被删除的节点左右孩子都有
    else {
        // 找到被删除节点的后继节点(这里后继不可能在祖先里,因为有右子树):在被删除节点的右子树中找到最小的(一直往左找到null为止)
        // 判断后继节点与被删除节点是否相邻
        BSTTreeNode deletedSuccessor = pointer.right;

        // 保存后继节点的父亲
        BSTTreeNode deletedSuccessorParent = pointer;

        // 找被删除节点的后继
        while (deletedSuccessor.left != null) {
            deletedSuccessorParent = deletedSuccessor;
            deletedSuccessor = deletedSuccessor.left;
        }
        //后继节点与被删除节点相邻  deletedSuccessorParent == pointer
        if (deletedSuccessor == pointer.right) {
            // 让后继替代被删除节点:把后继节点托付给被删除节点的父节点(只改变了父节点的指针)
            shift(deletedParent, pointer, deletedSuccessor);
            // 上位的节点的左指针需要改变:上位节点的左指针指向原本被删除节点的左孩子(右指针不需要改,他本身就带着右孩子上来的)
            deletedSuccessor.left = pointer.left;
        }
        // 后继节点与被删除节点不相邻,处理后继节点的后事:把后继节点的孩子托付给后继节点的父亲
        else {
            // 处理后事:把后继节点的孩子托付给后继节点的父亲 (这里的后继节点不会有左孩子,因为他本身就是最左边的孩子了)
            shift(deletedSuccessorParent, deletedSuccessor, deletedSuccessor.right);

            // 后继上位
            shift(deletedParent, pointer, deletedSuccessor);

            // 上位节点的左右指针都需要改变:上位节点的右指针指向自己的后代,后代托付给了他的父亲了;他上位之后要把左右指针都指向原本被删除节点的左右指针指向的地方
            deletedSuccessor.right = pointer.right;
            deletedSuccessor.left = pointer.left;
        }
    }
    return deletedParent.value;
}

/**
 * 托付方法:把孩子托付给父亲(只改变了父亲的指向)
 * @param parent  被删除节点的父亲
 * @param deleted 被删除节点
 * @param child   被删除节点的孩子
 */
public void shift(BSTTreeNode parent, BSTTreeNode deleted, BSTTreeNode child) {
    // 没有父亲,孩子直接成为根节点
    if (parent == null) {
        root = child;
    }
    // 如果本身被删除节点是左孩子,就让自己的孩子称为父亲的左孩子
    else if (parent.left == deleted) {
        parent.left = child;
    }
    // 如果本身被删除节点是右孩子,就让自己的孩子称为父亲的右孩子
    else {
        parent.right = child;
    }
}

八、查找相关方法

/**
 * 根据key获取value
 * @param key
 * @return
 */
public Object get(int key) {
    AVLNode node = root;
    while (node != null) {
        if (key < node.key) {
            node = node.left;
        } else if (key > node.key) {
            node = node.right;
        } else {
            return node.value;
        }
    }
    return null;
}

/**
 * 找出比key小的所有值
 * @param key
 * @return
 */
public List<Object> less(int key) {
    ArrayList<Object> list = new ArrayList<>();
    // 存储走过的路
    LinkedList<AVLNode> stack = new LinkedList<>();
    // 中序遍历就是由小到大 左 根 右
    AVLNode pointer = root;
    while (pointer != null || !stack.isEmpty()) {
        if (pointer != null) {
            stack.push(pointer);
            pointer = pointer.left;
        } else {
            AVLNode pop = stack.pop();
            if (pop.key < key) {
                list.add(pop.value);
            } else {
                break;
            }
            pointer = pop.right;
        }
    }
    return list;
}

/**
 * 找出比key大的所有值
 * 这里为了提高效率,采用反向中序遍历:右根左
 * @param key
 * @return
 */
public List<Object> greater(int key) {
    ArrayList<Object> list = new ArrayList<>();
    LinkedList<AVLNode> stack = new LinkedList<>();
    AVLNode pointer = root;
    while (pointer != null || !stack.isEmpty()) {
        if (pointer != null) {
            stack.push(pointer);
            pointer = pointer.right;
        } else {
            AVLNode pop = stack.pop();
            if (pop.key > key) {
                list.add(pop.value);
            } else {
                break;
            }
            pointer = pop.left;
        }
    }
    return list;
}

/**
 * 范围查询
 * @param leftKey
 * @param rightKey
 * @return
 */
public List<Object> between(int leftKey, int rightKey) {
    ArrayList<Object> list = new ArrayList<>();
    LinkedList<AVLNode> stack = new LinkedList<>();
    AVLNode pointer = root;
    while (pointer != null || !stack.isEmpty()) {
        if (pointer != null) {
            stack.push(pointer);
            pointer = pointer.left;
        } else {
            AVLNode pop = stack.pop();
            if (pop.key >= leftKey && pop.key <= rightKey) {
                list.add(pop.value);
            } else if (pop.key > rightKey) {
                break;
            }
            pointer = pop.right;
        }
    }
    return list;
}

九、AVL树的优缺点

优点

  1. AVL树是一种自平衡树,保证了树的高度平衡,从而保证了树的查询和插入操作的时间复杂度均为O(logn)。
  2. 相比于一般二叉搜索树,AVL树对查询效率的提升更为显著,因为其左右子树高度的差值不会超过1,避免了二叉搜索树退化为链表的情况,使得整棵树的高度更低。
  3. AVL树的删除操作比较简单,只需要像插入一样旋转即可,在旋转过程中树的平衡性可以得到维护。

缺点

  1. AVL树每次插入或删除节点时需要进行旋转操作,这个操作比较耗时,因此在一些应用中不太适用。
  2. 在AVL树进行插入或删除操作时,为保持树的平衡需要不断进行旋转操作,在一些高并发环节和大数据量环境下,这可能会导致多余的写锁导致性能瓶颈。
  3. AVL树的旋转操作相对较多,因此在一些应用中可能会造成较大的空间浪费。

你可能感兴趣的:(数据结构,算法)