[数据结构 C++] AVL树的模拟实现

news2024/11/26 22:26:13

在这里插入图片描述

文章目录

  • 1、AVL树
    • 1.1 AVL树的概念
  • 2、AVL树节点的定义
  • 3、AVL树的插入和旋转
    • 3.1 左单旋
      • 左旋代码实现
    • 3.2 右单旋
      • 右旋代码实现
    • 3.3 右左双旋
      • 右左双旋的代码实现
    • 3.4 左右双旋
      • 左右双旋的代码实现
    • 3.5 insert接口实现
  • 4、判断是否为AVL树
    • 判断AVL树的代码实现
  • 5、AVL树的性能

问题引入:
在上一篇文章中,我们提到了二叉搜索树在插入时,可能会形成单边树,会降低二叉搜索的性能。因此我们需要平衡二叉搜索树,降低二叉搜索树的高度,使得二叉搜索树趋于一颗完全二叉树的样子,这样就可以提高二叉搜索树的性能。本篇文章就来介绍一种平衡二叉树,AVL树。
在这里插入图片描述

1、AVL树

1.1 AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
    在这里插入图片描述
    如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(log N),搜索时间复杂度O(log N)。
    我们了解了AVL树的基本规则后,下面我们来实现一下AVL树。

2、AVL树节点的定义

template <class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;

	// 右子树 - 左子树 的高度差
	int _bf; // 平衡因子

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

3、AVL树的插入和旋转

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么
AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子

当某个节点的平衡因子被修改为2的时候,就需要旋转来调节,因此就存在一下四种旋转方式:

3.1 左单旋

我们将 左单旋的情况抽象出来,如下图所示:
在这里插入图片描述

当 h >= 0,且parent->_bf == 2 && subR->_bf == 1时,触发左旋。
在这个图中,只能是在 c 子树新增,才能触发左旋的条件parent->_bf == 2 && subR->_bf == 1。此时进行左旋。
如果是在 b 子树新增,那么仅仅左旋是不够的,
旋转步骤:将60的左树变为30的右树,将60的左树变为30,最后将parent和subR的平衡因子变为0就完成了左旋。

左旋代码实现

// 左单旋
void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* parentParent = parent->_parent;

    parent->_right = subRL;
    if (subRL)
        subRL->_parent = parent;
    subR->_left = parent;
    parent->_parent = subR;

    if (_root == parent) // 父节点就是根节点
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else // 子树情况
    {
        if (parentParent->_left == parent)
        {
            parentParent->_left = subR;
        }
        else
        {
            parentParent->_right = subR;
        }
        subR->_parent = parentParent;
    }
    // 修改平衡因子
    parent->_bf = subR->_bf = 0;
}

3.2 右单旋

我们将 右单旋的情况抽象出来,如下图所示:
在这里插入图片描述
当 h >= 0,且 parent->_bf == 2 && subL->_bf == -1时,触发右旋。
在这个图中,只能是在 a子树新增,才能触发右旋的条件parent->_bf == -2 && subL->_bf == -1。此时进行右旋。
如果是在 b 子树新增,那么仅仅右旋是不够的。
旋转步骤:将30的右树接到60的左树并断开与30的链接,再将60接到30的右树,并将60的父节点改为3,最后再调整parent与SubL的平衡因子为0,就完成整个右旋。

右旋代码实现

// 右单旋
void RotateR(Node* parent)
{
    Node* parentParent = parent->_parent;
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    if (subLR)
        subLR->_parent = parent;
    subL->_right = parent;
    parent->_parent = subL;

    if (_root == parent) // 父节点是根节点
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else // 子树情况
    {
        if (parentParent->_left == parent)
        {
            parentParent->_left = subL;
        }
        else
        {
            parentParent->_right = subL;
        }
        subL->_parent = parentParent;
    }
    // 修改平衡因子
    parent->_bf = subL->_bf = 0;
}

3.3 右左双旋

我们将 右左双旋的所有情况抽象出来,如下图所示:
在这里插入图片描述

