数据结构——AVL树

文章目录:

1. AVL树(二叉平衡树)

1.1:背景

1.2:AVL树

1.3:AVL树的分类

1.4:平衡因子(balance factor  简称:bf)

2. 实现AVL树

2.1:AVL树节点的定义

2.2:AVL树插入节点操作:

2.3:AVL树的旋转

旋转的分类:

1. 左单旋:

2. 右单旋

3. 左右双旋

4. 右左双旋

总结

3. AVL树的验证

4. AVL树性能分析


1. AVL树(二叉平衡树)

1.1:背景

        二叉搜索树虽然可以提高搜索的效率,但是如果序列有序or接近有序,二叉搜索树会退化成单支树。这种情况下搜索元素就相当于在顺序表中查找,效率反而低下。

        两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis对此提出了解决方案:向二叉搜索树中插入新的节点后,如果可以保证每个节点的左右子树高度差不超过1,那么就可以降低树的高度,从而提高搜索效率。

AVL:两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis名字的缩写

1.2:AVL树

AVL树本质上:一棵高度平衡的二叉搜索树

1.3:AVL树的分类

        1. 空树

        2. 具有以下性质的二叉搜索树:

                a. 左右子树都是AVL树

                b. 左右子树的高度差的绝对值不超过1 (AVL树也叫做二叉平衡树)

1.4:平衡因子(balance factor  简称:bf)

bf = 左右子树高度差

AVL树每个节点的平衡因子绝对值不超过1: bf = (0、±1)

2. 实现AVL树

2.1:AVL树节点的定义

public class AVLTree {
    //AVL节点的内部类
    public static class TreeNode{
        public int val;
        public int bf;//balance factor 平衡因子
        public TreeNode left;
        public TreeNode right;
        public TreeNode parent;

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

    public TreeNode root;

}

2.2:AVL树插入节点操作:

AVL树本质上是一棵高度平衡的二叉搜索树,所以插入一个节点的步骤:

        1.按照二叉搜索树的方式插入节点

        2.更新每个节点的平衡因子

        3.对不平衡的子树进行调整,使其符合AVL树的性质

    public boolean insert(int val){
        //按照二叉搜索树的方式插入元素
        TreeNode node = new TreeNode(val);
        if(root==null){
            root=node;
            return true;
        }
        TreeNode parent=null;
        TreeNode cur=root;
        while(cur != null){
            if(val>cur.val){
                parent=cur;
                cur=cur.right;
            } else if (val<cur.val) {
                parent=cur;
                cur=cur.left;
            }else {
                return false;
            }
        }
        //cur==null  插入节点
        if(val>parent.val){
            parent.right=node;
        } else{
            parent.left=node;
        }
        node.parent=parent;
        cur=node;

        //调节平衡因子
        while(parent!=null){
            //先看是插在了左子树还是右子树
            if(cur==parent.right){
                //右 右树高度增加 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 { //bf==2 不平衡 旋转
                if(parent.bf==2){//右子树高 降低右子树的高度
                    if(cur.bf==1){ //插入到了较高右子树的右侧
                        //左旋
                        rotateLeft(parent);
                    }else { //cur.bf==-1 插入到了较高右子树的左侧
                        //先右旋再左旋
                        rotateRL(parent);
                    }
                } else { //左子树高 降低左子树的高度
                    if(cur.bf==-1){//插入到了较高左子树的左侧
                        //右旋
                        rotateRight(parent);
                    }else { //cir.bf==1 插入到了较高左子树的右侧
                        //先左旋再右旋
                        rotateLR(parent);
                    }
                }
                //上述代码走完 平衡
                break;
            }
        }
        return true;
    }

插入一个节点后,需要更新parent的平衡因子

        插入在了parent的右子节点的位置,parent的 bf ++

        插入在了parent的左子节点的位置,parent的 bf --

检查插入节点后parent的平衡因子:有三种情况 0 、±1、±2

        1. 如果是0:插入之前parent的平衡因子为正负1,插入之后调整为0,满足AVL树的性质,插入成功;

        2. 如果是±1:插入之前parent的平衡因子一定为0,插入后被调整为±1,当前子树高度增加,需要继续向上更新

        3. 如果是±2:parent的平衡因子不符合AVL树的性质,需要进行旋转

2.3:AVL树的旋转

在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。

旋转的分类:

        左单旋:新节点插入到较高右子树的右侧(右右型)

        右单旋:新节点插入到较高左子树的左侧(左左型)

        左右双旋:新节点插入到了较高左子树的右侧

        右左双旋:新节点插入到了较高右子树的左侧

1. 左单旋:

新节点插入到较高右子树的右侧,需要降低右子树的高度

步骤:

