数据结构之AVL树

1. 平衡二叉搜索树(Balanced Binary Search Tree)

平衡:当节点数量固定时,左右子树的高度越接近,这颗二叉树就越平衡,即高度越低。最理想的平衡就是像完全二叉树和满二叉树一样,高度最小。

常见平衡二叉搜索树

  1. AVL树
  2. 红黑树

改进方案:

在二叉搜索树的基础上进行改进,在节点添加、删除之后,尽量让二叉树恢复平衡。

  • 改进前:
    在这里插入图片描述
  • 改进后:
    在这里插入图片描述
    但如果调整的次数过多,也有可能会增加时间复杂度。因此使用尽量少的调整次数达到适度的平衡即可。

2. AVL树

2.1 AVL概念

  1. 平衡因子(Balance Factor):某节点的左右子树的高度差(左 - 右)。
    在这里插入图片描述
  2. 特点:每个节点的平衡因子只能是 1 0 -1 否则称为失衡,即每个节点的左右高度差不能超过 1 。
  3. 搜索、添加、删除的时间复杂度都是O(logn)。

2.2 添加

2.2.1 失衡情况

在添加元素时最坏情况可能会导致所有的祖先节点都会失衡,但并不会导致非祖先节点和父节点失衡。

比如下方是一棵树的一部分:
在这里插入图片描述

如果添加元素13
在这里插入图片描述
就会导致其除父节点外的祖先节点失衡一直到根节点都是处于失衡的状态。

2.2.2 LL-右旋转(单旋)

LL:其左子节点的左子几点导致其失衡。

如下图:在添加之前节点n的左右高度T0 = T1,父节点p的高度T2 = T0 + 1,祖先节点g的高度T3 = T2 + T0,现在处于平衡状态。
在这里插入图片描述

添加元素之后:g右高度T3 = T2 + T0 + 1,它的平衡因子就变成了2,导致祖先节点g失衡。在这里插入图片描述

解决方案:让g进行右旋转一次。让p的右子节点变为g的左子节点,将g变为p的右子节点,使p变为这颗子树的根节点。修改节点的parent指向,按顺序修改节点gp的高度。调整之后会不仅会使改树保持AVL树的平衡,也依然会保持搜索树的性质。

g.left = p.right;
p.right = g;

调整指向:
在这里插入图片描述
修改节点结构。
在这里插入图片描述

2.2.3 RR-右旋转(单旋)

RR:其右子节点的右子节点导致其失衡。

此时处于平衡状态:
在这里插入图片描述
T3添加一个子节点,会导致g的平衡因子变为-2。
在这里插入图片描述
解决方案:

 g.right = p.left;
 p.left = g;

然后让p成为子树的根节点。之后修改parent属性,按顺序修改gp的高度。

修改指向:
在这里插入图片描述
调整结构:
在这里插入图片描述

2.2.4 LR - RR 左旋转,LL右旋转(双旋)

如下图要在添加新节点(红色方块)时,需要先找左子节点再找右子节点,称为LR。

在这里插入图片描述

添加之后会时g的平衡因子变为2,导致失衡。
解决方案:

  1. 先对p进行左旋转
    左旋转之后图示:显然没有恢复平衡。
    在这里插入图片描述
  2. 再进行右旋转之后恢复平衡:
    在这里插入图片描述
2.2.5 RL - LL右旋转,RR 左旋转(双旋)

在添加时先找右子节点再找左子节点的情况称为RL。

在这里插入图片描述
解决方案:

  1. 先右旋转
    在这里插入图片描述

  2. 再左旋转
    在这里插入图片描述

  3. 恢复平衡
    在这里插入图片描述

2.2.6 接口设计

因为AVL树是由BST树的特殊分支,因此直接继承BST树即可。

  • BST树新增方法:由子类去实现
	 /**
     * 添加节点之后处理,由子类去实现
     * @param node
     */
    protected void afterAdd(Node<E> node){

    }

  • add()方法中调用:
   /**
     * 添加元素
     * @param e
     */
    public void add(E e){
        elementNotNullCheck(e);
        // 添加第一个节点
        if (root == null){
            root = new Node<>(e,null);
            size++;
            // 添加节点之后的处理
            afterAdd(root);
            return;
        }
        // 记录父节点
        Node<E> parent = root;
        // 记录当前比较的节点
        Node<E> node = root;
        // 记录比较值
        int cmp = 0;
        while (node != null){
            // 获取与当前节点的比较值
            cmp = compare(e, node.e);
            // 保存父节点
            parent = node;
            if (cmp > 0){ // 大于当前节点的值,就找右子节点
                node = node.right;
            }else if (cmp < 0){ // 小于当前节点的值就找左子节点
                node = node.left;
            }else { // 相等
                // 用传入的值覆盖掉原先该节点的值
                node.e = e;
                return;
            }
        }
        // 根据最后一次的比较值判断放在左子节点还是右子节点。
        Node<E> nowNode = new Node<>(e, parent);
        if (cmp > 0){
            parent.right = nowNode;
        }else {
            parent.left = nowNode;
        }
        // 添加节点之后的处理
        afterAdd(nowNode);
        size++;
    }
