ok,那么今天来分享下,一种高阶数据结构——AVL树

之前分享的数据结构中,已经介绍过了二叉搜索树。

这个二叉搜索树又称二叉排序树

其性质如下:

——如若它的左子树不为空,则左子树上的所有节点的值都小于根节点的值

——如若它的右子树不为空,则右子树上的所有节点的值都大于根节点的值

——它的左右子树也分别为二叉搜索树。

虽然二叉搜索树可以缩短查找的效率,最优情况下:O(log2​n) 最坏情况下:O(n)

AVL树

那么把它给“进化”一下,使它向二叉搜索树中插入新节点后,还能保证插入的每个节点的左右子树高度之差的绝对值不超过1

那么它就是变成了AVL树。

其性质如下:

——它的左右子树都是AVL树

——左右子树高度差的绝对值不超过1,既可以1 / -1 /0

举个例子:

此时这就是一个AVL树

那么绿色标记的是左右子树的高度差——平衡因子

这里我规定为计算其高度差:右树高度 减 左树高度

当然也可以规定:左树高度减去右树高度

所以说,一颗二叉搜索树是高度平衡的,它就是AVL树,如果它有n个节点,其高度可保持在

O(log2n),这里是指log以2为底的n,搜索时间复杂度同样也为此。

那么接下来到实现环节

AVL树的节点是如何定义的呢?

static class TreeNode{
        public int val;//节点的值
        public int bf;//节点的平衡因子
        public TreeNode left;//左孩子
        public TreeNode right;//右孩子
        public TreeNode parent;//双亲节点

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

我们可以定义一个类来存储节点的信息

那么可以看到,和之前二叉搜索树定义是差不多的,多了个平衡因子和和双亲节点

平衡因子出现,是因为,高度平衡的原因。

那么为什么还要多个父亲节点呢?下文会说到,这里方便后续的旋转操作

接着,我们先来实现下插入操作

插入操作

首先这个插入一部分操作是和二叉搜索树是差不多的

因为AVL树实在二叉搜索树基础上改进的

 public TreeNode root;
    //插入操作
    public boolean  insert(int val){
        if(root==null){
            root=new TreeNode(val);
            return true;
        }
        TreeNode  parent=null;
        TreeNode   cur=root;
        while (cur!=null){
            if(cur.val<val){
                parent=cur;
                cur=cur.right;
            } else if (cur.val==val) {
                return false;
            }else {
                parent=cur;
                cur=cur.left;
            }
        }
        TreeNode newNode=new TreeNode(val);
        if(parent.val<val){
            parent.right=newNode;
        }else {
            parent.left=newNode;
        }

思路:如若当前节点是空的,那么插入节点成为新节点,

如若当前节点不为空,那么判断下当前的节点值和插入的节点值,当前的节点值大于插入的节点值,那么就往左边寻找,反之,就从右边寻找,如果等于,那就退出,不能插入相同的节点值

找到合适插入的叶子节点之后,那么判断当前的节点值与插入节点值大小,进行向左还是向右插入。

ok,此时完成了插入操作,那么问题就要来了,

如何进行平衡因子的调整

举个例子

这里往左插入一个,那么此时10这个节点的平衡因子-1,原本为0,减1,等于-1,因为之前规定平衡因子是右树-左树高度的

同理可得,那么此时10这个节点的平衡因子就调为+1,即 1

所以代码可以这样写

 //此时插入完成后,要进行平衡因子的调节
        //新节点的父亲节点为parent
        newNode.parent=parent;
        cur=newNode;
        while (parent!=null){
            if(parent.right==cur){
               //如果插入节点等于父亲节点的右边,那么bf++,假设数的高度等于右树减左树
                parent.bf++;
            }else {
                //如果插入节点等于父亲节点的左边,那么bf--,
                parent.bf--;
            }

当然这里要找到我们的10这个节点,那么10这个节点是作为左右子树的父亲节点,那么意思是找父亲节点

那么此时我们插入值的时候,先让插入节点的父亲节点保存此时的父亲节点信息

举个例子:

假设我们要插入的是18这个节点,那么插入之后,就要让18的父亲节点保存此时的parent,此时的parent是15这个节点

把父亲节点保存之后,再让为空的cur保存下新节点(newNode)

接着呢,就要开始修改下平衡因子,插入在左边呢,就减1,插入在右边呢,就+1

那么你插入完成之后,还得要进行一个重要的事,就是判断下当前的平衡因子是否是符合范围的,如果不符合,那么就要进行相对应的操作

比如当平衡因子为0时:

那么此时意味着我们的AVL树是不需要进行任何操作的

当平衡因子等于1 / -1,以及平衡因子等于2时

此图中,当20的平衡因子等于1的时候,就证明整个树是平衡了吗?显然是不是的

可以看出15这个节点的平衡因子是2,大于1,所以也是不平衡的,由此可得

当然,换过来,-1的时候,不一定也是平衡的。

所以,我们得出,如若是-1 或者 1,那么要向上回溯,看看是否都平衡了

如若当前的节点的平衡因子不等于0 / 1 / -1 那么必须要进行相关操作了

那么上诉代码,可以这样写:

 //此时判断下平衡因子
            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 {
                        //此时走到,意味着parent.bf=2,cur.bf=-1,出现异号,此时需要右左双旋
                        rotateRL(parent);
                    }
                }else {
                    //parent.bf==-2
                    //此时是左数高,需要往右转,即降低左数的高度,即右单旋
                    if(cur.bf==-1){
                        rotateRight(parent);
                    }else {
                        //此时到这里,意味着parent.bf=-2,cur.bf=1,是异号的,所以进行左右双旋
                        rotateLR(parent);
                    }
                }
             //此时完成旋转之后,就平衡了
             break;
            }
        }
        return true;
    }

