AVL树Java实现


AVL树(平衡二插搜索树)

1.概念

二插搜索树

要想了解AVL树,就得先知道二插搜树的性质:

  • 二插搜索树的左子树的值要小于父亲节点的值
  • 二插搜索树的右子树的值要大于父亲节点的值

在这里插入图片描述

如上图就是一棵二插搜索树

  • 二插搜搜树的最小值在左子树,最大值在右子树
  • 二插搜索树的中序遍历时一个有序序列

二插搜索树的查找效率正常情况下是 l o g 2 n log_{2}n log2n,但是在极端情况下如果这颗树转变成了单分支,也就是变成了链表形式,查找效率就是 O ( n ) O(n) O(n)了,这个时候AVL树的优势就来了。

在这里插入图片描述

AVL树的基本概念

AVL树又叫平衡二插搜索树,二插搜索树的查找效率在极端情况下是比较低的,而AVL树会保证左右子树的高度差的绝对值不会超过1,每次在插入新的节点后都会进行对应的调整,保证树的平衡。

一棵AVL数或者是空树,或者是会具有以下性质:

  • 它的左右子树都是AVL树
  • 左右子树高度只差(简称平衡因子)的绝对值不超过1(1/-1/0)

在这里插入图片描述

如果一棵 n n n个节点的二插搜索树的高度是平衡的,那么这个搜索树就是AVL树,那么可以它的高度就可以保持为 l o g 2 n log_{2}n log2n,查找的时间复杂度为 l o g 2 n log_{2}n log2n

2.AVL数的实现

定义AVL树

AVL树的每一个节点的定义方式如下:

  • bf为平衡因子:这里采用右子树高度-左子树高度来计算平衡因子(并不是唯一方式)
static class TreeNode {
    // 值
    int val;
    // 左子树高度-右子树高度
    int bf;// 平衡因子
    TreeNode left; //左孩子
    TreeNode right; // 右孩子
    TreeNode parent;// 父节点引用

    public TreeNode(int val) {
        this.val = val;
    }
}

AVL树的插入

AVL树遵循了搜索树的性质,按照搜索树的插入方式进行 插入就行

  • 第一步按照二插搜索树的方式插入节点
  • 第二步就是调整平衡因子,如果发现某一棵子树树已经不平衡就需要进行旋转

插入有几个逻辑:

  • 如果插入的元素比当前节点元素大,就插入到当前节点的右子树
  • 如果插入的元素比当前节点元素小,就插入到当前节点的左子树
  • 如果插入的元素和当前节点元素相等就插入失败
  • 插入新节点后,要将插入节点的parent指向其父节点

接着就是进行平衡因子的调整

  • 如果插入节点是在parent节点的左边,parent节点的平衡因子就减一
  • 同理如果插入节点在parent节点的右边,parent节点的平衡因子就加一

修改平衡因子后就需要进行判断,有三种情况:

  • 当前节点的平衡因子为0,说明插入之前树的平衡因子为 1 1 1或者- 1 1 1,插入节点后平衡因子变成0,此时满足AVL树的性质,插入成功。
  • 当前节点的平衡因子为1或者-1,说明当前子树是平衡的,但并不代表整个AVL树是平衡的,所以要继续从下往上修改对应路径上的平衡因子
  • 如果当前节点的平衡因子为2或者-2,说明当前树已经不平衡需要进行旋转。

在这里插入图片描述

上图是正常情况下的插入,插入元素后整棵树还是平衡的。但如果是其它情况就要进行旋转了:

/**
     * AVL树插入元素
     * @param val
     */