右左双旋的本质是先将子树右旋,让右侧一侧高,再进行整体的左旋,这样就完成了高度的调整。
双旋的插入位置可以是 b/c 子树,此类型插入之后就会触发右左双旋。
旋转步骤:直接复用右旋,再复用左旋即可。不过旋转的基点不同,右旋是以subR为基点,左旋是以parent为基点旋转的。旋转就完成了,难点在于平衡因子的调节。
平衡因子的调节:
这里主要是 记下subRL最初的平衡因子它的平衡因子就代表了插入节点是在subRL的左边还是右边插入的,由此可以推出最终的parent与subR的平衡因子。

  • 当subRL->_bf = 1时,最后parent->_bf = -1,subR->_bf = 0,subRL->_bf = 0;
  • 当subRL->_bf = -1时,最后parent->_bf = 0,subR->_bf = 1,subRL->_bf = 0;
  • 当subRL->_bf = 0时,最后parent->_bf = 0,subR->_bf = 0,subRL->_bf = 0;

右左双旋的代码实现

// 右左双旋
void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;

    RotateR(subR);
    RotateL(parent);

    if (bf == 0) // subRL 就是插入的
    {
        parent->_bf = subR->_bf = subRL->_bf = 0;
    }
    else if (bf == 1) // subRL 右边边插入
    {
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else if (bf == -1) // subRL 左边插入
    {
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

3.4 左右双旋

我们将 右左双旋的所有情况抽象出来,如下图所示:
在这里插入图片描述

左右双旋与右左双旋的思路是差不多的,我们来看看。
左右双旋的本质是先将子树左旋,让左侧一侧高,在进行整体的右旋,这样就完成了高度的调整。
双旋的插入位置可以是 b/c 子树,此类型插入之后就会触发左右双旋。
旋转步骤:直接复用左旋,再复用右旋即可。不过旋转的基点不同,右旋是以subR为基点,左旋是以parent为基点旋转的。旋转就完成了,难点也是在于平衡因子的调节。
平衡因子的调节:
这里主要是 记下subLR最初的平衡因子它的平衡因子就代表了插入节点是在subLR的左边还是右边插入的,由此可以推出最终的parent与subL的平衡因子。

  • 当subLR->_bf = 1时,最后parent->_bf = 1,subL->_bf = 0,subLR->_bf = 0;
  • 当subLR->_bf = 1时,最后parent->_bf = 0,subL->_bf = -1,subLR->_bf = 0;
  • 当subLR->_bf = 0时,最后parent->_bf = 0,subL->_bf = 0,subLR->_bf = 0;

左右双旋的代码实现

// 左右双旋
void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;

    RotateL(subL);
    RotateR(parent);

    if (0 == bf)
    {
        parent->_bf = subL->_bf = subLR->_bf = 0;
    }
    else if (1 == bf)
    {
        parent->_bf = 0;
        subL->_bf = -1;
        subLR->_bf = 0;
    }
    else if (-1 == bf)
    {
        parent->_bf = 1;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

3.5 insert接口实现

bool Insert(const pair<K, V>& kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;
    // 1、先找到插入的位置
    while (cur)
    {
        if (cur->_kv.first < kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_kv.first > kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            return false;
        }
    }
    // 2、new一个节点,并与parent链接起来
    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
        cur->_parent = parent;
    }
    else
    {
        parent->_left = cur;
        cur->_parent = parent;
    }
    // 3、调平横 —— 旋转 + 平衡因子的调节
    while (parent)
    {
        if (parent->_left == cur)
        {
            parent->_bf--;
        }
        else
        {
            parent->_bf++;
        }

        if (0 == parent->_bf)
        {
            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)
            {
                RotateL(parent);
            }
            else if (parent->_bf == 2 && cur->_bf == -1)
            {
                RotateRL(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == 1)
            {
                RotateLR(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == -1)
            {
                RotateR(parent);
            }

            // 1、旋转让这颗子树平衡了
            // 2、旋转降低了这颗子树的高度,恢复到跟插入前一样的高度,所以对上一层没有影响,不用继续更新
            break;
        }
        else
        {
            assert(false);
        }
    }
    return true;
}

4、判断是否为AVL树

AVL树的本质是搜索二叉树 + 平衡机制,所以验证步骤:
1、首先判断是否为搜索树,写一个中序遍历,看看是不是升序即可;
2、按照AVL树的性质来判断:

  • 每个节点的左右子树高度差绝对值小于等于1;
  • 节点的平衡因子是否正确;

判断AVL树的代码实现

bool _IsBalance(Node* pRoot)
{
    if (pRoot == nullptr)
        return true;

    int leftHeight = _Height(pRoot->_left);
    int rightHeight = _Height(pRoot->_right);
    if (rightHeight - leftHeight != pRoot->_bf)
    {
        cout << pRoot->_kv.first << "平衡因子异常" << endl;
        return false;
    }

    return rightHeight - leftHeight < 2
        && _IsAVLTree(pRoot->_left)
        && _IsAVLTree(pRoot->_right);
}

size_t _Height(Node* pRoot)
{
    if (pRoot == nullptr)
        return 0;

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

    return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

void _InOrder(Node* pRoot)
{
    if (pRoot == nullptr)
        return;

    _InOrder(pRoot->_left);
    cout << pRoot->_kv.first << " ";
    _InOrder(pRoot->_right);
}

5、AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O(log N)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
AVL树的实现代码放在代码仓库:https://gitee.com/xiaobai-is-working-hard-jy/data-structure/tree/master/AVLTree

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

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

相关文章

Maven Resources Compiler: Maven project configuration required for module

Maven Resources Compiler: Maven project configuration required for module ‘cc-pdf’ isn’t available. Compilation of Maven projects is supported only if external build is started from an IDE. 报错原因是&#xff0c;我在git建立一个新空仓库&#xff0c;然后把…

C++线程池的原理(画图)及简单实现+例子(加深理解)

1.为什么线程池会出现&#xff0c;解决什么问题&#xff1f; C线程池&#xff08;ThreadPool&#xff09;的出现主要是为了解决以下几个问题&#xff1a; 1.性能&#xff1a;创建和销毁线程都是相对昂贵的操作&#xff0c;特别是在高并发场景下&#xff0c;频繁地创建和销毁线…

Linux下误删除后的恢复操作测试之extundelete工具使用

一、工具介绍 extundelete命令的功能可用于系统删除文件的恢复。在使用前&#xff0c;需要先将要恢复的分区卸载&#xff0c;以防数据被意外覆盖。 语法格式&#xff1a;extundelete [参数] 文件或目录名 常用参数&#xff1a; --after 只恢复指定时间后被删除的文件 --bef…

Linux学习(9)——RAID与服务器的常见故障

目录 一、服务器常见故障 1、系统不停重启进入不了系统 2、卡在开机界面右下角有fA B2 H8 3、系统安装不上 4、如何进入服务器的bios 5、一般进入阵列卡的快捷键 6.网络不通 7.硬盘不识别 二、RAID相关知识 1、RAID的概念 2、RAID功能实现 3、RAID实现的方式 三、…

机器学习笔记 - 偏最小二乘回归 (PLSR)

一、偏最小二乘回归:简介 PLS 方法构成了一个非常大的方法族。虽然回归方法可能是最流行的 PLS 技术,但它绝不是唯一的一种。即使在 PLSR 中,也有多种不同的算法可以获得解决方案。PLS 回归主要由斯堪的纳维亚化学计量学家 Svante Wold 和 Harald Martens 在 20 世纪 80 年代…

海外服务器2核2G/4G/8G和4核8G配置16M公网带宽优惠价格表

腾讯云海外服务器租用优惠价格表&#xff0c;2核2G10M带宽、2核4G12M、2核8G14M、4核8G16M配置可选&#xff0c;可以选择Linux操作系统或Linux系统&#xff0c;相比较Linux服务器价格要更优惠一些&#xff0c;腾讯云服务器网txyfwq.com分享腾讯云国外服务器租用配置报价&#x…

ByteTrack算法流程的简单示例

ByteTrack ByteTrack算法是将t帧检测出来的检测框集合 D t {\mathcal{D}_{t}} Dt​ 和t-1帧预测轨迹集合 T ~ t − 1 {\tilde{T}_{t-1}} T~t−1​ 进行匹配关联得到t帧的轨迹集合 T t {T_{t}} Tt​。 首先使用检测器检测t帧的图像得到检测框集合 D t {\mathcal{D}_{t}} …

手机技巧:分享10个vivo手机实用小技巧技巧,值得收藏

目录 1. 快速切换应用 2、智能助手Jovi 3. 轻按唤醒屏幕 4. 快速启动相机 5. 分屏功能 6. 手势操作 7. 一键清理 8.忘记密码 9.玩游戏耗电快 10.手机丢失后该怎么办 1. 快速切换应用 向右或向左滑动底部的虚拟按键即可。 2、智能助手Jovi vivo手机自带智能助手Jovi…

【Java EE初阶八】多线程案例(计时器模型)

1. java标准库的计时器 1.1 关于计时器 计时器类似闹钟&#xff0c;有定时的功能&#xff0c;其主要是到时间就会执行某一操作&#xff0c;即可以指定时间&#xff0c;去执行某一逻辑&#xff08;某一代码&#xff09;。 1.2 计时器的简单介绍 在java标准库中&#xff0c;提供…

CMake入门教程【核心篇】添加应用程序(add_executable)

&#x1f608;「CSDN主页」&#xff1a;传送门 &#x1f608;「Bilibil首页」&#xff1a;传送门 &#x1f608;「本文的内容」&#xff1a;CMake入门教程 &#x1f608;「动动你的小手」&#xff1a;点赞&#x1f44d;收藏⭐️评论&#x1f4dd; 文章目录 1. 概述2. 使用方法2…

【计算机视觉】常用图像数据集

图像数据集 模型需要好的数据才能训练出结果&#xff0c;本文总结了机器学习图像方面常用数据集。 MNIST 机器学习入门的标准数据集&#xff08;Hello World!&#xff09;&#xff0c;10个类别&#xff0c;0-9 手写数字。包含了60,000 张 28x28 的二值训练图像&#xff0c;10…

计算机网络(2)

计算机网络&#xff08;2&#xff09; 小程一言专栏链接: [link](http://t.csdnimg.cn/ZUTXU) 计算机网络和因特网&#xff08;2&#xff09;分组交换网中的时延、丢包和吞吐量时延丢包吞吐量总结 协议层次及其服务模型模型类型OSI模型分析TCP/IP模型分析 追溯历史 小程一言 我…

Graphics Control

Graphics Control提供了一个易于使用的图形设置管理解决方案,帮助您加快开发。它附带了一个常用设置库,如分辨率、垂直同步、全屏模式、光晕、颗粒、环境光遮挡等。我们的可自定义设置面板UI预制件为您提供了一个可用的UI面板,支持完整的游戏手柄和键盘输入。图形控制还附带…

【前沿技术杂谈:ChatGPT】ChatGPT——热潮背后的反思

【前沿技术杂谈&#xff1a;ChatGPT】ChatGPT——热潮背后的反思 缘起&#xff1a;无中生有&#xff0c;涅槃重生人工智能技术人工智能的发展史无中生有内容自动生成技术的发展代表企业OpenAI-GPT系列技术的发展历程ChatGPT新特点 热潮&#xff1a;万众瞩目&#xff0c;群雄逐鹿…

Unity | Shader基础知识番外(向量数学知识速成)

目录 一、向量定义 二、计算向量 三、向量的加法&#xff08;连续行走&#xff09; 四、向量的长度 五、单位向量 六、向量的点积 1 计算 2 作用 七、向量的叉乘 1 承上启下 2 叉乘结论 3 叉乘的计算&#xff08;这里看不懂就百度叉乘计算&#xff09; 八、欢迎收…

Vue3地图选点组件

Vue3地图选点组件 <template><div style"width: 100%; height: 500px"><div class"search-container"><el-autocompletev-model"suggestionKeyWord"class"search-container__input"clearable:fetch-suggestion…

【已解决】You have an error in your SQL syntax

报错讯息 java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘desc,target_url,sort,status,create_by,modify_by,created,last_update_time FROM…

图像分割 分水岭法 watershed

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 本文的C#版本请访问&#xff1a;图像分割 分水岭法 watershed&#xff08;C#&#xff09;-CSDN博客 Watershed算法是一种图像处理算…

SSM的校园二手交易平台----计算机毕业设计

项目介绍 本次设计的是一个校园二手交易平台&#xff08;C2C&#xff09;&#xff0c;C2C指个人与个人之间的电子商务&#xff0c;买家可以查看所有卖家发布的商品&#xff0c;并且根据分类进行商品过滤&#xff0c;也可以根据站内搜索引擎进行商品的查询&#xff0c;并且与卖…