数据结构----AVL树

       小编会一直更新数据结构相关方面的知识,使用的语言是Java,但是其中的逻辑和思路并不影响,如果感兴趣可以关注合集。

       希望大家看完之后可以自己去手敲实现一遍,同时在最后我也列出一些基本和经典的题目,可以尝试做一下。大家也可以自己去力扣或者洛谷牛客这些网站自己去练习,数据结构光看不敲是学不好的,加油,祝你早日学会数据结构这门课程。

       有梦别怕苦,想赢别喊累。

 前言

        在之前学习二叉搜索树时,我们知道当一颗二叉搜索树的左右高度不平衡时,那这棵树的查询的时间复杂度可能达到O(n)。这显然不是我们想要的。

        其实在树中存在一种操作----旋转,这种操作的对象是节点,可以达到改变树结构的目的,因此我们可以借助旋转来把上面这颗左右高度不平衡的二叉搜索变得左右高度平衡一点。我们可以把根节点4向右旋转,同时把根节点3也向右旋转,那下面这棵树就变平衡了。

        在树形结构中规定如果一个节点的左右孩子高度差超过1,则此节点失衡,才需要旋转。

 概述

        在计算机科学中avl树又称平衡二叉搜索树,顾名思义就是可以让检索效率一直是O(logn)的一种树形数据结构。二叉搜索树在插入和删除时,节点可能失衡,因此如果在插入和删除时通过旋转,始终让二叉搜索树保持平衡,称为自平衡的二叉搜索树。另外AVL树是自平衡二叉搜索树的实现之一,后面我们要学习的红黑树也是自平衡二叉搜索树的实现之一。

 实现

    结构

        avl树的结构其实和二叉搜索树差不多,唯一区别就是需要判断每个节点是否平衡,因此我们多定义一个变量去维护当前节点高度,初始值为1,因为我们计算高度是从当前节点开始。

static class AVLNode {
        int key;
        int value;
        AVLNode left;
        AVLNode right;
        int height = 1; //高度

        public AVLNode(int key, int value) {
            this.key = key;
            this.value = value;
        }

        public AVLNode(int key) {
            this.key = key;
        }

        public AVLNode(int key, int value, AVLNode left, AVLNode right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }

        有的地方实现是维护两个变量,一个左子树高度,一个右子树高度,其实大差不差,能实现这两种的其中一种,另一种实现起来也是游刃有余。

        接着我们也需要提供几个工具方法,例如获取当前节点高度,更新节点高度,判断节点是否平衡这些的。

        获取当前节点高度就很简单的啦,我们有一个变量height是维护高度的,直接返回就好了。

    //求节点高度
    private int height(AVLNode node) {
        return node == null ? 0 : node.height;
    }

         更新节点高度,其实如果把之前题目做过的话,有一道二叉树的最大深度这道题其实和这个逻辑是一样的,求出左右子树高度最大值然后+1就好了。

        104. 二叉树的最大深度 - 力扣(LeetCode)

    //更新节点高度(新增,删除,旋转)
    private void updateHeight(AVLNode node) {
        node.height = Math.max(height(node.left), height(node.right)) + 1;
    }

        判断节点是否平衡,我们还需要引出一个概念就是平衡因子(balance factor 简称 bf),这玩意其实就是左子树 - 右子树的高度差,根据bf的值就可以判断左右子树是否平衡。

  

    // 判断该节点是否平衡
    // 平衡因子(balance factor) = 左子树高度 - 右子树高度
    // 返回 0,1,-1 平衡
    // >1 或者 <-1 不平衡
    private int bf(AVLNode node) {
        return height(node.left) - height(node.right);
    }

   失衡情况

        在实现avl树之前,我们还需要知道avl树的四种失衡情况,才能更好决定旋转的节点与方向。

        第一种情况,下面这就是一颗左高右低的不平衡二叉搜索树。

       

        我们可以将节点3向右旋转,那这时节点3就变成了根节点,但是节点3此时也有了2,4,5三个子节点,这当然是不行的,所以我们还需要将节点4的父节点改成节点5,那树的结构就变成了下面这样。

    第二种情况也是左高右低的不平衡二叉树,不过在子树结构上又有所不同。

        