2.2.6 接口实现

这里主要实现的是afterAdd()方法。

2.2.6.1 完善AVL节点类
  • 新增height属性,因为BST树和二叉树中不需要去维护改属性,因此这里在AVL树中新增节点类并包含height属性,并继承BinaryTree.Node节点。
    private static class AVLNode<E> extends Node<E>{
        // 高度
        // 每次新增时的节点都是叶子节点,叶子节点的高度为1。 
        int height = 1;

        public AVLNode(E e, Node<E> parent) {
            super(e, parent);
        }
    }
  • 新增crateNode(E e, Node<E> parent)接口:因为BST树的添加方法中的节点是二叉树的节点,导致无法使用AVL树的节点去维护height属性。因此这里在二叉树中新增接口由子类去实现。
   /**
     * 创建节点接口
     * @param e
     * @param parent
     * @return
     */
    protected Node<E> createNode(E e, Node<E> parent){
        return new Node<>(e,parent);
    }
  • 修改add(E e):直接调用createNode即可。
    /**
     * 添加元素
     * @param e
     */
    public void add(E e){
        elementNotNullCheck(e);
        // 添加第一个节点
        if (root == null){
            root = createNode(e,null);
            size++;
            // 添加节点之后的处理
            afterAdd(root);
            return;
        }
        // 记录父节点
        Node<E> parent = root;
        // 记录当前比较的节点
        Node<E> node = root;
        // 记录比较值
        int cmp = 0;
        while (node != null){
            // 获取与当前节点的比较值
            cmp = compare(e, node.e);
            // 保存父节点
            parent = node;
            if (cmp > 0){ // 大于当前节点的值,就找右子节点
                node = node.right;
            }else if (cmp < 0){ // 小于当前节点的值就找左子节点
                node = node.left;
            }else { // 相等
                // 用传入的值覆盖掉原先该节点的值
                node.e = e;
                return;
            }
        }
        // 根据最后一次的比较值判断放在左子节点还是右子节点。
        Node<E> nowNode = createNode(e,parent);
        if (cmp > 0){
            parent.right = nowNode;
        }else {
            parent.left = nowNode;
        }
        // 添加节点之后的处理
        afterAdd(nowNode);
        size++;
    }

  • 重写接口:在AVL树中重写createNode接口:
    @Override
    protected Node<E> createNode(E e, Node<E> parent) {
        return new AVLNode<E>(e,parent);
    }
  • 计算平衡因子:在AVL树中新增计算平衡因子的方法。
        /**
         * 获取平衡因子
         * @return
         */
        public int balanceFactor(){
            int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
            int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
            return leftHeight - rightHeight;
        }
2.2.6.2 更新正常节点高度
  • 分析导致失衡节点因为失衡是添加节点导致的,因此找到最先失衡的父节点就可以处理掉失衡,因此需要一直往parent方向去找节点。
  • 判断是否平衡
    /**
     * 判断是否平衡
     * @param node
     * @return
     */
    private boolean isBalance(Node<E> node){
       return Math.abs(((AVLNode<E>)node).balanceFactor()) <= 1;
    }
  • 更新高度
    ①在AVLNode节点中新增更新高度方法:

            /**
             * 更新当前节点的高度
             */
            private void updateHeight(){
                int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
                int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
                height = 1 + Math.max(leftHeight,rightHeight);
            }
    

    ②在外层封装更新高度方法

        /**
         * 更新节点高度
         * @param node
         */
        private void updateHeight(Node<E> node){
            ((AVLNode<E>)node).updateHeight();
        }
    
2.2.6.3 恢复平衡
  • 在二叉树的节点类新增判断是左子节点还是右子节点操作

    		/**
             * 判断是不是父节点的左子树
             * @return
             */
            public boolean isLeftChild(){
                return parent != null && this == parent.left;
            }
    
            /**
             * 判断是不是父节点的右子树
             * @return
             */
            public boolean isRightChild(){
                return parent != null && this == parent.right;
            }
    
2.2.6.3.1 左旋转
  • RR情况:
    在这里插入图片描述
  • 修改指向
		Node<E> parent = grand.right;
        parent.right = grand.left;
        parent.left = grand;

在这里插入图片描述

  • 修改parent:
    	// 更新 p 的 parent 使成为当前子树的根节点
        parent.parent = grand.parent;
        if (grand.isLeftChild()){
            grand.parent.left = parent;
        }else if (grand.isRightChild()){
            grand.parent.right = parent;
        }else { // grand 是根节点
            root = parent;
        }

        // 更新 p.left 的 parent
        if (grand.right != null){
            grand.right.parent = grand;
        }

        // 更新 g 的 parent
        grand.parent = parent;

