用JAVA代码手动实现AVL树

一、概念

在实现二叉树之后,我们发现在某些极端的情况下,二叉树可能会发生退化,退化成复杂度为O(N) 的链表,为了避免这种情况,我们必须要给二叉树添加限定条件。

AVL树是一种带有平衡条件的二叉查找树,由Adelson-Velskii Landis于1962年首先提出。使二叉查找树成为AVL树的性质是:二叉查找树的每个节点的左子树和右子树的高度最多差1(空树的高度定义为-1)

如上图所示,左边的是AVL树,右边的就不是,因为其左子树和右子树的高度差为2。


 

二、平衡策略

要想让AVL始终保持平衡,我们需要一定的策略。

如上图左侧的AVL树,如果我们向其插入6,就会破坏AVL树的平衡,因为在8处的左子树高度为2,右子树高度为0。为了让右侧的树保持平衡,我们很容易想到可以像如下图那样旋转一次,使其重新保持平衡。

为了尽可能的减少节点的移动(提升效率),通过分析我们可以知道:因插入节点而破坏AVL树平衡的情况有四种。

假设我们将需要平衡的节点称为a,那么总共就是以下这四种情况

    1、a节点左子树的左子节点(如图1)。

    2、a节点右子树的右子节点(如图2)

    3、a节点左子树的右子节点(如图3)

    4、a节点右子树的左子节点(如图4)

上图中要平衡的a节点 就是图中的节点8,其中第1、2种情况是关于a节点镜像对称;第3、4种情况同样也是关于a节点镜像对称。1、2的情况下,为了保持平衡只需要单旋转一次即可;3、4的情况稍复杂点,需要经过双旋转才能保持平衡。(第3种情况第一次旋转变成第1种情况,第二次旋转就可以保证平衡)

新增的情况分析完了,那么删除呢?删除和新增情况基本类似。

如上图,将左侧的AVL树的节点10 删除,这便破坏了树的平衡,我们仔细看下删除10 以后的树结构,这和上面情况1结构很类似,只不过,删除情况下节点7 的左右子节点都是齐全的,而在情况1节点7 只有左子节点,于是我们在进行单旋转的时候要考虑到节点8 应该放的位置,如上图节点8 应该放在节点9 的左子节点位置。


 

三、代码实现

经过以上的分析,下面我们开始手动代码实现:

首先我们先定义出AVL树的节点,AVL树的节点定义和二叉树节点定义很类似,只不过AVL树要多出高度这个属性。

private static class AvlNode<T>{
    T element;
    AvlNode<T> left;
    AvlNode<T> right;
    int height;
    AvlNode(T element){
        this.element= element;
        this.left = null;
        this.right = null;
        this.height = 0;
    }
}

在定义好节点之后,我们根据上面分析的策略,实现单旋转,从上面的分析很容易看出,单旋转有两种:左旋转和右旋转。

// 左旋转,传入要平衡的节点a
private AvlNode<T> rotateWithLeftChild(AvlNode<T> a){
    AvlNode<T> k = a.left;
    a.left = k.right;
    k.right = a;
    //节点位置改变以后要同时改变相应节点的高度
    k.height = a.height;
    a.height--;
    return k;
}
// 右旋转,传入要平衡的节点a
private AvlNode<T> rotateWithRightChild(AvlNode<T> a){
    AvlNode<T> k = a.right;
    a.right= k.left;
    k.left = a;
    //节点位置改变以后要同时改变相应节点的高度
    k.height = a.height;
    a.height--;
    return k;
}
// 返回节点的高度,空树的高度为-1
private int height(AvlNode<T> a){
    return a == null ? -1 : a.height;
}

单旋转完成以后,我们开始完成双旋转,拿情况3来说,该操作应当是节点6先进行一次右旋转变成情况1,再进行一次左旋转完成平衡。

private AvlNode<T> doubleWithLeftChild(AvlNode<T> a) {
        a.left = rotateWithRightChild(a.left);
        return rotateWithLeftChild(a);
}
private AvlNode<T> doubleWithRightChild(AvlNode<T> a) {
        a.right= rotateWithLeftChild(a.right);
        return rotateWithRightChild(a);
}

单旋转和双旋转完成以后,接下来我们就要添加平衡的方法了,从AVL树的定义我们可以知道,如果节点a的左子树和右子树的高度差大于1,那么我们就应当触发单旋转或者双旋转来保持树的平衡

private AvlNode<T> balance(AvlNode<T> t) {
    if(t == null){
        return null;
    }
    if(height(t.left) - height(t.right) > 1){
        if( height(t.left.left) >= height(t.left.right) ){
            // 情况1
            t = rotateWithLeftChild(t);
        }else{
            // 情况3
            t = rotateWithLeftChild(t);
        }
    }
    if(height(t.right) - height(t.left) > 1){
        if(height(t.right.right) >= height(t.right.left)){
            // 情况2
            t = rotateWithRightChild(t);
        }else{
            // 情况4
            t = doubleWithRightChild(t);
        }
    }
    // 更新t节点的高度
    t.height = Math.max(height(t.left), height(t.right)) + 1;
    return t;
}

按照当前的理论,二叉树的平衡条件已经实现,接下来就是在我们之前实现的二叉查找树 的代码里面添加这些方法,并修改insert和remove方法,将这两个的方法的返回值改为  return balance(t) 即可。


 

四:测试验证

在之前实现二叉树后,我们逐一插入A、B、C、D、E进行测试,发现二叉树退化成了链表,现在我们添加平衡的方法后,再逐一插入A、B、C、D、E进行测试

@Test
public void AvlTreeTest() {
        AvlTree<String> t = new AvlTree<>();
        t.insert("A");
        t.insert("B");
        t.insert("C");
        t.insert("D");
        t.insert("E");
        t.printTree();
}

结果如下图展示

情况符合我们的预期,至此,我们的AVL树已经成功实现。

关于AVL树的实际应用,我们很容易就能想到其在查找方面具有很高的效率,因为整棵树的结构都是有序的,当查找一个值的时候,将该值和根节点的值对比,就能够确定要查找的值是在左子树下面还是在右子树下面,理论上其操作时间为O(logN) ,而链表的操作时间为O(N),随着数据量的增多,AVL树操作的效率提升越明显。


需要AVL树代码的朋友可以 关注本公众号,回复:AVL树代码 即可获取完整代码



本人因所学有限,如有错误之处,望请各位指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值