那么此时,如若发现当前的节点的平衡因子等于2或者-2时,我们的操作是对树进行旋转。

那么旋转呢又分四种类型

右单旋、左单旋、左右单旋、右左单旋

那么接下来一一介绍,哪种情况,对应哪种旋转,

右单旋

举个例子:

此时当一棵树进行这样的插入,直到插入10这个节点,然后再往10这个节点的左边,再插入5,意思是往较高左子树的左侧插入一个节点

那么此时平衡因子是有问题的

30节点没问题,60节点的平衡因子有问题了,它等于-2

那么此时parent和cur在哪呢?

如图所示

为什么到这里?前面的代码有说过的

后面就不再叙说了

此时看着刚刚给到的树图片,很明显发现左边树高了,那么此时我们就要进行旋转,把左树高度降低,然后匀节点到右树那边

那么此时你看,右边是右单旋的结果,这棵树的平衡因子是都不超过2的,所以达到平衡。

那么是如何移动的呢?

思路:

首先让30的左边连接60这个节点

然后让60的左边连接30的右边,此时连接到这里,那么还是符合二叉搜索树的。

那么接着,要修改下它们的父亲节点是谁了

旋转后,60的父亲节点是30,40的父亲节点是60

此时,还得修改最重要的一个东西,平衡因子,可以发现,我们动了30、60、40这个几个节点,它们最终的平衡因子都是为0的

那么接下来,我们其实可以用几个临时节点来进行操作移动的,如上图所示

那么上代码:

private void rotateRight(TreeNode parent){
        //首先定义两个节点
        TreeNode subL=parent.left;
        TreeNode subLR=subL.right;
        parent.left=subLR;
        subL.right=parent;
        //有可能subL的左边节点为空,那么就要判断一下
        if(subLR!=null){
            subLR.parent=parent;
        }
        TreeNode pParent=parent.parent;
        //修改parent.parent的节点之前要保存一下parent.parent,有可能当前的parent上面还有一个节点
        parent.parent=subL;
        //下面修改subL的指向
        if(parent==root){
            //parent就是头节点
            root=subL;
            root.parent=null;
        }else {
            if (pParent.left == parent) {
                //如果parent的父亲节点左边等于parent,那么代表着parent左边要连接subL
                pParent.left = subL;
            } else {
                pParent.right = subL;
            }
            //无论怎么样,subL的父亲节点永远都是pParent
            subL.parent=pParent;
        }

        //修改完指向后,那么就要修改下此时平衡因子
        subL.bf=parent.bf=0;
    }

 if(subLR!=null){
            subLR.parent=parent;
        }

这里值得说下

如若我们的30节点下没有40这个节点,那么此时不去判断就去执行这个subLR.parent=parent;

就会报空指针异常

TreeNode pParent=parent.parent;

为什么要进行这一步呢,因为不能保证30的节点上是否还有父亲节点,所以也要进行判断修改下,

但是parent.parent=subL;这一步已经修改了这个parent 即30父亲节点的指向,所以要提前把parent的父亲节点保存下来。

