平衡二叉搜索树(AVL)——【C++实现插入、删除等操作】

news2025/1/24 11:46:10

在这里插入图片描述

本章完整代码gitee地址:平衡二叉搜索树

文章目录

  • 🌳0. 前言
  • 🌲1. AVL树概念
  • 🌴2. 实现AVL树
    • 🌿2.1 结构定义
    • 🌿2.2 插入
      • 💐左单旋
      • 💐右单旋
      • 💐左右双旋
      • 💐右左双旋
    • 🌿2.3 查找
    • 🌿2.4 删除
    • 🌿2.5 树的高度
    • 🌿2.6 是否为平衡树
    • 🌿2.7 遍历(中序)

🌳0. 前言

C++的mapset这两个容器的底层就搜索树,关于搜索树,之前此篇文章讲过:数据结构——二叉搜索树,但它可能会出现极端情况:退化为链表

image-20230906133632229

所以再次基础上,就要将这颗树变为平衡的二叉搜索树——ALV树红黑树,本章讲解的是AVL树

🌲1. AVL树概念

AVL树俄罗斯的两位数学家G.M.Adlson-VelskiiE.M.Landis在1962年发表的论文《An algorithm for the organization of information》公开了这种结构:向二叉树插入一个新节点后,保证每个左右子树的高度之差绝对值不超过1,即可降低树的高度,减少平均搜索的长度

将左子树减去右子树的深度的值,成为平衡因子BF(Balance Factor),由于绝对值不超过1,则平衡因子的范围是**[-1,1]**,如果超过,就说明目前这棵树不是平衡的,需要进行调整

image-20230906135941793

由于树的左右子树是平衡的,所以要对这棵树操作的时候,最多进行高度次操作:O(lonN)

满二叉树:2h-1 = N

AVL树:2h - X = N(X的范围为[1,2h-1-1]),属于O(lonN),这个量级

🌴2. 实现AVL树

🌿2.1 结构定义

//定义成kv结构
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)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}
};

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;

public:
	//功能
private}

🌿2.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 (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;
        }
    }

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

    //检查
    while (parent)
    {
        //更新平衡因子
        if (cur == parent->_left)
        {
            parent->_bf--;
        }
        else
        {
            parent->_bf++;
        }

        //判断
        if (parent->_bf == 0)
        {
            //很健康,直接跳出
            break;
        }
        else if (parent->_bf == 1 || parent->_bf == -1)
        {
            //小问题,无大碍
            //继续往上更新
            //cur = parent;
            parent = parent->_parent;
            cur = cur->_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)	//单纯左高 -- 右单旋
            {
                RotateR(parent);
            }
            else if (parent->_bf == 2 && cur->_bf == -1)	//右子树高,插入点在右子树的左子树引发 -- 右左双旋	
            {
                RotateRL(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == 1)	//左子树高,插入点在左子树的右子树引发	-- 左右双旋
            {
                RotateLR(parent);
            }
            else
            {
                assert(false);
            }

            break;
        }
        else
        {
            //防止写的代码有bug
            assert(false);
        }
    }
    return true;
}

我们设平衡因子为:右子树-左子树

  1. 新增左节点,parent平衡因子减一

  2. 新增右节点,parent平衡因子加一

  3. 更新后的parent平衡因子 == 0 ,说明parent所在子树高度不变,无需继续更新祖先节点

    更新后的parent平衡因子 == 1 或- 1,说明parent所在子树高度发生变化,影响祖先,需要继续更新祖先节点

  4. 更新后的parent平衡因子 == 2 或 -2,说明parent所在子树高度发生变化,且不平衡,需要对parent对所在子树进行旋转,让其平衡

这里的平衡因子起到一个检测作用,查看这棵树是否“生病”,如果发现“生病”,立即“治疗”,所以不可能出现大于2的绝对值的平衡因子

💐左单旋

单纯右边高,采用左单旋进行调整,具体情况如图:

image-20230907101404348

核心操作:

  1. `parent->right = cur->left;``
  2. ``cur->left = parent;`

左单旋实现:

//左单旋
void RotateL(Node* parent)
{
    Node* cur = parent->_right;
    Node* curleft = cur->_left;

    parent->_right = curleft;
    if (curleft)
    {
        curleft->_parent = parent;
    }
    cur->_left = parent;
    Node* ppnode = parent->_parent;
    parent->_parent = cur;

    if (parent == _root)
    {
        _root = cur;
        cur->_parent = nullptr;
    }
    else
    {
        if (ppnode->_left == parent)
        {
            ppnode->_left = cur;
        }
        else
        {
            ppnode->_right = cur;
        }
        cur->_parent = ppnode;
    }
    parent->_bf = cur->_bf = 0;
}

