程序员的内功心法-AVL树

接上篇文章《二叉搜索树》了解到二叉搜索树在极端情况也不能满足我们对于查询性能的要求。

二叉树的一些统计特性

  • 第n层最多的节点个数2n-1
  • 高度为h的二叉树,最多包含2h-1个节点,所以n个节点的二叉树的最小高度是log2n + 1
  • 查找成功时,查找次数不会超过树的高度h

二叉树查询性能的衡量

我们下面来使用 A - H字符来观察二叉搜索树在不同的插入顺序下构造的树的结果

自然顺序的平均查找长度为ASL=(1+ 2 + 3 + 4+ 5+ 6+ 7 +8) / 8 = 4.5

计算特定顺序的平均查找长度ASL=(1 + 2*2 + 3*4 + 4*1) / 8 = 2.6

当我们数据相同,但是采用不同的插入顺序,使平均查找长度不一样。所以我们要解决这个问题,先观察两个初始化方式两个树的特点,大致发现使用特定顺序初始化的树,感觉树的节点分布比较平衡。由于统计特点3和特点2,我们希望n个节点的二叉树的接近log2n + 1,那么我们就可以最大化的提升查询性能.

所以为了解决这个问题,我们引入新的二叉搜索树实现-平衡二叉树(AVL树)

AVL树内容定义

  • 平衡因子BalanceFactor:左右子树的高度差BF=HL - HR
  • 规定左右子树的高度差的绝对值不超过1 |BF| ≤ 1

节点定义

原有节点的基础上增加height属性

class AVLNode<T extends Comparable<T>> {

    private T data;

    //左节点
    private AVLNode<T> left;

    //右节点
    private AVLNode<T> right;

    //当前节点的高度
    private int height;
}

高度计算

由于平衡二叉树的平衡指高度方面的平衡,我们先来计算树的高度

树的高度H指:左HL右HR子树高度的最大值 + 1

int height(AVLNode<T> node){
    if (Objects.isNull(node)) {
        return 0;
    }
    int rHeight = height(node.getRight());
    int lHeight = height(node.getLeft());
    return Math.max(rHeight, lHeight) + 1;

查找

由于平衡二叉树也是二叉查找树的一种,查询方式和二叉搜索树相同,不再赘述。

调整平衡

为了保证左右平衡,所以我们一系列的操作来维持左右子树的高度在BF规定的范围之内

插入分类

空树时,直接初始化为根结点。

针对作为子节点的插入,插入节点只能为被插入节点的左节点B或者右节点F。而被插入节点D可以是其父节点G的左节点或其父节点A的右节点。所以我们将所有情况分为4类GDB路径(LL插入)、GDF路径(LR插入)、ADF路径(RR插入)、ADB路径(RL插入)

接下来我们将处理所有的情况

RR插入

当插入节点在右子树的右节点上(ADF路径

操作步骤:

  1. 将右子节点D作为根节点
  2. 原根节点A作为新根节点D的左子节点
  3. 将D节点的左子节点B设置为原根节点A的右子节点

实现代码如下:

AVLNode<T> singleRightRotation(AVLNode<T> node) {
    AVLNode<T> result = node.getRight();
    AVLNode<T> left = result.getLeft();
    node.setRight(left);
    result.setLeft(node);
    return result;
}

LL插入

当插入的节点在左子树的左节点上(GDB路径

操作步骤:

  1. 将左子节点D作为根结点
  2. 原根节点G作为新根节点D的右子节点
  3. 将D节点的右子节点F作为原结点G的左子节点

实现代码:

AVLNode<T> singleLeftRotation(AVLNode<T> node) {
    AVLNode<T> result = node.getLeft();
    AVLNode<T> right = result.getRight();
    node.setLeft(right);
    result.setRight(node);
    return result;

RL插入

当插入的节点在右子树的左节点上(ADB路径)

操作步骤:

  • 针对A节点的右子节点D做左旋转
  • 针对A节点做右旋转

实现代码:

AVLNode<T> doubleRightLeftRotation(AVLNode<T> node){
    AVLNode<T> right = singleLeftRotation(node.getRight());
    node.setRight(right);
    return singleRightRotation(node);
}

LR插入

当插入的节点在右子树的左节点上(GDF路径)

操作步骤:

  • 针对G节点的左子节点D做右旋转
  • 针对G节点做左旋转

实现代码:

AVLNode<T> doubleLeftRightRotation(AVLNode<T> node) {
    AVLNode<T> left = singleRightRotation(node.getLeft());
    node.setLeft(left);
    return singleLeftRotation(node);
}

删除节点

我们在删除节点时,思路:

  • 叶子节点直接删除
  • 包含一个子节点,将子节点替换到父节点
  • 包含两个子节点,使用后继节点替换被删除节点,删除后继节点即可 更详细的可以回顾下《二叉搜索树》的内容

平衡调整的思路:节点被删除后,相当于在兄弟节点插入新的节点

代码如下:

        return null;
    }
    T nodeData = node.getData();
    int flag = data.compareTo(nodeData);
    if (flag > 0) { //右子树
        AVLNode<T> right = delete(node.getRight(), data);
        node.setRight(right);
        AVLNode<T> lNode = node.getLeft();
        int rHeight = getHeight(right);
        int lHeight = getHeight(lNode);
        int bf = lHeight - rHeight;
        if (bf == 2) {//右子树被删除节点,不平衡
            //查看左兄弟节点,如果左兄弟有右子节点高度大于左子节点需要进行左右旋转 (删除情况2)
            if (getRightNodeHeight(lNode) > getLeftNodeHeight(lNode)) {
                node = doubleLeftRightRotation(node);
            } else {    //右节点的高度小于或者等于左子节点的高度,左单旋即可(删除情况1)
                node = singleLeftRotation(node);
            }
        }
    } else if (flag < 0) { //左子树
        AVLNode<T> left = delete(node.getLeft(), data);
        node.setLeft(left);
        AVLNode<T> right = node.getRight();
        int lHeight = getHeight(node.getLeft());
        int rHeight = getHeight(right);
        int bf = rHeight - lHeight;
        if (bf == 2) {//左子树被删除节点,不平衡
            //查看右兄弟节点,如果左子节点高度大于右子节点高度,进行右左旋转 (删除情况4)
            if ( getLeftNodeHeight(right) > getRightNodeHeight(right)) {
                node = doubleRightLeftRotation(node);
            } else {
                //左子树的高度小于等于右子节点的高度,左单旋转即可(删除情况3)
                node = singleRightRotation(node);
            }
        }
    } else { //found
        if (Objects.nonNull(node.getLeft()) && Objects.nonNull(node.getRight())) {  //存在左右子节点
            AVLNode<T> rMin = findMin(node.getRight()); //后继节点替代
            node.setData(rMin.getData());
            delete(node.getRight(), rMin.getData());    //删除后继节点
        } else {
            node = Objects.isNull(node.getLeft()) ? node.getRight() : node.getLeft();
        }
    }
    if (Objects.nonNull(node)) {
        buildHeight(node);
    }
    return node;
}

由于AVL是一个高度严格平衡的二叉搜索树,查找效率在log2n级别。但是在维护节点高度平衡时,需要进行旋转操作(插入时最多两次旋转;删除节点时AVL树需要调整整个查询路径的高度平衡,最多需要log2n次旋转)

后面,我们将介绍另外一种平衡搜索二叉树(红黑树)!

系列

《程序员内功心法——搜索二叉树》

《程序员内功心法——AVL树》

欢迎大家关注留言分享和我交流!

  

猜你喜欢

转载自blog.csdn.net/zhangjing1019/article/details/116307140