AVL树的原理讲解-------java实现

前面讲到了二叉查找树,虽然能够很好的应用于大多数的场景,但是他们在最坏情况下性能还是很差的,如二叉查找树最坏情况下是一颗高度为N的树,显然不利于查找。因此我们需要让查找树保持一种平衡,如二叉查找树最优情况的状态一样。

一、AVL树基础

AVL树是一颗保持平衡的二叉查找树,保证了树的深度为O(logN)
其每个节点的左子树和右子树的高度都最多相差1
在这里插入图片描述
上图就是一颗AVL树,其任意节点的左孩子和右孩子的高度差不超过1。

但是当我们插入或删除一个节点时,很容易破坏AVL树的平衡度,因此我们需要一种操作,在插入或删除完成前重新对树进行平衡,恢复AVL树的性质,这种操作就是旋转

假设待平衡节点为 A,则不平衡的条件就是A 的两颗子树的高度差为2,因此就会出现四种情况,针对不同情况我们做不同的旋转操作:

  • 1.在A的左孩子的左子树进行插入 (单旋转,向右旋转)
  • 2.在A的左孩子的右子树进行插入 (双旋转,先左,再右)
  • 3.在A的右孩子的左子树进行插入 (双旋转,先右再左)
  • 4.在A的右孩子的右子树进行插入 (单旋转,向左旋转)

情况1:
如下图左树,插入了一个节点G后,在A点处失去了平衡(这里树的节点无意义,随意取的,只关注平衡性即可),因此我们需要在A处将树向右旋转,变成右边的树,可以想象成用手提着B,将其提起来,然后将B的右孩子变为A的左孩子
在这里插入图片描述
对应代码如下:
右旋转就先获取其左孩子left
1.node的左孩子变为left的右孩子
2.left的右孩子变为node
3.重新计算node和left的高度

 /**
     * 单旋转(右旋转)(情形1)
     * @param node
     * @return
     */
    private AVLNode<T>rotateRight(AVLNode<T> node){
        AVLNode<T> left=node.left;
        node.left=left.right;
        left.right=node;
        node.height=Math.max(height(node.left),height(node.right))+1;
        left.height=Math.max(height(node.left),node.height)+1;
        return left;
    }

情况4:
如下图1,在A的右孩子的右子树插入了节点G,此时在A节点失去了平衡,因此在A处向左旋转,原理同情况1。
在这里插入图片描述
实现如下:
左旋转就先获取其右孩子right
1.node的右孩子变为right的左孩子
2.right的左孩子变为node
3.重新计算node和right的高度

/**
     * 单旋转(左旋转)(情形4)
     * @param node
     * @return
     */
    private AVLNode<T> rotateLeft(AVLNode<T> node){
        AVLNode<T> right=node.right;
        node.right=right.left;
        right.left=node;
        node.height=Math.max(height(node.left),height(node.right))+1;
        right.height=Math.max(node.height,height(right.right))+1;
        return right;
    }

情况2:
如下图1,在A的左孩子B的右子树插入了节点D后,在A处失去了平衡,因此需要先对其左孩子B进行左旋转,变成了图2,不难发现图2的情况就是情况1,因此我们只要再对A进行右旋转就可以恢复树的平衡性。
之所以会变成情况1,是因为对B进行左旋转后,按照左旋转的特点,会将B的右子树G提升一个高度到B的位置,B自然就会下去一个高度,再加上一个新的G,自然也就变成了情况1。
在这里插入图片描述
如下图3,对A进行右旋转后,树已恢复平衡。
在这里插入图片描述
实现如下:
有了情况1和情况4的基础,再实现情况2就很简单:
1.先对A的左孩子左旋转
2.再对A进行右旋转

 /**
     * 双旋转(先左后右)(针对情形2)
     * @param node
     * @return
     */
    private AVLNode<T> doubleLeftAndRight(AVLNode<T> node){
        node.left=rotateLeft(node.left);
        return rotateRight(node);
    }

情况3:
如图1,在A的右孩子的左子树加了一个节点D,造成A处失去平衡,因此先对E进行右旋转变成图2,此时变成情况4,再对A进行左旋转变成图3,就恢复了平衡。
在这里插入图片描述
在这里插入图片描述
实现如下:

 /**
     * 双旋转(先右再左) (针对情形3)
     * @param node
     * @return
     */
    private AVLNode<T> doubleRightAndLeft(AVLNode<T> node){
        node.right=rotateRight(node.right);
        return rotateLeft(node);
    }

有了上面的基础,再对AVL树操作就不困难了。

二、数据结构

也是一个左孩子,一个右孩子,一个数据域,外加一个当前节点的高度,当然你也可以使用Key-Value结构,按Key进行构造树,一个key关联一个value,这里简单使用了一个泛型T

public class AVLTree<T extends Comparable<? super T>> {
 
    private AVLNode<T> root;
    //节点的左右孩子高度差不能超过该值
    private static final int HEIGHT_DIFFERENCE=1;

    public AVLTree(AVLNode<T> root) {
        this.root = root;
    }
}    