        1. 将bf = 2的节点向左下方旋转

        2. 将其右节点的左节点作为其右节点

        3. 其成为其右节点的左节点

        4. 更新平衡因子

代码:

    //左单旋
    private void rotateLeft(TreeNode parent){
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;

        parent.right=subRL;
        subR.left=parent;

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

        TreeNode pParent=parent.parent;
        parent.parent=subR;

        if(pParent==root){
            root=subR;
            root.parent=null;
        }else{
            if(pParent.left==parent){
                pParent.left=subR;
            }else{
                pParent.right=subR;
            }
            subR.parent=pParent;
        }

        subR.bf=0;
        parent.bf=0;
    }

2. 右单旋

新节点插入到较高左子树的左侧,需要降低左子树的高度

步骤:

        1. 将bf = -2的节点向右下方旋转

        2. 将其左节点的右节点作为其左节点

        3. 其成为其左节点的右节点

        4. 更新平衡因子

代码:

    //右单旋
    private void rotateRight(TreeNode parent){
        TreeNode subL=parent.left;
        TreeNode subLR=subL.right;

        parent.left=subLR;
        subL.right=parent;

        if(subLR!=null){ //subLR可能不存在
            subLR.parent=parent;
        }
        //先记录 parent.parent
        TreeNode pParent =  parent.parent;
        parent.parent=subL;

        if(pParent==root){ // pParent 为根节点
            root=subL;
            root.parent=null;
        }else{ //不是根节点
            // 看parent是pParent的左节点还是右节点
            if(pParent.left==parent){
                pParent.left=subL;
            }else{ //pParent.right==parent
                pParent.right=subL;
            }
            subL.parent=pParent;
        }

        //调节平衡因子
        subL.bf=0;
        parent.bf=0;
    }

3. 左右双旋

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

步骤:

        1. 先对parent的左子树进行左旋

        2. 再对整棵树进行右旋

        3. 更新平衡因子

代码:

    //左右双旋
    private void rotateLR(TreeNode parent){
        TreeNode subL=parent.left;
        TreeNode subLR=subL.right;
        int bf= subLR.bf;

        rotateLeft(parent.left);
        rotateRight(parent);

        //更新平衡因子
        //看插入的节点是其父节点的左子还是右子
        if(bf == -1){
            subL.bf=0;
            subLR.bf=0;
            parent.bf=-1;
        }else if(bf == 1){
            subL.bf=-1;
            subLR.bf=0;
            parent.bf=0;
        }
    }

4. 右左双旋

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

步骤:

        1. 先对parent的右子树进行右旋

        2. 再对整棵树进行左旋

        3. 更新平衡因子

代码:

    //右左双旋
    private 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;
            subR.bf=0;
            subRL.bf=0;
        }else if(bf == -1){
            parent.bf=0;
            subR.bf=0;
            subRL.bf=-1;
        }
    }

总结

新节点插入后,假设以parent为根的子树不平衡:

parent与其较高子树节点的平衡因子时同号时单旋转,异号时双旋转。

旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。

3. AVL树的验证

AVL树本质上是一棵高度平衡的二叉搜索树,所以验证一棵AVL树步骤:

        1. 先验证是二叉搜索树:中序遍历得到的序列有序

        2. 在验证高度平衡:

                a. 每个节点的平衡因子绝对值不超过1

                b. 节点的平衡因子是否计算正确

    //中序遍历
    public void inOrder(TreeNode root){
        if(root==null){
            return;
        }
        inOrder(root.left);
        System.out.println(root.val+" ");
        inOrder(root.right);
    }

    //求二叉树高度
    private int height(TreeNode root){
        if(root==null){
            return 0;
        }
        int leftH = height(root.left);
        int rightH = height(root.right);

        return Math.max(leftH, rightH)+1;
    }

    //AVL树的验证
    public boolean isBalance(TreeNode root){
        if(root==null){
            return true;
        }
        int leftH = height(root.left);
        int rightH = height(root.right);

        //检查平衡因子
        if(rightH-leftH!=root.bf){
            System.out.println(root.val+"平衡因子异常");
            return false;
        }

        return Math.abs(leftH-rightH)<=1
                && isBalance(root.left)
                && isBalance(root.right);

    }

4. AVL树性能分析

一棵N个节点的AVL树:

        高度:logN

        查询的时间复杂度:O(logN)

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O(logN) ;

但AVL树每次插入或删除节点都要通过旋转对结构进行调整,旋转的次数比较多,所以不适合频繁插入和删除数据;

因此:如果需要 一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值