public boolean insert(int val) {
    TreeNode newNode = new TreeNode(val);
    if (root == null) {
        // 第一次插入
        root = newNode;
        return true;
    }
    TreeNode parent = null;
    TreeNode cur = root;
    while (cur != null) {
        parent = cur;
        if (cur.val > val) {
            cur = parent.left;
        } else if (cur.val == val) {
            System.out.println("插入失败元素已经存在");
            return false;
        } else {
            cur = parent.right;
        }
    }
    // 在对应位置插入新元素
    if (parent.val > val) {
        parent.left = newNode;
    } else {
        parent.right = newNode;
    }
    newNode.parent = parent;
    cur = newNode;
    // 调整平衡因子
    while (parent != null) {
        if (parent.left == cur) {
            parent.bf--;
        } else {
            parent.bf++;
        }

        if (parent.bf == 0) {
            // 说明所有树已经平衡,无需调整
            break;
        } else if (parent.bf == 1 || parent.bf == -1) {
            // 当前子树是平衡的,但不能说明整棵树平衡,继续向上调整
            cur = parent;
            parent = cur.parent;
        } else {
            if (parent.bf == 2) {
                if (cur.bf == 1) {
                    // 进行左旋
                    rotateLeft(parent);
                } else {
                    // cur.bf == -1
                    // 右左双旋
                    rotateRL(parent);
                }
            }else {
                //parent.bf == -2
                if (cur.bf == -1) {
                    // 进行右旋
                    rotateRight(parent);
                } else {
                    // cur.bf == 1
                    // 左右双旋
                    rotateLR(parent);
                }
            }
            // 调整完后树已经平衡
            break;
        }

    }
    return true;
}

AVL树的旋转

右单旋

当新节点插入到较高左子树的左侧,此时就会出现平衡因子为-2,其子节点为-1,就需要进行右单旋。右单旋其实就是降低左树的高度来提升右树的高度

在这里插入图片描述

右单旋步骤:

  • 先记录相关节点,parentL、parentLR、pParent
  • parentLR可能出现不存在的情况,如果存在则将该节点的的parent指向当前调整的parent
  • 将parent的left指向parentLR
  • 将parent的parent指向parentL
  • 再将parentL的right指向parent
  • 接着需要判断调整的节点是否是根节点
  • 如果是根节点只需要将,root指向parentL,再将parentL的parent置为null
  • 入过不是根节点则需要判断parent是pParent的左节点还是右节点,对应修改引用
  • 最后再调整对应的平衡因子

在这里插入图片描述

/**
 * 右单旋
 * @param parent
  */
private void rotateRight(TreeNode parent) {
    // 记录对应节点
    TreeNode parentL = parent.left;
    TreeNode parentLR = parentL.right;
    TreeNode pParent = parent.parent;
    // 如果parentLR存在
    if (parentLR != null) {
        parentLR.parent = parent;
    }
    parent.left = parentLR;
    parent.parent = parentL;
    parentL.right = parent;
    // 要调整的是根节点
    if (parent == root){
        root = parentL;
        parentL.parent = null;
    } else {
        // 如果不是根节点就需要判断,当前子树是parent的左子树还是右子树
        if (pParent.left == parent) {
            pParent.left = parentL;
        } else {
            pParent.right = parentL;
        }
        parentL.parent = pParent;
    }
    // 调整平衡因子
    parentL.bf = 0;
    parent.bf = 0;
}
左单旋

当把新节点插入到AVL树中较高右子树的右侧后,调整平衡因子发现节点的平衡因子为2且它的子树为1,此时就需要进行左单旋了。左单旋其实就是降低右树的高度来提升左树的高度。

在这里插入图片描述

左单选步骤:

  • 记录相关节点parentR、parentRL、pParent
  • 让parent的right指向parentRL
  • parentRL可能有不存在的情况,如果存在则让其的parent指向parent
  • 再让parent的parent指向parentR
  • 接着让parentR的left指向parent
  • 判断旋转的是否是根节点,如果是在pParent是为空的,所以要进行特殊判断
  • 最后更新平衡因子

在这里插入图片描述

/**
* 左单旋
* @param parent
*/
private void rotateLeft(TreeNode parent) {
    // 记录对应节点
    TreeNode parentR = parent.right;
    TreeNode parentRL = parentR.left;
    TreeNode pParent = parent.parent;

    // 修改节点
    parent.right = parentRL;
    // 如果parentRL存在
    if (parentRL != null) {
        parentRL.parent = parent;
    }
    parent.parent = parentR;
    parentR.left = parent;
    // 如果旋转的是根节点
    if (parent == root) {
        root = parentR;
        parentR.parent = null;
    } else {
        // 如果旋转的不是根节点就判断旋转的是pParent的左子树还是右子树
        if (pParent.left == parent) {
            pParent.left = parentR;
        } else {
            pParent.right = parentR;
        }
        parentR.parent = pParent;
    }
    // 更新平衡因子
    parent.bf = 0;
    parentR.bf = 0;

}
左右双旋