     这时如果我们按照上面的处理方式将节点二右旋一次就得到了下面这样的一棵树

       这时这颗树的不平衡问题仍然没有解决,因此我们知道有的情况不能单靠一次右旋就能解决问题。

       其实前人已经帮我们总结好了二叉搜索树的四种失衡情况。

        第一种叫做左左(LL),看下面这棵树,失衡节点(节点5)的bf > 1,即左边更高,这是第一个左的含义,接着失衡节点的左孩子(节点3)的bf>=0 ,即左孩子也是左边更高或者等高,这是第二个左的含义。

       第二种叫做左右(LR),看下面这颗树,失衡节点(节点6)的bf > 1,即左边更高,这是第一个左的含义,失衡节点的左孩子(节点2)的bf < 0 即左孩子这边是右边更高,这是第二个右的含义。

       前两种情况理解了,其实剩下两种就是对称的。

       第三种叫做右左(RL),下面这颗树,失衡节点(节点2)的bf <  -1,即右边更高,这是第一个右的含义,失衡节点的右孩子(节点6)的bf > 0 ,即右孩子这边是左边更高,这是第二个左的含义。

        第四种叫做右右(RR),下面这颗树,失衡节点(节点2)的bf <  -1,即右边更高,这是第一个右的含义,失衡节点的右孩子(节点4)的bf <= 0 ,即右孩子这边是右边更高或者等高,这是第二个右的含义。

       了解完四种情况后,我们来看一下对于四种情况我们怎样旋转才可以恢复平衡,其实左左只需要左孩子节点向右旋转一次,就可以恢复平衡,与之对称右右只需要右孩子节点向左旋转一次就可以了,这都是两种比较简单的情况。接着对于左右和右左我们都需要两次旋转才可以恢复平衡。对于左右,我们先让失衡节点的左孩子先向左旋转就来到了下面这种情况,这种情况其实就是左左,我们再右旋一此就好了。右左也是一样的,先右子树右旋,就变成了右右的情况,然后左旋一次就可以恢复情况了。

   左旋

         其实左旋就两步,第一步是让原来的右子树节点上位到根节点,第二步就是把原来右子树的左子树变成原来根节点的左子树。另外不要忘了更新节点高度,其实在二叉搜索树旋转的过程中,只有根节点和旋转的孩子节点高度才会改变,更新高度的顺序不可以改变,这里可以思考一下为什么。

    /**
     * @Param:node 要旋转的节点
     * @Return: 新的根节点
     */
    private AVLNode leftRotate(AVLNode node) {
        AVLNode rightTree = node.right;
        // 左旋,右边肯定高,不用考虑rightTree为null
        AVLNode rightTreeLeft = rightTree.left;
        rightTree.left = node; // 上位根节点
        node.right = rightTreeLeft; // 换父节点
        // 更新高度
        updateHeight(node);
        updateHeight(rightTree);
        return rightTree;
    }

   右旋

        其实通过前面的左旋,我们也大概知道了右旋也是分两步,与左旋对称的两步。最后更新高度

    /**
     * @Param:node 要旋转的节点
     * @Return: 新的根节点
     */
    private AVLNode rightRotate(AVLNode node) {
        AVLNode leftTree = node.left;
        // 右旋,左边肯定高,不用考虑leftTree为null
        AVLNode leftTreeRight = leftTree.right;
        leftTree.right = node; // 上位根节点
        node.left = leftTreeRight; // 换父节点
        // 更新高度
        updateHeight(node);
        updateHeight(leftTree);
        return leftTree;
    }

    左右旋

        其实我们完成左旋转和右旋转后,这个方法很好实现,先调一次左旋再调一次右旋就好了,但是不要忘了左子树左旋完后把返回的根节点设置为根节点的左子树

    /**
     * 先左旋左子树,再右旋根节点
     * @param node
     * @Return: 新的根节点
     */
    private AVLNode leftRightRotate(AVLNode node) {
        node.left = leftRotate(node.left);
        return rightRotate(node);
    }

     右左旋

        同样不要忘了右子树右旋完后把返回的根节点设置为根节点的右子树