ok,到这里,右单旋讲完了

接下来讲讲左单旋

左单旋

类比上上面的,当树出现不平衡,且左树较高时,那么就进行了右单旋

现在是这样的情况,同样是树出现了不平衡,这时候是右树较高了,那么我们就进行左单旋

比如上图,30这个节点的平衡因子就是2,40节点的平衡因子是1

很明显就要进行旋转,把一些节点进行左旋

类似于右单旋的操作,定义两个临时节点来进行旋转

思路:

40节点的左边连接30节点,30的左边连接35这个节点,

然后接着修改30、40、35这几个节点的父亲节点

然后接着修改下它们平衡因子,

思路和右单旋是差不多的

 public void rotateLeft(TreeNode parent){
        //首先定义两个节点
        TreeNode subR=parent.right;
        TreeNode subRL=subR.left;
        parent.right=subRL;
        subR.left=parent;
        //subRL此时有可能为空,需要判断下
        if(subRL!=null){
            subRL.parent=parent;
        }
        //此时有可能parent的parent不为空,所以修改parent.parent指向之前,保存一下
        TreeNode pParent=parent.parent;
        parent.parent=subR;
        //到这里要修改下parent的parent的指向了
        if(root==parent){
            root=subR;
            root.parent=null;
        }else {
            if(pParent.left==parent){
                pParent.left=subR;
            }else {
                pParent.right=subR;
            }
            subR.parent=pParent;
        }
        //到这里进行修改平衡因子
        subR.bf=parent.bf=0;
    }

此时,我们也可以发现,左单旋和右单旋,代码是差不多,只不过修改的一些节点是不一样而已。

那么此时,我们可以发现这样一个现象

当parent的平衡因子=2,cur所在节点的平衡因子是1,那么此时是左单旋

如果是parent所在节点的平衡因子=-2,cur所在节点的平衡因子是-1,此时是右单旋

那么有没有出现,paren所在节点t为正,cur所在节点所在为负,或者parent所在节点为负,cur所在的节点为正呢?

接下来就讲讲左右单旋

左右单旋

这样情况,就是有着parent的平衡因子是 -2,cur的平衡因子是 1

那么此时如若我们进行单纯的左右单旋

比如进行右旋

很明显此时树还是不平衡的,

同理,单纯的左旋也是不行的。

那么如何做呢?

我们先要进行左旋,再进行右旋

​编辑

为什么会有左右双旋,会有两个情况呢?

因为在25这个节点进行插入的时候,插入25左节点和右节点,其情况是不一样的,对应的就有两个图了

那么和左单旋、右单旋是有点点类似的,那么就是说旋转完后,也要进行平衡因子的修改

此时发现,我们图A定义的节点中,subR的平衡因子等于-1,parent的平衡因子等于0,subRL的平衡因子等于0

图B定义的节点中,subR的平衡因子是0,parent平衡因子是1,subRL平衡因子是0

那么如何进行区别呢?

可以看到导致这两个的情况发生,是当初插入23节点时,插入25的左边还是右边,才会发生的

所以我们判断下25那个的平衡因子即未插入前   subRL的平衡因子是1,还是-1就行

如若是-1,对应的是图B,如若是1,那么对应的是图A

那么代码如下
 

public void rotateLR(TreeNode parent){
        //涉及到旋转,就要进行到平衡因子的修改,所以要记录一下
        TreeNode subL=parent.left;
        TreeNode subLR=subL.right;
        int bf=subLR.bf;
        //进行左右双旋
        rotateLeft(parent.left);
        rotateRight(parent);
        //进行平衡因子的修改
        if(bf==-1){
            subLR.bf=0;
            subL.bf=0;
            parent.bf=1;
        }else if(bf==1){
            subLR.bf=0;
            subL.bf=-1;
            parent.bf=0;
        }
    }

ok,左右单旋讲完了

那么同理讲讲右左单旋吧

右左单旋

右左单旋,意味是parent的平衡因子是2,cur对应节点的平衡因子是-1,是异号的

举个例子

此时同样的,单一进行右单旋,或者左单旋也是不行的

所以现在进行右左双旋

同样的因为插入18后面的节点,放在左右位置不同,导致会有两种情况,是和左右双旋差不多的