有些情况下,单纯对树进行左旋或者右旋还是无法保证树是平衡状态,所以此时就需要双旋。比如在较高左子树的右侧插入一个新元素,就需要进行左右双旋。

插入时需要考虑两种情况,一个是插入到左节点和插入到右节点:根据不同情况下的修改负载因子是不一样的,要进行特判。通过parentLR的平衡因子来判断新元素插入左节点还是右节点

插入到较高左子树的右侧的左节点

在这里插入图片描述

插入到较高左子树的右侧的右节点

在这里插入图片描述

假设我们以元素插入到较高左子树的右侧的右节点为例子:

  • 先对parentL进行左单旋

在这里插入图片描述

  • 再对parent进行右单旋

在这里插入图片描述

最后调整平衡因子有两种情况:

  • 通过记录的parentLR的平衡因子来判断修改,如果 p a r e n t L R . b f = = 1 parentLR.bf==1 parentLR.bf==1,说明新元素插入到了右节点,如果 p a r e n t L R . b f = = − 1 parentLR.bf==-1 parentLR.bf==1说明新元素插入到了左节点
  • 如果是记录的 b f = = − 1 bf==-1 bf==1,说明插入的元素在左子树,则需要修改对应3个节点的平衡因子, p a r e n t . b f = 1 parent.bf=1 parent.bf=1 p a r e n t L . b f = 0 parentL.bf=0 parentL.bf=0 p a r e n t L R . b f = 0 parentLR.bf=0 parentLR.bf=0的平衡因子
  • 如果记录的 b f = = 1 bf == 1 bf==1,说明插入的元素在右子树,则需要修对应3个节点的平衡因子, p a r e n t L . b f = − 1 parentL.bf=-1 parentL.bf=1 p a r e n t L R . b f = 0 parentLR.bf=0 parentLR.bf=0 p a r e n t . b f = 0 parent.bf=0 parent.bf=0
/**
     * 进行左右双旋
     * @param parent
     */
private void rotateLR(TreeNode parent) {
    // 记录相关节点
    TreeNode parentL = parent.left;
    TreeNode parentLR = parentL.right;
    int bf = parentLR.bf;
    // 先左旋parent.left
    rotateLeft(parentL);
    // 再右旋parent
    rotateRight(parent);
    // 修改平衡因子
    // 分两种情况
    if (bf == -1) {
        // 插入到较高左子树右侧的左子树
        parent.bf = 1;
        parentL.bf = 0;
        parentLR.bf = 0;
    } else if (bf == 1) {
        //bf == 1
        // 插入到较高左子树右侧的右子树
        parentL.bf = -1;
        parentLR.bf = 0;
        parent.bf = 0;
    }

}
右左双旋

右左双旋是当元素插入在较高右子树的左侧发生的。插入后要考虑两种情况,一个是元素插入在较高右子树的左侧的左节点,另外一种是元素插入在较高右子树的左侧的右节点。

插入到较高右子树左侧的左节点

在这里插入图片描述

插入到较高右子树左侧的右节点

在这里插入图片描述

以插入到较高右子树左侧的右节点为例子

  • 先对parentR进行右旋

在这里插入图片描述

  • 再对parent进行左旋

在这里插入图片描述