    /**
     * 先右旋右子树,再左旋根节点
     * @param node
     * @Return: 新的根节点
     */
    private AVLNode rightLeftRotate(AVLNode node) {
        node.right = rightRotate(node.right);
        return leftRotate(node);
    }

        到此四种失衡情况以及四种旋转情况的代码我们都已经完成了。 

  balance

        接着我们来实现一个工具方法,它的作用就是检查节点是否失衡并调整,如果失衡就重新平衡这棵树,然后返回根节点。如果没失衡我们也是返回根节点。

        首先判断节点是否失衡,我们只需要通过它的平衡因子就可以知道了,拿到了当前节点的平衡因子,如果它失衡,我们就对照四种失衡情况进行旋转方法的调用,这个balance方法实现起来也是轻轻又松松啊。

    /**
     * 平衡方法
     * @param node
     * @Return: 新的根节点
     */
    private AVLNode balance(AVLNode node) {
        if (node == null) {
            return null;
        }
        // 平衡因子
        int bf = bf(node);
        if (bf > 1 && bf(node.left) >= 0) { // LL
            return rightRotate(node);
        } else if (bf > 1 && bf(node.left) < 0) { // LR
            return leftRightRotate(node);
        } else if (bf < -1 && bf(node.right) > 0) { // RL
            return rightLeftRotate(node);
        } else if (bf < -1 && bf(node.right) <= 0) { // RR
            return leftRotate(node);
        }
        return node;
    }

  put

        接着我们来实现平衡二叉搜索树新增节点的方法,其实大体的逻辑和上一篇我们在二叉搜索树中插入节点是一样的,我们还是用递归去实现,唯一不同的就是,在节点插入后,我们要去更新高度,同时更新高度之后可能会导致失衡,所以我们还需要调用一下balance方法,就多了这两步,其它和二叉搜素树都是一样的。

    /**
     * 新增节点
     * @param key
     * @param value
     * @Return: void
     */
    public void put(int key, int value) {
        root = doPut(root, key, value);
    }

    private AVLNode doPut(AVLNode node, int key, int value) {
        // 1.找到空位,创建新节点
        if (node == null) {
            return new AVLNode(key, value);
        }
        // 2. key已经存在,更新
        if (key == node.value) {
            node.value = value;
            return node;
        }
        // 3.继续查找
        if (key < node.key) {
            node.left = doPut(node.left, key, value); // 向左
        } else {
            node.right = doPut(node.left, key, value); // 向右
        }
        // 更新高度
        updateHeight(node);
        // 调整
        return balance(node);
    }

 remove

        删除节点其实和二叉搜索树的逻辑也是一样的,不同的还是我们需要更新节点高度检查调整树的结构。关于删除逻辑有不清楚的可以去看我的上一篇关于二叉树的文章,那里有详细讲解。

    /**
     * 删除节点
     *
     * @param key
     * @Return: void
     */
    public void remove(int key) {
        root = doRemove(this.root, key);
    }

    private AVLNode doRemove(AVLNode node, int key) {
        // 1. node == null
        if (node == null) {
            return null;
        }
        // 2. 没找到key
        if (key < node.key) {
            node.left = doRemove(node.left, key);
        } else if (node.key < key) {
            node.right = doRemove(node.right, key);
        } else { // 3. 找到key
            if (node.left == null && node.right == null) { // 3.1 没有孩子
                return null;
            } else if (node.left == null) { // 3.2 只有一个孩子
                node = node.right;
            } else if (node.right == null) {
                node = node.left;
            } else {// 3.3 有两个孩子
                AVLNode s = node.right;
                while (s.left != null) {
                    s = s.left;
                }
                // s 后继节点
                s.right = doRemove(node.right, s.key);
                s.left = node.left;
                node = s;
            }
        }
        // 4. 更新高度
        updateHeight(node);
        // 5. 调整
        return balance(node);
    }

相关题目

1382. 将二叉搜索树变平衡 - 力扣(LeetCode)

       平衡二叉搜索树与二叉搜索树的区别就是树的结构是否平衡,从而实现查找效率达到O(logn),完成上面这一道题也差不多了,另外可以多做一些二叉搜索树的题目。

        如果不是天才,就请一步一步来。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值