在这里插入图片描述

  • 更新高度
        // 更新高度
        updateHeight(grand);
        updateHeight(parent);
2.2.6.3.2 右旋转
  • LL情况:
    在这里插入图片描述
  • 修改指向
		Node<E> parent = grand.left;
        grand.left = parent.right;
        parent.right = grand;

在这里插入图片描述

  • 更新parent
        // 更新 p 的 parent 使成为当前子树的根节点
        parent.parent = grand.parent;
        if (grand.isLeftChild()){
            grand.parent.left = parent;
        }else if (grand.isRightChild()){
            grand.parent.right = parent;
        }else { // grand 是根节点
            root = parent;
        }

        // 更新 p.left 的 parent
        if (grand.left != null){
            grand.left.parent = grand;
        }

        // 更新 g 的 parent
        grand.parent = parent;

在这里插入图片描述

  • 更新高度
		// 更新高度
        updateHeight(grand);
        updateHeight(parent);
2.2.6.3.3 整合

既然左旋转和右旋转中更新部分的代码都是一样的(除更新parent的左或者右不同),不如直接封装成一个方法。

/**
     * 旋转之后 更新 parent 和 height
     * @param grand
     * @param parent
     * @param child
     */
    private void afterRotate(Node<E> grand,Node<E> parent,Node<E> child){
        // 更新 parent

        // 更新 p 的 parent 使成为当前子树的根节点
        parent.parent = grand.parent;
        if (grand.isLeftChild()){
            grand.parent.left = parent;
        }else if (grand.isRightChild()){
            grand.parent.right = parent;
        }else { // grand 是根节点
            root = parent;
        }

        // 更新 p.left 的 parent
        if (child != null){
            child.parent = grand;
        }

        // 更新 g 的 parent
        grand.parent = parent;

        // 更新高度
        updateHeight(grand);
        updateHeight(parent);
    }
  • 左旋转:
    /**
     * 左旋
     * @param grand
     */
    private void rotateLeft(Node<E> grand){
        Node<E> parent = grand.right;
        Node<E> child = parent.left;
        grand.right = child;
        parent.left = grand;
        afterRotate(grand, parent, child);
    }

  • 右旋转:
    /**
     * 右旋
     * @param grand
     */
    private void rotateRight(Node<E> grand){
        Node<E> parent = grand.left;
        Node<E> child = parent.right;
        grand.left = child;
        parent.right = grand;
        afterRotate(grand, parent, child);
    }
  • 整合恢复平衡方法:
	 /**
     * 恢复平衡
     * @param grand 最先(高度最低,最往下)失衡的节点
     */
    private void reBalance(Node<E> grand){
        // 获取node比较高的子节点
        Node<E> parent = ((AVLNode<E>)grand).tallerChild();
        Node<E> node = ((AVLNode<E>)parent).tallerChild();
        if (parent.isLeftChild()){ // L
            if (node.isLeftChild()){ // LL
                rotateRight(grand);
            }else { // LR
                rotateLeft(parent);
                rotateRight(grand);
            }
        }else { // R
            if (node.isLeftChild()){ // RL
                rotateRight(parent);
                rotateLeft(grand);
            }else { // RR
                rotateLeft(grand);
            }
        }
    }
  • 整合afterAdd()方法
	@Override
    protected void afterAdd(Node<E> node) {
        while (( node = node.parent ) != null){
            if (isBalance(node)){
                // 更新高度
                // 这里的node最起码就是新节点的父节点
                updateHeight(node);
            }else {
                // 恢复平衡
                // 这里是高度最低的不平衡节点
                reBalance(node);
                // 退出循环使树恢复平衡
                break;
            }
        }
    }
2.2.6.4 统一恢复平衡方法

如图可见,因为二叉搜索树是有序的,如果让其恢复平衡,可以套用一个统一的模板,只需要传入相应的节点即可。

