AVL树详解+模拟实现

news2024/9/24 9:24:19

1:概念

当数据有序,二叉搜索树将趋近于单叉树,查找元素相当于在顺序表中查找元素,效率低下,两位俄罗斯数学家G.M.Adelson-Velskii和E.M.Landis创建了AVL树。特性如下:

  • 左右子树高度差的绝对值不超过1

  • 左右子树都为AVL树

如果一棵树是高度平衡的,搜索效率就可以保持在log2的N

2:模拟实现

2.1:结构体定义

因为map/multimap/set/multiset底层是平衡搜索树,所以可以采用键值对插入的方式。

template<class K,class V>
struct AVLTreeNode
{
    pair<K, V> _kv;
    AVLTreeNode<K, V>* left;
    AVLTreeNode<K, V>* right;
    AVLTreeNode<K, V>* parent;
    int _bf;
    AVLTreeNode(const pair<K,V>& kv)
        :left(nullptr)
        ,right(nullptr)
        ,parent(nullptr)
        ,_kv(kv)
        ,_bf(0)
    {}
};

_bf是平衡因子,表示左右子树高度差。

2.2:平衡因子

平衡因子表示左右高度差,通常用右子树高度-左子树高度。

2.3:插入

插入就和二叉搜索树一样,用kv的键值对的K(first)比较,但是AVL树的插入分为2个步骤,插入节点和调整平衡因子。

    bool Insert(const pair<K, V>& kv)
    {
        if (_root == nullptr)
        {
            _root = new Node(kv);
        }
        Node* parent = nullptr;
        Node* cur = _root;
        while (cur)
        {
            if (cur->_kv.first > kv.first)
            {
                parent = cur;
                cur = cur->left;
            }
            else if (cur->_kv.first < kv.first)
            {
                parent = cur;
                cur = cur->right;
            }
            else
            {
                return false;
            }
        }
        cur = new Node(kv);
        if (parent->_kv.first > kv.first)
        {
            parent->left = cur;
            cur->parent = parent;
        }
        else
        {
            parent->right = cur;
            cur->parent = parent;

        }

先找到合适的位置,再判断自己是左节点还是右节点,将这个new的新节点和父亲链接起来。

2.3:调整平衡因子

因为是插入孩子节点,所以要调整父亲的平衡因子,因此当父亲为空的时候停止调整。

           while(parent)
            if (cur = parent->left)
            {
                parent->_bf--;
            }
            else
            {
                parent->_bf++;
            }

cur是左,平衡因子--,因为左高右低,同理反之。

因为AVL树要保持高度平衡,所以接下来要对父亲的平衡因子情况做判断。

            if (parent->_bf == 0)
            {
                //没有高度变化
                break;
            }
            else if (parent->_bf == 1 || parent->_bf == -1)
            {
                cur = parent;
                parent = parent->parent;
            }
            else if (parent->_bf == 2 || parent->_bf == -2)
            {
                //旋转
                if (parent->_bf == 2 && cur->_bf == 1)
                {
                    //右子树高度发生了变化
                    RotateR(parent);
                    break;
                }
            }
  • 如果父亲平衡因子变成0,说明原本是1或者-1,也就是从不平衡到平衡,这次插入是填充矮的一边,高度是不会发生变化的,不需要进行旋转。

  • 如果父亲平衡因子变成了1的绝对值,说明从0到1和-1,(不可能从2变成1,从-2变成-1是因为插入的每一次要保证是AVL树,如果插入前已经不平衡,那么后续怎么旋转都没用),那么此时子树高度就发生了变化,变得更高了,但是如果是1的绝对值,还能满足AVL树的特性。

但是如图,9后再插入值,9的平衡因子变成了1,8的平衡因子变成了2,不能满足AVL树的特性了,所以当父亲的平衡因子变成1的绝对值的时候,表明parent所在子树的高度变化了,要持续向上更新。

  • 当父亲的平衡因子变为2的绝对值,并且cur的平衡因子是1的时候,说明当前是在cur的右节点上插入了节点,导致高度严重不平衡。

2.3.1:左单旋

假设abc是高度为h的avl子树,因为之前说了cur的bf为1,说明是从c这里插入节点,c的高度变为h+1,此时就会发生左单旋,左单旋又分为多种情况。

2.3.1.1:h=0

abc此时为空节点,将30链接到60的左孩子。

2.3.1.2:h=1

把b作为30的右孩子,30作为60的左孩子。因为b必然大于30小于60。30和a必然小于60。

c的左右新增都会引发旋转。

2.3.1.3:h=2

abc高度为2,那么a和b必然各自有3种情况,对于c肯定是z的形状,因为c插入了新节点导致c的高度变为了h+1,才会导致树不平衡,如果c是x或者y,插入了新节点后高度不会变,不会引发旋转。而且c如果是x或者y的情形,就等同于第一种情况h=0,因为我们这里所说的不同的h情况不只是针对整棵树,也有可能是部分的子树旋转。所以这里c如果是x或者y就和第一种情况一样。

所以c是z的形状,当c的2叶子节点的4个孩子节点任意位置插入1个节点,c的高度变为h+1,60的平衡因子变成1,30的平衡因子变成2,必然引发30的旋转。

2.3.1:左单旋实现

旋转的目的:

  • 让左右子树高度差不超过1

  • 旋转过程保证他是搜索树

  • 更新调整孩子节点的平衡因子

  • 保证子树的高度和插入前一致

    void RotateR(Node* parent)
    {
        Node* subR = parent->right;
        Node* subRL = subR->left;
        parent->right = subRL;

        if (subRL)
        {
            //不为空节点
            subRL->parent = parent;
        }
        Node* ppNode = parent->parent;
        subR->left = parent;
        parent->parent = subR;
        if (ppNode == nullptr)
        {
            //更新root
            _root = subR;
            _root->parent = nullptr;
        }
        //parent不是根节点,说明还要把父亲和subr链接
        else
        {
            if (ppNode->left == parent)
            {
                ppNode->left = subR;
            }
            else
            {
                ppNode->right = subR;
            }
            subR->parent = ppNode;
        }
        parent->_bf = subR->_bf = 0;
        
    }

因为结束条件是parent为空就停止调整,所以每次传parent给单旋函数。

实际上调整的是parent右节点/右节点的左节点/parent/parent的parent这4个节点的关系

思路就是

  • 定义2个指针指向parent的右和右的左

  • parent的右指向subRL

  • 当subRL不为空的时候,subRL的parent指向parent

  • subR的左指向parent

  • 定义指针指向parent的parent,因为不确定parent是否为根节点,如果不为根节点,因为parent的parent需要更改为subR,但是parent不是根节点,他还有parent,还应该把更改前的parent的parent与subR进行链接,否则找不到parent的parent。

  • parent的parent指向subR

  • 如果parent的parent是空,说明parent本来是根节点,此时就让根节点变成subR,并且让subR也就是root根节点的父亲变为空指针。不置空那么subR的父亲还仍然指向parent。

  • 如果parent的parent不为空,说明parent不是根节点,此时判断parent是左还是右,然后对应着把parent的parent的左/右链接到subR,

  • 把subR的父亲链接到ppnode

  • 调整parent和subR的平衡因子(目的之一)

为什么旋转一次就可以break了?因为到了当前子树部分(也有可能到了根,不影响),因为当前子树部分的高度变化了,导致上面的高度也会变化,所以旋转当前部分的子树,子树的高度又变为插入之前一样的高度了,上面的高度又会恢复,所以旋转一次break就行。

2.3.2:右单旋

与单左旋差不多

    void RotateL(Node* parent)
    {
        Node* subL = parent->left;
        Node* subLR = subL->right;
        parent->left = subLR;
        if (subLR)
        {
            subLR->parent = parent;
        }
        Node* ppnode = parent->parent;
        parent->parent = subL;
        subL->right = parent;
        if (ppnode == nullptr)
        {
            _root = subL;
            _root->parent = nullptr;
        }
        else
        {
            if (ppnode->left = parent)
            {
                ppnode->left = subL;
            }
            else
            {
                ppnode->right = subL;
            }
            subL->parent = ppnode;
        }
        parent->_bf = subL->_bf = 0;
    }

2.3.3:双旋转

我们前面2种情况的插入是直着插入,如果是右边这种旋转的插入,就算是用前面的办法,把2的左边给1的右边,1作为3的左边,旋转后这样仍然是旋转的样子,不能让子树保持跟插入前的高度一致(就算不能让父亲重新回到0这个平衡因子)。

因此给出了方法,二次旋转。

针对这个又可以画图来分析一下什么情况会导致需要旋转

2.3.3.1:h=0

(此时bc的父亲节点相当于不存在)

2.3.3.2:h=1

2.3.3.3:h=2

b和c必须为z才能导致插入后会发生旋转。

2.3.3.4:双旋过程

双旋情况:

parent和cur分别为-2,1或者2,-1

步骤:

  • 先以30为父亲节点,此时90相当于ppnode,会用来进行链接60

  • 然后以90为父亲节点,再进行右旋。

但是用代码复用的话,平衡因子更新有问题,左旋的话30和60的因子会变成0,右旋的话60和90的因子会变成0。

如果不从单旋分下来看这个问题,本质上这个问题就是把60的b拆分给30的右,60的c拆分给90的左,这样的话90的bf是1,30的bf才是0,更何况如果父亲的bf是-2&&cur的bf是1的时候,b和c哪个插入都不知道,这样一来b和c的平衡因子都不确定,明显平衡因子有错误。

同样的,在h=0的时候,60就是新增,先进行左旋的话(以60为轴点),就是60的左边给30的右边,30变成60的左边,再以90为轴点右旋,就是60的右边变成90的左边,90变成60的右边。

2.3.3.5:双旋的平衡因子调整和双旋LR代码

因此可以根据60的平衡因子来判断是b插入还是c插入还是60就是新增,针对3个情况写出如下的代码。

    void RotateRL (Node* parent)
    {
        Node* subL = parent->left;
        Node* subLR = subL->right;
        int bf = subLR->_bf;

        RotateL(parent->right);
        RotateL(parent);
        if (bf == -1)
        {
            //sublr左边新增
            subL->_bf = 0;
            parent->_bf = 1;
            subLR->_bf = 0;
        }
        else if (bf == 1)
        {
            //sublr右边新增
            parent->_bf = 0;
            subL->_bf = -1;
            subLR->_bf = 0;
        }
        else if(bf == 0)
        {
            //sublr就是新增
            parent->_bf = 0;
            subL->_bf = 0;
            subLR->_bf = 0;

        }
        else
        {
            assert(false);
        }
    }

2.3.3.6:RF双旋代码

    void RotateRL(Node* parent)
    {
        Node* subR = parent->right;
        Node* subRL = subL->left;
        int bf = subRL->_bf;

        RotateR(parent->right);
        RotateL(parent);
        if (bf == -1)
        {
            //subrl左边新增
            subR->_bf = 1;
            parent->_bf = 0;
            subRL->_bf = 0;
        }
        else if (bf == 1)
        {
            //subrl右边新增
            parent->_bf = -1;
            subR->_bf = 0;
            subRL->_bf = 0;
        }
        else if (bf == 0)
        {
            //subrl就是新增
            parent->_bf = 0;
            subR->_bf = 0;
            subRL->_bf = 0;

        }
        else
        {
            assert(false);
        }
    }

注意pair的头文件包一下<utility>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/400344.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Django/Vue实现在线考试系统-06-开发环境搭建-Visual Studio Code安装

1.0 VS Code下载和安装 Visual Studio Code,简称 VS Code,是由微软公司开发的 IDE 工具。与微软其他 IDE(如 Visual Studio)不同的是,Visual Studio Code 是跨平台的,可以安装在 Windows、Linux 和 macOS平台上运行。不仅如此, Visual Studio Code 没有限定只能开发特定…

Revit中如何添加一个新的管道直径

有些时候项目当中会遇到一些管径比较小的管道&#xff0c;但是在直径中又没有适合的&#xff0c;怎么办?很简单&#xff0c;跟紧以下几个步理就可以了。 首先&#xff0c;我们拿一个管段为“铁&#xff0c;铸铁30”的为例子&#xff0c;如图1所示&#xff0c;系统中这管段是没…

1.数据结构的研究

数据结构很重要&#xff01; 数据结构很重要&#xff01;! 数据结构很重要&#xff01;! ! 思考 1.数据结构研究的内容有哪些&#xff1f;&#xff08;What&#xff09; 2.为什么要研究数据结构? ? (Why) 3.如何更好的研究数据结构? ? &#xff1f;(How) 注&#xff1a;特别…

Hadoop小结

Hadoop是什么Hadoop是一 个由Apache基金 会所开发的分布式系统基础架构。主要解决,海量数据的存储和海量数据的分析计算问题。广义上来说&#xff0c;Hadoop通 常是指一个更广泛的概念一Hadoop 生态圈。Hadoop优势Hadoop组成HDFS架构Hadoop Distributed File System&#xff0c…

蓝桥杯--ISBN号码

ISBN号码 技巧 数字转为字符【数字‘0’】 字符转为数字【字符-‘0’】 这道题比较简单 题目大意 每一本正式出版的图书都有一个 ISBN 号码与之对应&#xff0c;ISBN 码包括 9 位数字、1 位识别码和 3 位分隔符&#xff0c;其规定格式如 “x-xxx-xxxxx-x”&#xff0c;其中符号…

java多线程(二四)java多线程基础总结

一、进程与线程 1.进程 进程是操作系统结构的基础&#xff1b;是一次程序的执行&#xff1b;是一个程序及其数据在处理机上顺序执行时所发生的活动。操作系统中&#xff0c;几乎所有运行中的任务对应一条进程&#xff08;Process&#xff09;。一个程序进入内存运行&#xff…

前装L2标配车型均价连续第二年「低于」L1,市场进入爆发期

L2级辅助驾驶&#xff0c;正在进入市场红利期。 高工智能汽车研究院监测数据显示&#xff0c;2022年度中国市场&#xff08;不含进出口&#xff09;乘用车前装标配搭载辅助驾驶&#xff08;L0-L2&#xff09;交付1001.22万辆&#xff0c;首次突破千万辆规模&#xff0c;同时&a…

带你玩转spring声明式事务-使用中需要注意的点

本文向大家介绍spring声明式事务使用过程中需要注意的地方。事务特性1. 原子性&#xff08;Atomicity&#xff09;事务是一个原子操作&#xff0c;由一系列动作组成。事务的原子性确保动作要么全部完成&#xff0c;要么完全不起作用。2. 一致性&#xff08;Consistency&#xf…

九龙证券|6G概念重新活跃 数字经济板块引领A股尾盘回升

周三&#xff0c;沪深两市缩量调整&#xff0c;沪指全天以弱势震荡为主&#xff0c;尾盘在数字经济概念带动下快速拉升&#xff0c;全天微跌0.06%&#xff0c;报3283.25点&#xff1b;深证成指跌落0.09%&#xff0c;报15598.29点&#xff1b;创业板指跌落0.26%&#xff0c;报23…

[算法]归并排序

参考&#xff1a;《漫画算法-小灰的算法之旅》 目录 参考&#xff1a;《漫画算法-小灰的算法之旅》 1、什么是归并排序 2、归并的具体操作 3、代码 4、时间复杂度和空间复杂度 5、归并排序是稳定排序 1、什么是归并排序 归并排序就像是组织一场元素之间的“比武大会”&…

【C++】30h速成C++从入门到精通(二叉树)

说明为什么要在C当中单独再次提及数据结构中的二叉树&#xff1a;map和set特性需要先铺垫二叉搜索树&#xff0c;而二叉搜哦书也是一种树形结构二叉搜索树的特性了解&#xff0c;有助于更好的理解map和set特性二叉树中部分面试题有难度有些OJ使用C语言实现比较麻烦二叉搜索树概…

Kubernetes14:Helm为了部署像微服务这种的大型项目

Kubernetes14&#xff1a;Helm介绍&#xff08;为了部署像微服务这种的大型项目&#xff09; 1、Helm的引入 (1)之前方式部署应用基本过程 编写yaml文件 1、deployment kubectl create deployment nginx --imagenginx --dryrun -o yaml > nginx.yaml2、Service kubect…

Web前端:前端开发人员的职责有哪些?

前端开发&#xff0c;就是要创造上面提到的网站面向用户的部分背后的代码&#xff0c;并通过建立框架&#xff0c;构建沉浸性的用户体验。前端工程师还需要确保网站在各种浏览器和设备上都能正常运行&#xff0c;并且能够根据用户需求不断优化和改进网站。前端开发人员的角色和…

【C语言进阶】文本与二进制操作文件,优化通讯录。

前言&#xff1a;上篇文章&#xff0c;我们已经学习了有关本地磁盘文件的常用文件操作&#xff0c;已经能够对本地文件进行调用与读写。我们磁盘中还存在着一些内容用二进制存储的文件&#xff0c;这也就是我们今天将要讲解的内容。一、文本文件与二进制文件根据数据的组织形式…

SpiderFlow爬虫获取网页节点

SpiderFlow爬虫获取网页节点 一、SpiderFlow 文档地址&#xff1a;https://www.spiderflow.org/ 二、问题&#xff1a;获取一篇文章的标题、来源、发布时间、正文、下载附件该怎么获取&#xff1f; 举例&#xff1a;【公示】第三批智能光伏试点示范名单公示 三、抓取网页步骤…

Heatmap-based Out-of-Distribution Detection 论文阅读

原文地址 概要 我们的工作将分布失调【out-of-distribution,OOD】检测作为神经网络输出解释问题进行研究。我们学习了一种热图【heatmap】表示&#xff0c;用于检测OOD图像&#xff0c;同时可视化ID和OOD的图像区域。给定一个训练过的固定分类器&#xff0c;我们训练一个解码…

ArrayList源码分析(JDK17)

ArrayList类简介类层次结构构造无参构造有参构造添加元素add&#xff1a;添加/插入一个元素addAll:添加集合中的元素扩容mount与迭代器其他常见方法不常见方法不常见方法的源码和小介绍常见方法的源码和小介绍积累面试题ArrayList是什么&#xff1f;可以用来干嘛&#xff1f;Ar…

funkyheatmap | 用这个包来完美复刻Nature Biotechnology的高颜值神图吧!~

1写在前面 天气开始暖和了☀️&#xff0c;发现旅游的人好多啊&#xff01;~&#x1f972; 不知道自己什么时候能有时间出去看看外面的世界&#xff0c;实在是太忙了。&#x1f637; 最近用到的有个包感觉很不错&#xff0c;分享给大家&#xff0c;funkyheatmap包。&#x1f61…

进程与多线程(入门)

什么是线程 要了解什么是线程&#xff0c;得先知道什么是程序。 程序&#xff1a;为完成特定任务&#xff0c;用某种语言编写的一组指令的集合。 例如&#xff0c;QQ&#xff0c;Steam&#xff0c;亦或者java写的helloword。 这些都是程序 了解了程序&#xff0c;还得清楚什么…

d3.js绘制饼状图,悬浮出现字以及点击事件

代码以及注释如下&#xff1a; const width 300; // 定义圆的宽度 const height 300; // 定义圆的高度 const radius Math.min(width, height) / 2; // 算出半径 const color d3.scaleOrdinal() .range(["#98abc5", "#8a89a6", "#6b486b&qu…