class AVLNode<T>{
    AVLNode(T t){
        this(t,null,null);
    }
    AVLNode(T data,AVLNode<T> lt,AVLNode<T> rt){
        this.data=data;
        left=lt;
        right=rt;
        this.height=0;
    }

    T data;
    AVLNode<T> left;
    AVLNode<T> right;
    int height;


}

AVL树返回最大值最小值以及contains方法同上一篇二叉查找树相同,不赘述。

三、插入一个元素

public void insert(T data){
        if (data==null){
            throw new IllegalArgumentException("数据为空");
        }
         root= insert(data, this.root);
    }

    private AVLNode<T> insert(T data,AVLNode<T> t){
        if (t==null){
            return new AVLNode<>(data,null,null);
        }
        int compareResult=data.compareTo(t.data);
        if (compareResult<0){
            t.left=insert(data,t.left);
        }else if (compareResult>0){
            t.right=insert(data,t.right);
        }else {
            ;
        }
        return balance(t);
    }

可以看出,除了最后一行,其余部分同二叉查找树相同。
每次insert()调用后,都要对当前节点进行重新平衡,即当找到插入的节点后,按递归调用栈的顺序,对路径上的每个节点进行重新平衡。
balance()节点平衡代码如下:

 /**
     * 对节点t进行平衡
     * @param t
     * @return
     */
    private AVLNode<T> balance(AVLNode<T> t) {
        if (t==null){
            return null;
        }
        if (height(t.left)-height(t.right)>HEIGHT_DIFFERENCE){
            //左边高
            if (height(t.left.left)>=height(t.left.right)){
                 //左子树的左子树高
                //此为情形1,在左子树的左子树插入了元素,直接右旋转
                t=rotateRight(t);
            }else {
                //左子树的右子树高
                //此为情形2,在左子树的右子树插入了元素,先左旋转再右旋转
                t=doubleLeftAndRight(t);
            }

        }else if (height(t.right)-height(t.left)>HEIGHT_DIFFERENCE){
            //右边高
            if (height(t.right.left)>height(t.right.right)){
                //右子树的左子树高了
                //此为情形3,在右子树的左子树插入了元素,先右旋转再左旋转
                t=doubleRightAndLeft(t);
            }else {
                //右子树的右子树高
                //此为情形4,在右子树的右子树插入了元素,直接左旋转
                t=rotateLeft(t);
            }
        }
        //重新计算该节点的高度
        t.height=Math.max(height(t.left),height(t.right))+1;
        return t;
    }
     private int height(AVLNode<T> t){
        return t==null?-1:t.height;
    }

四、删除一个元素

逻辑同二叉查找树,同插入一样,需要在每次remove调用后对当前节点进行平衡。

 /**
     * 删除数据
     * @param x
     */
    public void remove( T x )
    {
        root = remove( x, root );
    }
    private AVLNode<T> remove(T data,AVLNode<T> root){

        if (root==null){
            return null;
        }
        int compareResult=data.compareTo(root.data);
        if (compareResult<0){
            root.left=remove(data,root.left);
        }else if (compareResult>0){
            root.right=remove(data,root.right);
        }else if (root.left!=null&&root.right!=null){
            //待删除节点有两个孩子
            root.data=findMin(root).data;
            root.right=remove(root.data,root.right);
        }else {
            //待删除节点没有孩子或有一个孩子
            root=root.left==null?root.right:root.left;
        }
        //重新平衡树
        return balance(root);
    }

我们可以按照中序遍历返回AVL树的节点队列:

 /**
     * 返回数据队列
     * @return
     */
    public Queue<T>iterator(){
        if (isEmpty()){
            return null;
        }else {
            Queue<T>queue=new Queue<>();
           return iterator(root,queue);
        }
    }
    private Queue<T> iterator( AVLNode<T> t,Queue<T> queue )
    {
        if( t != null )
        {
             iterator( t.left,queue );
            queue.enqueue(t.data);
            iterator( t.right, queue);
        }
        return queue;
    }

测试:

 public static void main(String[] args) {
        AVLTree<Integer> avlTree = new AVLTree<>(new AVLNode<>(1));
        avlTree.insert(2);
        avlTree.insert(3);
        avlTree.insert(4);
        avlTree.insert(5);
        avlTree.insert(6);
        avlTree.insert(7);
        avlTree.insert(15);
        avlTree.insert(16);

        avlTree.insert(14);
        //中序遍历打印
        avlTree.printTree();
        System.out.println("==========删除节点15=========");
        
        avlTree.remove(15);
        Queue<Integer> queue = avlTree.iterator();
        while (!queue.isEmpty()){
            System.out.println(queue.dequeue());
        }
        Integer max = avlTree.findMax();
        System.out.println("最大元素:"+max);
        System.out.println("节点17是否存在"+avlTree.contains(17));
  }

结果:

1
2
3
4
5
6
7
14
15
16
==========删除节点15=========
1
2
3
4
5
6
7
14
16
最大元素:16
节点17是否存在false

总结:

AVL树总体上的实现和二叉查找树相同,只是为了重新让树恢复平衡需要在递归调用中对当前节点进行balance()。
AVL树的查找、插入或删除,最坏的情况下的复杂度均为O(log(n))

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值