最后调整平衡因子有两种情况:

  • 通过记录parentRL的平衡因子来进行判断修改,如果 p a r e n t R L . b f = = 1 parentRL.bf==1 parentRL.bf==1说明新元素插入到了右节点,如果 p a r e n t R L . b f = = − 1 parentRL.bf==-1 parentRL.bf==1说明新元素插入到了左节点
  • 如果parentRL的平衡因子 b f = = 1 bf==1 bf==1,说明新元素插入到了右子树,则需要修改 p a r e n t . b f = = − 1 parent.bf==-1 parent.bf==1 p a r e n t R . b f = 0 parentR.bf=0 parentR.bf=0 p a r e n t R L . b f = 0 parentRL.bf=0 parentRL.bf=0
  • 如果parentRL的平衡因子 b f = = − 1 bf == -1 bf==1,说明新元素插入到了左子树,则需要修改 p a r e n t R . b f = 1 parentR.bf=1 parentR.bf=1 p a r e n t . b f = 0 parent.bf=0 parent.bf=0 p a r e n t R L = 0 parentRL=0 parentRL=0
/**
     * 进行右左双旋
     * @param parent
     */
private void rotateRL(TreeNode parent) {
    // 记录相关节点
    TreeNode parentR = parent.right;
    TreeNode parentRL = parentR.left;
    int bf = parentRL.bf;

    rotateRight(parentR);
    rotateLeft(parent);
    if (bf == -1) {
        parentR.bf = 1;
        parent.bf = 0;
        parentRL.bf = 0;
    } else if (bf == 1) {
        parent.bf = -1;
        parentR.bf = 0;
        parentRL.bf = 0;
    }
}

删除元素

AVL树删除元素,先要找到该元素再进行删除,但这里需要考虑到多种情况。

  • 要删除的是根节点
    • 删除的节点的左子树为空
    • 删除的节点的右子树为空
    • 删除的节点的左右子树都不为空
  • 要删除的不是根节点
    • 删除的节点的左子树为空
    • 删除的节点的右子树为空
    • 删除的节点的左右子树都不为空

针对左右不为空的情况采用替换删除:

  • 去删除节点的左子树找最大值,或者去删除节点的右子树找最小值
  • 更新平衡因子的时候这里和插入相反的
    • 如果删除后平衡因子是 1 1 1或者 − 1 -1 1,说明调整前的平衡因子是0,修改后变成-1和1,并不影响上一层,依旧是平衡的
    • 如果删除后平衡因子是 0 0 0,说明修改前平衡因子是 b f = = − 1 bf==-1 bf==1或者 b f = = 1 bf==1 bf==1,说明了把高的那一棵子树的节点删掉了,此时当前子树是平衡的,但并不代表上一层就是平衡的,所以要继续向上调整
    • 如果删除后更新平衡因子 b f = = 2 bf == 2 bf==2或者 b f = = − 2 bf == -2 bf==2,说明不平衡需要进行旋转

3. 验证AVL树

验证AVL树采用判断每一个子树的左右子树高度差作为判断(右子树高度 − - 左子树高度),同时验证父节点的平衡因子的是否对应该差值。

才用后序遍历进行减枝,从AVL树的叶子节点从底之顶进行判断,可以避免重复判断,只要有一棵子树不平衡就无需判断其它节点了。

时间复杂度 O ( n ) O(n) O(n)

空间复杂度 O ( n ) O(n) O(n)

/**
     * 判断是否AVL树
     * @return
     */
public boolean isBalanced() {
    return balanced(root) >= 0;
}
public int balanced(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int left = balanced(root.left);
    int right = balanced(root.right);
    if (right-left != root.bf) {
        System.out.println("节点:"+root+" 平衡因子出现问题");
        return -1;
    }
    // 当有一颗子树不平衡时就无需判断其它节点了
    if (left >= 0 && right >= 0 && Math.abs(right-left) < 2) {
        return Math.max(right,left)+1;
    } else {
        return -1;
    }
}

4.AVL树性能分析

AVL是一棵高度绝对平衡的二插搜索树,该树要求每个节点的左右子树高度差的绝对值不能超过1,这样可以保证其查询的时间复杂度为 O ( l o g 2 n ) O(log_{2}n) O(log2n),但如果频繁对AVL进行删除和插入操作,性能是非常低的。插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置 。所以如果需要一种查询速度快且数据有序的数据结构,并且只对这些数据进行查询就可以使用AVL树,一旦设计到插入和删除就不适合使用AVL树了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱敲代码的三毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值