在这里插入图片描述

    /**
     * 统一版恢复平衡
     * @param grand
     */
    private void reBalancePro(Node<E> grand){
        Node<E> parent = ((AVLNode<E>)grand).tallerChild();
        Node<E> node = ((AVLNode<E>)parent).tallerChild();
        if (parent.isLeftChild()){ // L
            if (node.isLeftChild()){ // LL
                rotate(grand,
                        node.left, node, node.right,
                        parent,
                        parent.right, grand, grand.right);
            }else { // LR
                rotate(grand,
                        parent.left, parent, node.left,
                        node,
                        node.right, grand, grand.right);
            }
        }else { // R
            if (node.isLeftChild()){ // RL
                rotate(grand,
                        grand.left, grand, node.left,
                        node,
                        node.right, parent, parent.right);
            }else { // RR
                rotate(grand,
                        grand.left, grand, parent.left,
                        parent,
                        node.left, node, node.right);
            }
        }
    }
	/**
     * 旋转
     * @param r 子树根节点
     * @param a
     * @param b
     * @param c
     * @param d
     * @param e
     * @param f
     * @param g
     */
    private void rotate(
            Node<E> r,
            Node<E> a, Node<E> b, Node<E> c,
            Node<E> d,
            Node<E> e, Node<E> f, Node<E> g){

        // 让 d 成为这颗子树的根节点
        d.parent = r.parent;
        if (r.isRightChild()){
            r.parent.right = d;
        }else if (r.isLeftChild()){
            r.parent.left = d;
        }else {
            root = d;
        }

        // a b c
        b.left = a;
        if (a != null){
            a.parent = b;
        }
        b.right = c;
        if (c != null){
            c.parent = b;
        }
        // 因为修改了 b 的左右子树,所以需要更新高度
        updateHeight(b);

        // e f g
        f.left = e;
        if (e != null){
            e.parent = f;
        }
        f.right = g;
        if (g != null){
            g.parent = f;
        }
        // 因为修改了 f 的左右子树,所以需要更新高度
        updateHeight(f);

        // b d f
        d.left = b;
        b.parent = d;
        d.right = f;
        f.parent = d;
        updateHeight(d);
    }
2.2.6.5 测试
  • BST
    在这里插入图片描述

  • AVL
    在这里插入图片描述

2.3 删除

执行删除时只可能会导致父节点删除,其它的节点不会有影响。

如图删除节点16
在这里插入图片描述
删除之后:只会改变其父节点15的平衡因子,不会改变其高度(除非只有被删除节点一个子节点,但是这种情况不会导致失衡),因此在只会造成其父节点或者祖先节点中的一个节点失衡。
在这里插入图片描述
删除的四种情况也和添加一样:但如果在恢复平衡的过程中改变了这颗子树的高度,就可能会导致这颗子树的父节点失衡,可能会一直持续到root节点,最坏可导致恢复O(logn)次。

2.3.1 设计接口
  • 在二叉搜索树中新增:
    /**
     * 删除节点之后的处理,由子类去实现
     * @param node
     */
    protected void afterRemove(Node<E> node){

    }
  • remove()方法中调用afterRemove,这里调用需要等节点真正删除完毕之后调用:
    /**
     * 根据传入的节点执行删除
     * @param node
     */
    private void remove(Node<E> node){
        if (node == null){
            return;
        }
        size--;

        // 判断度为2
        if (node.hasTwoChildren()){
            // 获取后继节点
            Node<E> s = successor(node);
            // 后继节点的值覆盖当前节点的值
            node.e = s.e;
            // 将node指向s节点
            node = s;
        }

        // 获取被删除节点的子节点,如果左子节点为空则获取右子节点;
        // 如果子节点都为空,则表示该节点是叶子节点返回null。
        Node<E> relacement = node.left != null ? node.left : node.right;

        if (relacement != null){ // node 是度为1的节点
            // 更改parent
            relacement.parent = node.parent;
            // 更改parent的left(right)的指向
            if (node.parent == null){ // node是度为1的根节点。
                root = relacement;
            }else if (node == node.parent.left){ // 被删除节点是父节点的左子节点
                node.parent.left = relacement;
            } else { // 被删除节点是父节点的右子节点
                node.parent.right = relacement;
            }
            // 删除之后处理
            afterRemove(node);
        }else if(node.parent == null){ // node是叶子节点并且是根节点
            root = null;

            // 删除之后处理
            afterRemove(node);
        }else { //node 是普通叶子节点
            if (node == node.parent.left){ // 当前节点是父节点的右子节点
                node.parent.left = null;
            }else { // 是父节点的左子节点
                node.parent.right = null;
            }

            // 删除之后处理
            afterRemove(node);
        }
    }

2.3.2 实现接口
    @Override
    protected void afterRemove(Node<E> node) {
        while (( node = node.parent ) != null){
            if (isBalance(node)){
                // 更新高度
                // 这里的node最起码就是新节点的父节点
                updateHeight(node);
            }else {
                // 恢复平衡
                // 这里是高度最低的不平衡节点
                reBalancePro(node);
                // 恢复之后不能退出,因为可能会导致父节点也失衡
            }
        }
    }

2.4 总结

  1. 添加:可能会导致所有的祖先节点都失衡,只要使其高度恢复平衡,整棵树就可以恢复平衡。
  2. 删除:只可能回导致父节点失衡,恢复平衡后可能会导致所有的祖先节点失衡。
  3. 平均复杂度:
    ①搜索:O(logn);
    ②添加:O(logn),最多需要O(1)次旋转操作;
    ③删除:O(logn),最多需要O(logn)次旋转操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值