其他的平衡因子的修改是通过这个subRL的平衡因子是1,还是-1来进行两种不同情况的修改

 public void rotateRL(TreeNode parent){
        //涉及到旋转,就要进行到平衡因子的修改,所以要记录一下
        TreeNode subR=parent.right;
        TreeNode subRL=subR.left;
        int bf=subRL.bf;
        //进行右左双旋
        rotateRight(parent.right);
        rotateLeft(parent);
        //进行平衡因子的修改
        if(bf==-1){
            parent.bf=-1;
            subRL.bf=0;
            subR.bf=0;
        }else if(bf==1){
            subR.bf=1;
            subRL.bf=0;
            parent.bf=0;
        }
    }

那么到这里呢,四个旋转也讲完了

那么这个插入操作,就是这样子,

删除操作

那么删除操作呢?

这里不做实现了

不过有相对应的一些思路:

就像是和之前二叉搜索树,有点类似的,找到删除节点,然后找到另一个”替死鬼“(参照二叉搜索数的删除操作),然后进行更新平衡因子,然后不平衡,进行旋转

那么问题又来了?如何判定是不是平衡的呢?

判断是否平衡

是否平衡,那么平衡因子起到关键作用

而平衡因子呢,又是右树高度-左树高度得到的

此时,又涉及到树的高度,那么此时得先求树的高度

比如,求到了当前节点的树左右子树的高度,那么此时进行右子树高度减去左子树高度,看看绝对值是否小于1,

还有一个需要注意,我们的平衡因子是通过bf++/bf--计算得来,万一,本来就计算错了

所以不能单纯看右子树高度减去左子树高度,看看绝对值是否小于1,

还要看看此时的平衡因子和左右子树的高度差是否相等

但当前节点平衡,下面左右子树就一定平衡了?

显然是不一定,那么此时需要进行从当前节点开始判断下,左树是否平衡,右树是否平衡

代码如下

 //求树的高度,这个是涉及到检查AVL树是否平衡的操作
    private int height(TreeNode root){
        if(root==null){
            return 0;
        }
        int leftH=height(root.left);
        int rightH=height(root.right);
        return leftH > rightH ? leftH+1 : rightH+1;
    }
    public boolean isBalanced(TreeNode root){
        if(root==null){
            return true;
        }
        int left=height(root.left);
        int right=height(root.right);
        //存在一个可能,就是平衡因子本来就是计算错的,所以为了防止这个
        if((right-left)!=root.bf){
            System.out.println("当前节点平衡因子出现问题:"+root.val);
            return false;
        }
        //此时看它的平衡因子是否小于,以及左右树是否符合当前的条件
        return Math.abs(left-right)<=1 && isBalanced(root.left) && isBalanced(root.right);
    }

树的高度,就使用了递归去求,找到左边的高度,和右边的高度,一比较,谁大,谁的高度再+1

+1是说明,是要包含此时根节点

到这里已全部讲完所学到的AVL树。

后面附上源码:

public class AVL {
    //首先AVL树在二叉搜索树的基础上完成的,首先创建个二叉搜索树
    static class TreeNode{
        public int val;
        public int bf;
        public TreeNode left;
        public TreeNode right;
        public TreeNode parent;

