<C++>AVL数

news2024/11/18 15:26:00

文章目录

  • 1. AVL树的概念
  • 2. AVL树节点的定义
  • 3. AVL树的插入
  • 4. AVL树的旋转
  • 5. AVL树的验证
  • 6. AVL树的性能

1. AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树要么是空树,要么是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)

image-20220903163715729

2. AVL树节点的定义

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;// balance factor

	AVLTreeNode(const pair<K, V>& kv)
		: _kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

3. AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。

那么AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子
//按搜索二叉树规则插入
//更新平衡因子,旋转使其平衡
bool Insert(const pair<K, V>& kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
        if (kv.first > cur->_kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (kv.first < cur->_kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else
            return false;
    }

    cur = new Node(kv);
    if (kv.first > parent->_kv.first)
    {
        parent->_right = cur;
    }
    else
    {
        parent->_left = cur;
    }
    cur->_parent = parent;

    //更新平衡因子
    while (parent)//最深要更新到根
    {
        if (cur == parent->_left)
        {
            parent->_bf--;
        }
        else
        {
            parent->_bf++;
        }

        //是否继续更新
        if (0 == parent->_bf)//两边平衡了
        {
            break;
        }
        else if (1 == parent->_bf || -1 == parent->_bf)// 子树的高度变了,继续更新祖先
        {
            cur = parent;
            parent = parent->_parent;
        }
        else if (2 == parent->_bf || -2 == parent->_bf)
        {
            //子树不平衡,需要旋转处理
            if (parent->_bf == 2 && cur->_bf == 1)//左单旋
            {
                RotateL(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == -1)//右单旋
            {
                RotateR(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == 1)
            {
                RotateLR(parent);
            }
            else if (parent->_bf == 2 && cur->_bf == -1)
            {
                RotateRL(parent);
            }

            break;
        }
        else
        {
            // 插入之前AVL就存在不平衡子树,就有>=2的平衡因子出现,之前的程序有问题
            assert(false);
        }
    }
    return true;
}

4. AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

  1. 新节点插入较高左子树的左侧—左左:右单旋

image-20220903164031544

/*
上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
1. 30节点的右孩子可能存在,也可能不存在
2. 60可能是根节点,也可能是子树
如果是根节点,旋转完成后,要更新根节点
如果是子树,可能是某个节点的左子树,也可能是右子树
*/
void _RotateR(PNode pParent)
{
    // pSubL: pParent的左孩子
    // pSubLR: pParent左孩子的右孩子,注意:该
    PNode pSubL = pParent->_pLeft;
    PNode pSubLR = pSubL->_pRight;
    // 旋转完成之后,30的右孩子作为双亲的左孩子
    pParent->_pLeft = pSubLR;
    // 如果30的左孩子的右孩子存在,更新亲双亲
    if(pSubLR)
    	pSubLR->_pParent = pParent;
    // 60 作为 30的右孩子
    pSubL->_pRight = pParent;
    // 因为60可能是棵子树,因此在更新其双亲前必须先保存60的双亲
    PNode pPParent = pParent->_pParent;
    // 更新60的双亲
    pParent->_pParent = pSubL;
    // 更新30的双亲
    pSubL->_pParent = pPParent;
    // 如果60是根节点,根新指向根节点的指针
    if(NULL == pPParent)
    {
        _pRoot = pSubL;
        pSubL->_pParent = NULL;
    }
    else
    {
        // 如果60是子树,可能是其双亲的左子树,也可能是右子树
        if(pPParent->_pLeft == pParent)
            pPParent->_pLeft = pSubL;
        else
            pPParent->_pRight = pSubL;
    }
    // 根据调整后的结构更新部分节点的平衡因子
    pParent->_bf = pSubL->_bf = 0;
}
  1. 新节点插入较高右子树的右侧—右右:左单旋

image-20220903164239776

  1. 新节点插入较高左子树的右侧—左右:先左单旋再右单旋

image-20220903164259914

将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再考虑平衡因子的更新。

// 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进行调整
void _RotateLR(PNode pParent)
{
    PNode pSubL = pParent->_pLeft;
    PNode pSubLR = pSubL->_pRight;
    // 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节点的平衡因子
    int bf = pSubLR->_bf;
    // 先对30进行左单旋
    _RotateL(pParent->_pLeft);
    // 再对90进行右单旋
    _RotateR(pParent);
    if(1 == bf)
    	pSubL->_bf = -1;
    else if(-1 == bf)
    	pParent->_bf = 1;
}
  1. 新节点插入较高右子树的左侧—右左:先右单旋再左单旋

image-20220903164411064

总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑

  1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
    当pSubR的平衡因子为1时,执行左单旋
    当pSubR的平衡因子为-1时,执行右左双旋
  2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
    当pSubL的平衡因子为-1是,执行右单旋
    当pSubL的平衡因子为1时,执行左右双旋

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

5. AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    节点的平衡因子是否计算正确
bool _IsBalanceTree(Node* root)
{
    if (root == nullptr)
        return true;

    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    int diff = rightHeight - leftHeight;

    if (abs(diff) >= 2)
    {
        cout << root->_kv.first << "不平衡咯" << endl;
        return false;
    }
    if (diff != root->_bf)
    {
        cout << "这都能算错?快去检测" << endl;
        return false;
    }

    return _IsBalanceTree(root->_left)
        && _IsBalanceTree(root->_right);
}

6. AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

性能这方面,接下来讲的红黑树做的更好,它的旋转修改频率更低

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

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

相关文章

MySQL中alter命令知识

MySQL中alter命令知识 文章目录MySQL中alter命令知识(一) 删除、添加、修改字段1、删除表中的字段数据2、添加新字段指定字段插入位置3、修改字段类型和名称&#xff08;二&#xff09;、修改表名&#xff08;三&#xff09;、修改存储引擎创建copy_emp表&#xff0c;便于后面案…

Prompt Learning 简介

最近去参会&#xff0c;看到了大量关于Prompt相关的论文&#xff0c;或者说跟NLP NLU相关的新论文或多或少都使用到了Prompt learning的一些思想或者设置。由于本人主业不是是做NLP的&#xff0c;所以对NLP顶会的这一现象觉得很有意思&#xff0c;趁闲暇学习了一下Prompt learn…

对话 BitSail Contributor | 姚泽宇:新生火焰,未来亦可燎原

2022 年 10 月&#xff0c;字节跳动 BitSail 数据引擎正式开源。同期&#xff0c;社区推出 Contributor 激励计划第一期&#xff0c;目前已有 12 位开发者为 BitSail 社区做出贡献&#xff0c;成为了首批 BitSail Contributor。 江海的广阔是由每一滴水珠构成的&#xff0c;Bi…

【高阶数据结构】手撕哈希表(万字详解)

&#x1f308;欢迎来到数据结构专栏~~手撕哈希表 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句…

【jqgrid篇】jqgrid.setCell 改变单元格的值 改变单元格的样式设置单元格属性

setCellrowid,colname, data, class, propertiesjqGrid对象 改变单元格的值。rowid&#xff1a;当前行id&#xff1b;colname&#xff1a;列名称&#xff0c;也可以是列的位置索引&#xff0c;从0开始&#xff1b;data&#xff1a;改变单元格的内容&#xff0c;如果为空则不更 …

将GO、Pathway富集结果整合在一张高颜值圆圈图上

富集分析是生物医学论文中非常常见的一类分析&#xff0c;例如GO富集分析&#xff0c;Pathway富集分析等。其结果一般包括以下几个要素&#xff1a;1&#xff0c;名字&#xff08;GO term或者KEGG description&#xff09;&#xff1b;2&#xff0c;该名字所包含的基因数目&…

400G数据中心短距离传输方案:400G QSFP-DD SR8光模块

随着更快、更高可靠性的网络需求增加&#xff0c;400G将是下一代骨干网升级和新建设的方向。400G光模块在构建400G网络系统中起着至关重要的作用。前面我们为大家介绍了短距离单模应用的400G QSFP-DD DR4光模块&#xff0c;本期文章&#xff0c;我们一起来了解一下短距离多模光…

自定义类型:结构体,枚举,联合(2)

TIPS 1. 类型的定义可以考虑放在头文件里头。 2. 一个汉字存储的时候占两个字节空间 3. 关于结构体变量初始化的一些细节 4. 关于结构体内存对齐的补充 1. 2. S1和S2类型的成员一模一样&#xff0c;但是S1和S2所占空间的大小有了一些区别。 3. 这两个结构体类型成员都…

【Linux】六、Linux 基础IO(一)|重谈文件|C语言文件操作|操作系统文件操作(系统文件I/O)|文件描述符

目录 一、重谈文件 二、C语言文件操作 2.1 重谈C语言文件操作 2.2 补充细节 三、操作系统文件操作&#xff08;系统文件I/O&#xff09; 3.1 文件相关系统调用&#xff1a;close 3.2 文件相关系统调用&#xff1a;open 3.2.1 open 的第二个参数 flags 3.2.2 open 的第…

解决跨微服务调用token共享问题

场景描述 使用jeecg搭建SpringCloud微服务系统模块&#xff0c;各个系统模块单独创建了拦截器进行权限校验。结果发现跨微服务调用存在鉴权失败问题。不能正常跨微服务调用。 原因描述 单个微服务鉴权拦截器。 package org.jeecg.modules.taxation.inerceptor;import org.s…

【MySQL】MySQL单表操作

序号系列文章2【MySQL】MySQL基本操作详解3【MySQL】MySQL基本数据类型4【MySQL】MySQL表的七大约束5【MySQL】字符集与校对集详解文章目录MySQL单表操作1&#xff0c;数据操作1.1&#xff0c;复制表结构和数据1.2&#xff0c;解决主键冲突1.3&#xff0c;清空数据1.4&#xff…

二叉树详解(概念+遍历实现)

一、基本概念 1.最左孩子结点&#xff1a;一个结点的孩子结点中位于最左边的孩子结点。例如&#xff0c;A——B&#xff0c;B——E&#xff1b; 2.树的高度&#xff1a;树的最高层数&#xff1b; 3.路径长度&#xff1a;树中的任意两个顶点之间都存在唯一的一条路径。一条路径所…

我们这样做容器分层性能测试

前言目前闲鱼不少业务正在从H5/Weex升级到Kun&#xff08;基于W3C标准&Flutter打造的混合高性能终端容器&#xff09;&#xff0c;从测试角度来看&#xff0c;我们希望这种升级迭代对于用户体验是正向的&#xff0c;所以用好性能测试这把标准尺就显得格外重要。早期做性能保…

有什么比较好用的低代码开发平台?

国内有特色的低代码快速开发平台产品有哪些&#xff1f;这篇就来介绍下目前市面上主要的几家零代码开发平台&#xff01; 简道云、明道云、IVX这几家目前是无代码赛道的明星选手&#xff0c;在市场综合表现上名列前茅。宜创、红圈营销虽也极具潜力&#xff0c;但在市场表现力上…

Java开发技术之成为高级java工程师必须学习的三个技术

所谓的Java高级程序员往往是经验和能力的结合&#xff0c;并不是说掌握了哪几个技术就是高级程序员了&#xff0c;能否把掌握的知识运用到实际的项目中&#xff0c;并且解决了具体的问题&#xff0c;这个才是衡量一个Java程序员的标准。 那么对于一名Java程序员来说&#xff0…

Java项目:房屋租赁系统设计和实现(java+ssm+mysql+spring+jsp)

源码获取&#xff1a;博客首页 "资源" 里下载&#xff01; 主要功能描述&#xff1a; 1.登录管理&#xff1a;主要有管理员登录和租客登录 2.房源列表以及添加房源功能&#xff1a; 3.租赁合同管理以及在租房源和已退租房源信息管理: 4.看房申请和退租申请管理&a…

【 java 集合】HashMap源码分析

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

python基础篇之列表(增删改查)

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a;lqj_本人的博客_CSDN博客-微信小程序,前端,vue领域博主lqj_本人擅长微信小程序,前端,vue,等方面的知识https://blog.csdn.net/lbcyllqj?spm1000.2115.3001.5343 哔哩哔哩欢迎关注&…

excel数据统计:三个公式提高统计工作效率

善于在工作中使用函数、公式可以提高工作效率&#xff0c;结合近期学员们遇到的问题&#xff0c;老菜鸟总结了三个非常实用的公式&#xff0c;每个公式都可以解决一类问题。学会这三个公式套路&#xff0c;就能解决日常遇到的很多麻烦事。第一类问题&#xff1a;对指定时间段的…

通过nvm 控制node的常见命令

通过nvm 控制node查看本电脑安装的node版本号切换到对应的node版本号可以查看nvm的全部命令查看node可安装的全部版本号下载对应node的版本查看本电脑安装的node版本号 nvm ls 查看本电脑安装的node版本号 切换到对应的node版本号 nvm use 版本号 切换到对应的node版本号 注意…