动态示例:

left

💐右单旋

单纯的左边高,采用右单旋进行调整,具体情况如图:

image-20230907105426442

核心操作:

  1. parent->left = cur->right;
  2. cur->right = parent;

右单旋实现:

//右单旋
void RotateR(Node* parent)
{
    Node* cur = parent->_left;
    Node* curRight = cur->_right;

    parent->_left = cur->_right;
    //防止curRight为空
    if (curRight)
    {
        curRight->_parent = parent;
    }

    cur->_right = parent;
    //保存父亲的父亲节点
    Node* ppnode = parent->_parent;
    parent->_parent = cur;

    if (parent == _root)	
    {
        _root = cur;
        cur->_parent = nullptr;
    }
    else
    {
        if (ppnode->_left == parent)
        {
            ppnode->_left = cur;
        }
        else
        {
            ppnode->_right = cur;
        }
        cur->parent = ppnode;
    }
    //更新平衡因子
    parent->_bf = cur->_bf = 0;
}

动图示例:

right

💐左右双旋

新插入节点在较高左子树的右侧,采用右左双旋,具体情况如图:

image-20230907191809620

平衡因子更新需要看cur->right的平衡因子情况:

image-20230907193212207

  1. curRight == 0,它就是插入节点,全部更新为0
  2. curRight == 1c插入,cur->_bf = -1parent->_bf = 0
  3. curRight == -1b插入,cur->_bf = 0,·parent->_bf = 1`

核心操作:

  1. parent->left为旋转点左旋
  2. parent为旋转点右旋
  3. 根据curRight->_bf调整平衡因子

💐右左双旋

新插入节点在较高右子树的左侧,采用右左双旋,具体情况如图:

image-20230907171534105

这里平衡因子的更新,不能和单旋一样直接更新为0,要分情况,我们这里主要是看cur->left的平衡因子

image-20230907190633396

  1. curLeft->_bf == 0,则它就是插入节点,平衡因子全部更新为0
  2. curLeft->_bf == 1c插入,cur->_bf = 0parent->_bf = -1
  3. curLeft->_bf == -1,b插入,cur->_bf = 1parent->_bf = 0

核心操作:

  1. 以为parent->right为旋转点右旋
  2. parent为旋转点左旋
  3. 根据curLeft->_bf更新平衡因子

右左双旋代码实现:

	//右左双旋
	void RotateRL(Node* parent)
	{
		Node* cur = parent->_right;
		Node* curLeft = cur->_left;
		int bf = curLeft->_bf;
        
		RotateR(parent->_right);
		RotateL(parent);

		if (bf == 0)
		{
			//curLeft为新增节点
			cur->_bf = 0;
			curLeft->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			//curLeft右子树插入
			cur->_bf = 0;
			curLeft->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)
		{
			//curLeft左子树插入
			cur->_bf = 1;
			curLeft->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

动图示例:

r_lRtate

🌿2.3 查找

//查找
Node* Find(const K& key)
{
    return _Find(_root, key);
}

因为_root是私有成员,外部无法使用,所以我们设置一个子函数

Node* _Find(Node* root, const K& key)
{
    if (root == nullptr || root->_kv.first == key)
        return root;

    if (root->_kv.first > key)
        return _Find(root->_left, key);
    else
        return _Find(root->_right, key);
}

🌿2.4 删除

删除操作就不多说了,注释写的特别清楚

基本思路就是:

  1. 删除元素
  2. 更新平衡因子

与插入类似,但细节还是很多

//删除
bool Erase(const K& key)
{
    return _Erase(key);
}

子函数:

bool _Erase(const K& key)
{
    Node* parent = nullptr;
    Node* cur = _root;
    //查找元素
    while (cur)
    {
        if (cur->_kv.first > key)
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (cur->_kv.first < key)
        {
            parent = cur;
            cur = cur->_right;
        }
        else
            break;
    }
    //该元素不存在
    if (cur == nullptr)
        return false;
    //删除元素

    //1.左右子树都为空
    if (cur->_left == nullptr && cur->_right == nullptr)
    {
        //根节点直接删除退出
        if (cur == _root)
        {
            delete _root;
            _root = nullptr;
            return true;
        }
        if (cur == parent->_left)
        {
            //删除的是左孩子
            parent->_bf++;
            parent->_left = nullptr;
        }
        else
        {
            //删除的是右孩子
            parent->_bf--;
            parent->_right = nullptr;
        }
        delete cur;
        cur = parent;
    }
    else if (cur->_left == nullptr)	//左子树为空
    {
        if (cur == _root)
        {
            _root = cur->_right;
            cur->_right->_parent = nullptr;
            delete cur;
            cur = nullptr;

            return true;
        }
        //因为是平衡二叉树,如果左子树为空,那么右子树至多只有一个节点
        //这里前面已经判断了双空的情况,那么肯定右子树只有一个节点,直接删除即可
        Node* curRight = cur->_right;

        //替换元素
        cur->_kv.first = curRight->_kv.first;
        cur->_kv.second = curRight->_kv.second;

        //删除节点
        cur->_right = nullptr;
        delete curRight;
        curRight = nullptr;
        //删右孩子,父节点平衡因子-1
        cur->_bf--;
    }
    else if (cur->_right == nullptr)	//右子树为空
    {
        if (cur == _root)
        {
            _root = cur->_left;
            cur->_left->_parent = nullptr;
            delete cur;
            cur = nullptr;
            return true;
        }
        //与左子树为空同理
        Node* curLeft = cur->_left;

        //替换元素
        cur->_kv.first = curLeft->_kv.first;
        cur->_kv.second = curLeft->_kv.second;

        //删除节点
        cur->_left = nullptr;
        delete curLeft;
        curLeft = nullptr;
        //删除左孩子,父节点平衡因子+1
        cur->_bf++;
    }
    else
    {
        //左右子树都不为空
        //以左子树最大元素为例
        parent = cur;	//前驱
        Node* prev = cur->_left;	//找左子树最大元素
        while (prev->_right)
        {
            parent = prev;
            prev = prev->_right;
        }
        //替换元素
        cur->_kv.first = prev->_kv.first;
        cur->_kv.second = prev->_kv.second;

        //右边肯定没有元素了,因为找的就是最大的元素:左子树里面的最右边
        if (parent->_left == prev)
        {
            parent->_left = prev->_left;	
            parent->_bf++;
        }
        else
        {
            parent->_right = prev->_left;
            parent->_bf--;
        }
        delete prev;
        prev = nullptr;
        //指向删除节点父亲
        cur = parent;
    }

    parent = cur->_parent;

    //重新找调整位置
    if (cur->_bf == -2 || cur->_bf == 2)
    {
        if (cur->_bf == -2)
        {
            Node* curLeft = cur->_left;
            parent = cur;
            cur = curLeft;
        }
        else
        {
            Node* curRight = cur->_right;
            parent = cur;
            cur = curRight;
        }
    }

    //if (cur->_bf == 0 && parent != nullptr && abs(parent->_bf) == 2)
    //{
    //	if (cur = parent->_left)
    //		cur = parent->_right;
    //	else
    //		cur = parent->_left;
    //}

    while (parent)
    {
        //更新父亲的平衡因子
        parent->_bf = UpdateBf(parent);

        if (parent->_bf == 2 || parent->_bf == -2)
        {
            //“生病”了
            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)	//右子树高,插入点在右子树的左子树引发 -- 右左双旋	
            {
                RotateRL(parent);
            }
            else if (parent->_bf == 2 && cur->_bf == 1)	//左子树高,插入点在左子树的右子树引发	-- 左右双旋
            {
                RotateLR(parent);
            }
            else if (parent->_bf == 2)
            {
                //cur->_bf == 0时
                RotateL(parent);
                //再更新一次
                parent->_bf = UpdateBf(parent);
            }
            else if (parent->_bf == -2)
            {
                RotateR(parent);
                parent->_bf = UpdateBf(parent);
            }
            else
            {
                assert(false);
            }
        }

        cur = parent;
        parent = cur->_parent;
    }
    return true;
}
int UpdateBf(Node* root)
{
    if (!root)
        return 0;
    int leftH = _Height(root->_left);
    int rightH = _Height(root->_right);
    return rightH - leftH;
}

🌿2.5 树的高度

int _Height(Node* root)
{
    if (root == nullptr)
        return 0;

    //记录深度
    int leftH = _Height(root->_left);
    int rightH = _Height(root->_right);

    //记录更深的那一个
    return std::max(leftH, rightH) + 1;
}

🌿2.6 是否为平衡树

bool _IsAVLTree(Node* root)
{
    //空树符合AVL树
    if (root == nullptr)
        return true;

    //左子树的高度
    int leftH = _Height(root->_left);
    //右子树高度
    int rightH = _Height(root->_right);

    //看平衡因子是否符合
    int bf = rightH - leftH;

    if (bf != root->_bf)
        return false;

    //平衡因子绝对值不大于
    //在一次递归左右子树
    return abs(bf) < 2 && _IsAVLTree(root->_left) && _IsAVLTree(root->_right);	
}

🌿2.7 遍历(中序)

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

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

那本次分享就到这里咯,AVL树主要是重点了解其中是如何变平衡的(各种旋转),在实际中,我们只有使用C++里面的mapset(底层是红黑树)。

我们下期再见咯,如果还有下期的话。

Tips:
如果代码有bug,希望大家能指出来,看了网上好多的都有bug
不知道这个有没有bug,我测了一些数据,目前没发现bug

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

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

相关文章

c++day1

练习&#xff1a;使用cout完成输出斐波那契前20项的内容 1 1 2 3 5 8 13.。。。 #include <iostream> using namespace std;int main() {int a[20]{1,1};for(int i2;i<20;i){a[i]a[i-1]a[i-2];}for(int i0;i<20;i){cout<<a[i]<<" ";}retur…

javascript【格式化时间日期】

javascript【格式化时间日期】 操作&#xff1a; (1) 日期格式化代码 /*** 日期格式化函数<br/>* 调用格式&#xff1a;需要使用日期对象调用* <p> new Date().Format("yyyy/MM/dd HH:mm:ss"); </p>* param fmt 日期格式* returns {*} 返回格式化…

易优cms响应式月嫂家政服务公司网站模板源码—自适应手机端设计,支持后台管理

易优cms响应式月嫂家政服务公司网站模板源码 自适应手机端 带后台 模板基于EyouCMS内核制作,模板编码为UTF8 ,适合行业:家政服务类企业。 模板信息&#xff1a; 模板分类&#xff1a;摄像、婚庆、家政、保洁 适合行业&#xff1a;家政服务类企业 模板介绍&#xff1a; 本模…

龙蜥Anolis 8.8 安装MySQL

一、安装参考文档 官方文档&#xff1a;https://dev.mysql.com/doc/refman/8.0/en/linux-installation-yum-repo.html#yum-repo-installing-mysql出问题时的文档&#xff1a;https://blog.csdn.net/weixin_44798320/article/details/123446249 二、安装过程 2.1 下载官方的镜…

RocketMQ入门之学习环境搭建

文章目录 0.前言1.使用docker 方式搭建RocketMQ学习环境启动NameServer和 启动Broker常见报错 2.使用源码安装方式3. 常见问题3. 参考文档 0.前言 在学习RocketMQ 需要先自行搭建一个RocketMQ 服务端。本节我们先来搭建一个简单的学习环境。下个章节&#xff0c;我们写个简单的…

一起学数据结构(5)——栈和队列

1. 栈的相关定义及特点&#xff1a; 1. 栈的相关定义&#xff1a; 在正式介绍栈的定义之前&#xff0c;首先来回顾一下关于线性表的定义&#xff1a; 线性表是具有相同数据类型的个数据元素的有限序列&#xff0c;其中为表长。当时&#xff0c;可以把线性表看作一个空表&…

图床项目进度(三)——文件展示页

前言 该项目作为一个类网盘项目&#xff0c;主要包括上传下载&#xff0c;引用&#xff0c;预览等功能。 大致功能&#xff1a; 图片预览 这里的图片预览我使用的一个插件 const state: any reactive({ image: https://pic35.photophoto.cn/20150511/0034034892281415_b…

悲观锁和乐观锁、缓存

悲观锁&#xff1a; 悲观锁的实现通常依赖于数据库提供的机制&#xff0c;在整个处理的过程中数据处于锁定状态&#xff0c;session的load方法有一个重载方法&#xff0c;该重载方法的第三个参数可以设置锁模式&#xff0c;load(object.class , int id,LockMode.?)&#xff0…

deepin 如何卸载软件

文章目录 卸载软件&#xff08;正文&#xff09; 通常来讲在官方的应用商场卸载即可。 但是呢&#xff1f; 很不幸的是&#xff0c;没能够彻底删除软件。还是能够在启动器界面上看到应用。 这时候&#xff0c;你右键卸载&#xff0c;会提示“卸载失败”。如下图&#xff1a; …

VirtualBox RockyLinux9 网络连接

有几次都是隔一段时间之后启动虚拟机&#xff0c;用第三方ssh工具就连接不上了。 简单记录一下。 1、VirtualBox设置 2、IP设置 cd /etc/NetworkManager/system-connections/ vim enp0s3.nmconnection[connection] idenp0s3 uuid9c404b41-4636-397c-8feb-5c2ed38ef404 typeet…

windows nvm 安装 以及常用的命令

1 nvm 下载 链接&#xff1a;https://github.com/coreybutler/nvm-windows/releases 可下载以下版本&#xff1a; nvm-noinstall.zip&#xff1a;绿色免安装版&#xff0c;但使用时需要进行配置。 nvm-setup.zip&#xff1a;安装版&#xff0c;推荐使用 2 安装&#xff08…

python-38-python定时任务框架

Python定时任务 Python任务调度模块 – APScheduler python调度框架APScheduler使用详解 APScheduler动态增、删、改任务 apscheduler mysql 持久化任务 APScheduler调度框架 在项目中&#xff0c;我们可能遇到有定时任务的需求。 其一&#xff1a;定时执行任务。例如每天早上 …

为什么要学习源码之Java篇

为什么学习源码 大厂面试必问。二次开发。提升代码阅读能力&#xff0c;更能输出优质代码。提升技术功底。拥抱开源社区。快速定位线上问题。 学习源码的方式 首先最重要的是学会使用。具有全局观。先对大致有个细节的了解&#xff0c;一开始不要太关注于细节。学会看注释&a…

【计算机网络】TCP传输控制协议——三次握手

文章目录 握手的流程常考考点 握手的流程 一开始&#xff0c;客户端和服务端都处于CLOSE状态&#xff0c;先是服务端监听某个端口&#xff0c;处于LISTEN状态。然后客户端主动发起连接SYN&#xff0c;之后处于SYN-SEND状态。服务端收到发起的连接&#xff0c;返回SYN&#xff0…

Spring Data Commons远程命令执行漏洞复现(CVE-2018-1273)

一、漏洞说明 Spring Data是一个用于简化数据库访问&#xff0c;并支持云服务的开源框架,包含Commons、Gemfire、JPA、JDBC、MongoDB等模块。此漏洞产生于Spring Data Commons组件&#xff0c;该组件为提供共享的基础框架&#xff0c;适合各个子项目使用&#xff0c;支持跨数据…

快速学会git版本管理——创建分支和合并分支

首先创建分支 git创建分支只需要使用switch 命令&#xff1a; git switch -c 分支名 创建分支并切换到该分支 大家看后面的括号里已经变成了dev 说明我们切换成功了。 然后想要合并分支就在 创建的分支中 进行提交修改的内容&#xff0c;还是通过&#xff1a;add 命令和co…

EasyExcel入门(最简单的读)

官网&#xff1a;EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel (alibaba.com) 因为暂时项目没有用到&#xff0c;所以不急&#xff0c;知道了这个技术。就想着学着用一下&#xff01; 最简单的读 先看官方文档给的用法和解释&#xff01;&#xff01;&#xff01…

开开心心带你学习MySQL数据库之第八篇

索引和事务 ~~ 数据库运行的原理知识 面试题 索引 索引(index) > 目录 索引存在的意义,就是为了加快查找速度!!(省略了遍历的过程) 查找速度是快了&#xff0c;但是付出了一定的代价!! 1.需要付出额外的空间代价来保存索引数据 2.索引可能会拖慢新增,删除,修改的速度 ~~ …

信息系统项目管理师(第四版)教材精读思维导图-第十四章项目沟通管理

请参阅我的另一篇文章&#xff0c;综合介绍软考高项&#xff1a; 信息系统项目管理师&#xff08;软考高项&#xff09;备考总结_计算机技术与软件专业技术_铭记北宸的博客-CSDN博客 本章思维导图XMind源文件 14.1 管理基础 14.2 管理过程 14.3 规划沟通管理 14.4 管理沟通 14.…

机器学习:使用PCA简化数据

文章目录 使用场景主成分分析&#xff08;Principal component analysis&#xff09;实验&#xff1a;对半导体数据&#xff08;590个特征&#xff09;进行降维处理 使用场景 我们通过电视看实况足球&#xff0c;电视显示屏有100万个像素点&#xff0c;球所占的点数为100个。人…