        public TreeNode(int val){
            this.val=val;
        }
    }
    public TreeNode root;
    //插入操作
    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){
            if(cur.val<val){
                parent=cur;
                cur=cur.right;
            } else if (cur.val==val) {
                return false;
            }else {
                parent=cur;
                cur=cur.left;
            }
        }
        if(parent.val<val){
            parent.right=newNode;
        }else {
            parent.left=newNode;
        }
        //此时插入完成后,要进行平衡因子的调节
        //新节点的父亲节点为parent
        newNode.parent=parent;
        cur=newNode;
        while (parent!=null){
            if(parent.right==cur){
               //如果插入节点等于父亲节点的右边,那么bf++,假设数的高度等于右树减左树
                parent.bf++;
            }else {
                //如果插入节点等于父亲节点的左边,那么bf--,
                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 {
                        //此时走到,意味着parent.bf=2,cur.bf=-1,出现异号,此时需要右左双旋
                        rotateRL(parent);
                    }
                }else {
                    //parent.bf==-2
                    //此时是左数高,需要往右转,即降低左数的高度,即右单旋
                    if(cur.bf==-1){
                        rotateRight(parent);
                    }else {
                        //此时到这里,意味着parent.bf=-2,cur.bf=1,是异号的,所以进行左右双旋
                        rotateLR(parent);
                    }
                }
             //此时完成旋转之后,就平衡了
             break;
            }
        }
        return true;
    }

    /**
     * 右单旋
     * @param parent
     */
    private void rotateRight(TreeNode parent){
        //首先定义两个节点
        TreeNode subL=parent.left;
        TreeNode subLR=subL.right;
        parent.left=subLR;
        subL.right=parent;
        //有可能subL的左边节点为空,那么就要判断一下
        if(subLR!=null){
            subLR.parent=parent;
        }
        TreeNode pParent=parent.parent;
        //修改parent.parent的节点之前要保存一下parent.parent,有可能当前的parent上面还有一个节点
        parent.parent=subL;
        //下面修改subL的指向
        if(parent==root){
            //parent就是头节点
            root=subL;
            root.parent=null;
        }else {
            if (pParent.left == parent) {
                //如果parent的父亲节点左边等于parent,那么代表着parent左边要连接subL
                pParent.left = subL;
            } else {
                pParent.right = subL;
            }
            //无论怎么样,subL的父亲节点永远都是pParent
            subL.parent=pParent;
        }
        //修改完指向后,那么就要修改下此时平衡因子
        subL.bf=parent.bf=0;
    }

    /**
     * 左单旋
     * @param parent
     */
    public void rotateLeft(TreeNode parent){
        //首先定义两个节点
        TreeNode subR=parent.right;
        TreeNode subRL=subR.left;
        parent.right=subRL;
        subR.left=parent;
        //subRL此时有可能为空,需要判断下
        if(subRL!=null){
            subRL.parent=parent;
        }
        //此时有可能parent的parent不为空,所以修改parent.parent指向之前,保存一下
        TreeNode pParent=parent.parent;
        parent.parent=subR;
        //到这里要修改下parent的parent的指向了
        if(root==parent){
            root=subR;
            root.parent=null;
        }else {
            if(pParent.left==parent){
                pParent.left=subR;
            }else {
                pParent.right=subR;
            }
            subR.parent=pParent;
        }
        //到这里进行修改平衡因子
        subR.bf=parent.bf=0;
    }

    /**
     * 左右双旋
     * @param parent
     */
    public void rotateLR(TreeNode parent){
        //涉及到旋转,就要进行到平衡因子的修改,所以要记录一下
        TreeNode subL=parent.left;
        TreeNode subLR=subL.right;
        int bf=subLR.bf;
        //进行左右双旋
        rotateLeft(parent.left);
        rotateRight(parent);
        //进行平衡因子的修改
        if(bf==-1){
            subLR.bf=0;
            subL.bf=0;
            parent.bf=1;
        }else if(bf==1){
            subLR.bf=0;
            subL.bf=-1;
            parent.bf=0;
        }
    }

    /**
     * 右左双旋
     * @param parent
     */
    public void rotateRL(TreeNode parent){
        //涉及到旋转,就要进行到平衡因子的修改,所以要记录一下
        TreeNode subR=parent.right;
        TreeNode subRL=subR.left;
        int bf=subRL.bf;
        //进行右左双旋
        rotateRight(parent.right);
        rotateLeft(parent);
        //进行平衡因子的修改
        if(bf==-1){
            parent.bf=-1;
            subRL.bf=0;
            subR.bf=0;
        }else if(bf==1){
            subR.bf=1;
            subRL.bf=0;
            parent.bf=0;
        }
    }
    //中序遍历可以获得一个有序的数据
    public void inorder(TreeNode root){
        if(root==null){
            return;
        }
        inorder(root.left);
        System.out.print(root.val+" ");
        inorder(root.right);
    }
    //求树的高度,这个是涉及到检查AVL树是否平衡的操作
    private int height(TreeNode root){
        if(root==null){
            return 0;
        }
        int leftH=height(root.left);
        int rightH=height(root.right);
        return leftH > rightH ? leftH+1 : rightH+1;
    }
    public boolean isBalanced(TreeNode root){
        if(root==null){
            return true;
        }
        int left=height(root.left);
        int right=height(root.right);
        //存在一个可能,就是平衡因子本来就是计算错的,所以为了防止这个
        if((right-left)!=root.bf){
            System.out.println("当前节点平衡因子出现问题:"+root.val);
            return false;
        }
        //此时看它的平衡因子是否小于,以及左右树是否符合当前的条件
        return Math.abs(left-right)<=1 && isBalanced(root.left) && isBalanced(root.right);
    }


文章作者: 南汐
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 www.phblog.cloud
数据结构 数据结构
喜欢就支持一下吧