【C++】二叉搜索树+变身 = AVL树

头像
🚀个人主页:@小羊
🚀所属专栏:C++
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述

目录

  • 前言
  • 一、AVL树
  • 二、AVL树的实现
    • 2.1 平衡因子
    • 2.2 旋转处理
      • 2.2.1 左单旋:插入新节点后单纯的右边高
      • 2.2.2 右单旋:插入新节点后单纯的左边高
      • 2.2.3 左右旋:插入新节点后不是单纯的左边高
      • 2.2.4 右左旋:插入新节点后不是单纯的右边高
    • 2.3 验证AVL树的平衡
  • 三、完整代码


前言

本文仅适合了解二叉搜索树,但不了解AVL树底层原理的同学阅读哦。

本篇文章不会带你从头到尾实现AVL树,但会带你深入理解AVL树是怎么实现平衡的,怎么通过旋转变换实现保持平衡,以及实现平衡过程中的细节应该怎么处理等。


一、AVL树

前面的文章中我们分析过二叉搜索树的性能,得到的结果是理想情况下二叉搜索树的时间复杂度为O(LogN),但在极端情况下(即树蜕化为单边树时),这些操作的时间复杂度会退化为O(n),即使情况不那么极端,效率也不是特别高。

为了防止二叉搜索树出现一边偏高的情况,就需要想办法让二叉搜索树尽量保持平衡,所以两位苏联数学家(或称为俄罗斯数学家)G.M. Adelson-Velsky和E.M. Landis就发明了AVL树,其任何节点的两个子树的高度最大差别为1。

AVL树是具有一下性质的二叉搜索树:

  • 其左右子树都是AVL树
  • 左右子树高度差不超过1

二、AVL树的实现

本篇文章将沿用之前文章中Key-Value模型的代码,不再从底层开始实现,主要介绍在插入新节点后如何保持二叉搜索树的平衡问题。

2.1 平衡因子

如何保证AVL树的左右子树高度差不超过1?在AVL树的每个节点中存一个平衡因子,本文我们定义平衡因子 = 此节点右子树的高度 - 左子树的高度

  • 插入在左子树,平衡因子 - -
  • 插入在右子树,平衡因子++

更新祖先节点的平衡因子时,我们首先需要找到祖先节点,因此每个节点中还需要增加一个指向父节点的指针。
按照我们的需求,其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;//平衡因子//构造AVLTreeNode(const pair<K, V>& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}
}

是否继续往上更新祖先节点的平衡因子,要看parent所在子树的高度是否发生变化。

插入新节点后其父节点的平衡因子有以下几种情况:

  1. parent的平衡因子 == 0
    parent的平衡因子更新前是 -1 / 1,新节点插入在矮的那边,高度不变,不再往上更新
  2. parent的平衡因子 == 1 / -1
    parent的平衡因子更新前是0,parent所在子树高度都变化了,需要往上更新
  3. parent的平衡因子 == 2 / -2
    parent的平衡因子更新前是 -1 / 1,插入新节点后树不再平衡,需要旋转处理
pcur = new Node(kv);
if (parent->_kv.first > kv.first)//判断新节点应该插入左还是右
{parent->_left = pcur;
}
else
{parent->_right = pcur;
}
pcur->_parent = parent;//与父节点链接关系while (parent)//有可能更新到根节点去
{parent->_bf = parent->_left == pcur ? parent->_bf - 1 : parent->_bf + 1;if (parent->_bf == 0)//插入前后高度不变{break;}else if (parent->_bf == 1 || parent->_bf == -1){//高度变了,继续往上更新pcur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//插入节点后二叉树不平衡了,需要旋转处理}else{assert(false);//检测AVL树是否异常}
}

2.2 旋转处理

当二叉搜索树出现不平衡的情况时,需要旋转处理,对应插入后二叉搜索树的各种情况,主要有四种旋转的方式来保持平衡。

其中:

  • h代表子树的高度,可以是0、1、2…
  • 我们用能代表所有情况的四种类型的抽象图来研究旋转方式,单纯研究某几种情况没有意义

原则:

  1. 保持搜索树的性质
  2. 降低高度,控制平衡

2.2.1 左单旋:插入新节点后单纯的右边高

在这里插入图片描述

旋转处理过程中,我们主要关注三个节点(以上图为例):10(标记为parent)、30(标记为subR)、b(标记为subLR)。

在旋转过程中,有以下几种情况需要考虑:

  1. subR的左孩子可能存在,也可能不存在
  2. parent可能是根节点,也可能是子树。如果是根节点,旋转完成后,要更新根节点;如果是子树,可能是某个节点的左子树,也可能是右子树
//左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;//subRL是有可能为空的parent->_right = subRL;subR->_left = parent;Node* parentparent = parent->_parent;parent->_parent = subR;if (parentparent == nullptr)//subR有可能变成根{_root = subR;}else{if (parentparent->_left == parent){parentparent->_left = subR;}else{parentparent->_right = subR;}}subR->_parent = parentparent;if (subRL){subRL->_parent = parent;}parent->_bf = subR->_bf = 0;//更新平衡因子
}

旋转处理过程中主要是处理各节点的父节点指针的指向和平衡因子的更新。


2.2.2 右单旋:插入新节点后单纯的左边高

在这里插入图片描述

其处理方式和左单旋相似,可参考左单旋。

//右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;subL->_right = parent;Node* parentparent = parent->_parent;parent->_parent = subL;if (parentparent == nullptr){_root = subL;}else{if (parentparent->_left == parent){parentparent->_left = subL;}else{parentparent->_right = subL;}}subL->_parent = parentparent;if (subLR){subLR->_parent = parent;}subL->_bf = parent->_bf = 0;
}

2.2.3 左右旋:插入新节点后不是单纯的左边高

在这里插入图片描述

这种情况只用左旋或右旋只会原地打转,不能降低平衡。
我们需要先对subL进行左单旋,再对parent进行右单旋,最后更新平衡因子。

  • 双旋后平衡因子的更新要根据插入新节点后subLR的平衡因子来分情况讨论
  • 双旋最终结果是把subLR推到最上面,让其平衡因子为0
//左右旋
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{assert(false);}
}

2.2.4 右左旋:插入新节点后不是单纯的右边高

在这里插入图片描述

可参考左右旋。

//右左旋
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}
}

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


2.3 验证AVL树的平衡

我们可以分别计算出其左子树和右子树的高度,将其相减的值与节点中记录的平衡因子的值比较,看是否符合我们的预期。

int _Height(Node* root)
{if (root == nullptr){return 0;}int leftheight = _Height(root->_left);int rightheight = _Height(root->_right);return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}bool _isBalanceTree(Node* root)
{if (root == nullptr){return true;}int leftheight = _Height(root->_left);int rightheight = _Height(root->_right);int bf = rightheight - leftheight;if (abs(bf) > 1){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return _isBalanceTree(root->_left) && _isBalanceTree(root->_right);
}

三、完整代码

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:AVLTree() = default;AVLTree(const AVLTree<K, V>& t){_root = copy(t._root);}AVLTree<K, V>& operator=(AVLTree<K, V> t){swap(_root, t._root);return *this;}~AVLTree(){Destroy(_root);_root = nullptr;}bool Find(const K& key){Node* pcur = _root;while (pcur){if (key < pcur->_kv.first){pcur = pcur->_left;}else if (key > pcur->_kv.first){pcur = pcur->_right;}else{return true;}}return false;}bool Insert(const pair<K, V>& kv){//没有节点时需要单独处理if (_root == nullptr){_root = new Node(kv);return true;}Node* pcur = _root;Node* parent = nullptr;while (pcur){if (kv.first < pcur->_kv.first){parent = pcur;pcur = pcur->_left;}else if (kv.first > pcur->_kv.first){parent = pcur;pcur = pcur->_right;}else{return false;}}pcur = new Node(kv);if (parent->_kv.first > kv.first)//判断新节点应该插入左还是右{parent->_left = pcur;}else{parent->_right = pcur;}pcur->_parent = parent;//与父节点链接关系//更新平衡因子while (parent)//有可能更新到根节点去{parent->_bf = parent->_left == pcur ? parent->_bf - 1 : parent->_bf + 1;if (parent->_bf == 0)//插入前后高度不变{break;}else if (parent->_bf == 1 || parent->_bf == -1){//高度变了,继续往上更新pcur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//插入节点后二叉树不平衡了,需要旋转处理if (parent->_bf == 2 && pcur->_bf == 1){RotateL(parent);}else if (parent->_bf == -2 && pcur->_bf == -1){RotateR(parent);}else if (parent->_bf == 2 && pcur->_bf == -1){RotateRL(parent);}else if (parent->_bf == -2 && pcur->_bf == 1){RotateLR(parent);}break;//不管是哪种情况,旋转完后子树的高度没有变化,所以不再调整}else{assert(false);//检测AVL树是否异常}}return true;}void InOrder(){_InOrder(_root);cout << endl;}bool IsBalanceTree(){return _isBalanceTree(_root);}private://左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;//subRL是有可能为空的parent->_right = subRL;subR->_left = parent;Node* parentparent = parent->_parent;parent->_parent = subR;if (parentparent == nullptr)//subR有可能变成根{_root = subR;}else{if (parentparent->_left == parent){parentparent->_left = subR;}else{parentparent->_right = subR;}}subR->_parent = parentparent;if (subRL){subRL->_parent = parent;}parent->_bf = subR->_bf = 0;//更新平衡因子}//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;subL->_right = parent;Node* parentparent = parent->_parent;parent->_parent = subL;if (parentparent == nullptr){_root = subL;}else{if (parentparent->_left == parent){parentparent->_left = subL;}else{parentparent->_right = subL;}}subL->_parent = parentparent;if (subLR){subLR->_parent = parent;}subL->_bf = parent->_bf = 0;}//左右旋void RotateLR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 0){parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{assert(false);}}//右左旋void RotateRL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else{assert(false);}}Node* copy(Node* root){if (root == nullptr){return nullptr;}Node* copynode = new Node(root->_kv);copynode->_left = copy(root->_left);copynode->_right = copy(root->_right);return copynode;}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}void _InOrder(Node* root){if (root == nullptr)//递归一定要有结束条件{return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}int _Height(Node* root){if (root == nullptr){return 0;}int leftheight = _Height(root->_left);int rightheight = _Height(root->_right);return leftheight > rightheight ? leftheight + 1 : rightheight + 1;}bool _isBalanceTree(Node* root){if (root == nullptr){return true;}int leftheight = _Height(root->_left);int rightheight = _Height(root->_right);int bf = rightheight - leftheight;if (abs(bf) > 1){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return _isBalanceTree(root->_left) && _isBalanceTree(root->_right);}private:Node* _root = nullptr;
};

本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~

头像

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

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

相关文章

html5 + css3(上)

目录 HTML初识基础认知web标准vscode的简介和使用注释 HTML标签学习排版标签标题和段落换行和水平线标签 文本格式化标签媒体标签图片标签图片-基本使用图片-属性 路径绝对路径相对路径 音频标签视频标签链接标签 HTML基础列表标签列表-无序和有序列表-自定义 表格标签表格-使用…

【包教包会】2D图片实现3D透视效果(支持3.x、支持原生、可合批)

将去年写的SpriteFlipper从2.x升级到3.x。 如果需要2.x版本或需要了解算法思路&#xff0c;请移步&#xff1a;https://blog.csdn.net/weixin_42714632/article/details/136745051 优化功能&#xff1a;可同时绕X轴和Y轴旋转&#xff0c;两者效果会叠加。 完美适配Web、原生…

数据结构与算法篇(图)(持续更新迭代)

目录 一、引言 二、基本概念 三、图的定义 四、图的基本概念和术语 1. 有向图 2. 无向图 3. 简单图 4. 多重图 5. 完全图&#xff08;也称简单完全图&#xff09; 6. 子图 7. 连通、连通图和连通分量 8. 强连通图、强连通分量 9. 生成树、生成森林 10. 顶点的度、…

使用WPF实现一个快速切换JDK版本的客户端工具

发现网上一键切换JDK环境的方法都是在mac或Linux下的&#xff0c;本人主力电脑是Windows&#xff0c;于是看了一下WPF的文档&#xff0c;自己开发了一个客户端。 直接上代码吧&#xff1a; using JavaSwitch.Properties; using Newtonsoft.Json; using System; using System.…

【C++11】新特性

前言&#xff1a; C11 是C编程语言的一个重要版本&#xff0c;于2011年发布。它带来了数量可观的变化&#xff0c;包含约 140 个新特性&#xff0c;以及对 C03 标准中约600个缺陷的修正&#xff0c;更像是从 C98/03 中孕育出的新语言 列表初始化 C11 中的列表初始化&#xff0…

爬虫案例——爬取情话网数据

需求&#xff1a; 1.爬取情话网站中表白里面的所有句子&#xff08;表白词_表白的话_表白句子情话大全_情话网&#xff09; 2.利用XPath来进行解析 3.使用面向对象形发请求——创建一个类 4.将爬取下来的数据保存在数据库中 写出对应解析语法 //div[class"box labelbo…

vite学习教程03、vite+vue2打包配置

文章目录 前言一、修改vite.config.js二、配置文件资源/路径提示三、测试打包参考文章资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝3W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质作者、专注于Java后端技术领域。 涵盖技术内容&…

云手机可以解决TikTok运营的哪些问题?

随着社交媒体的飞速发展&#xff0c;TikTok迅速崛起&#xff0c;成为个人和企业进行品牌宣传和内容创作的首选平台。然而&#xff0c;在运营TikTok账号的过程中&#xff0c;不少用户会遇到各种问题。本文将详细阐述云手机如何帮助解决这些问题。 1. 多账号管理的高效便捷 通过云…

外包功能测试干了4年,技术退步太明显了。。。。。​

先说一下自己的情况&#xff0c;本科生&#xff0c;18年通过校招进入武汉某软件公司&#xff0c;干了差不多4年的功能测试&#xff0c;今年中秋&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测…

精准选择大模型:消费品行业的营销与体验创新之路

在消费品行业&#xff0c;大模型技术的引入正逐渐从一个新兴趋势转变为行业标配。随着人工智能的快速发展&#xff0c;特别是OpenAI等领军企业推出的创新技术&#xff0c;如Sora&#xff0c;大模型在市场营销、消费者行为分析、个性化推荐等方面展现出巨大潜力。然而&#xff0…

详解Java中的BIO、NIO、AIO

1、 详解Java中的BIO、AIO、NIO 1.1、引言 IO流是Java中比较难理解的一个知识点&#xff0c;但是IO流在实际的开发场景中经常会使用到&#xff0c;比如Dubbo底层就是NIO进行通讯。本文将介绍Java发展过程中出现的三种IO&#xff1a;BIO、NIO以及AIO&#xff0c;重点介绍NIO。…

动态桌面时钟 让时间在桌面舞动 发现生活中的美好瞬间!

在快节奏的现代生活中&#xff0c;时间是最宝贵的资源之一。无论是在工作还是生活中&#xff0c;我们都需要时刻关注时间&#xff0c;在桌面显示一个时钟&#xff0c;可以让你更方便的掌握时间。今天小编给大家推荐一个软件《芝麻时钟》&#xff08;下载地址&#xff1a;https:…

Linux高级编程_29_信号

文章目录 进程间通讯 - 信号信号完整的信号周期信号的编号信号的产生发送信号1 kill 函数(他杀)作用&#xff1a;语法&#xff1a;示例&#xff1a; 2 raise函数(自杀)作用&#xff1a;示例&#xff1a; 3 abort函数(自杀)作用&#xff1a;语法&#xff1a;示例&#xff1a; 4 …

汇编DEBUG程序调用

工具 系统&#xff1a;Windows 11 应用&#xff1a;DOSBox 0.74-3 下载安装教程&#xff1a;本人写的《DOSBox下载安装&#xff08;Windows系统 DOSBox 0.74-3&#xff09;》 https://blog.csdn.net/just_do_it_sq/article/details/142715182?spm1001.2014.3001.5501 相关文…

ARM 架构、cpu

一、ARM的架构 ARM是一种基于精简指令集&#xff08;RISC&#xff09;的处理器架构. 1、ARM芯片特点 ARM芯片的主要特点有以下几点&#xff1a; 精简指令集&#xff1a;ARM芯片使用精简指令集&#xff0c;即每条指令只完成一项简单的操作&#xff0c;从而提高指令的执行效率…

沂机管理系统/data/Ajax.aspx接口存在SQL注入漏洞

漏洞描述 沂机管理系统/data/Ajax.aspx接口存在SQL注入漏洞&#xff0c;攻击者可以获取服务器权限 漏洞复现 body"后台管理系统演示版" POC GET /data/Ajax.aspx?methodlog_list&page1&limit20&fkey1&fdate12024-10-0100%3A00%3A00&fdate2…

文章资讯职场话题网站源码整站资源自带2000+数据

介绍&#xff1a; 数据有点多&#xff0c;数据资源包比较大&#xff0c;压缩后还有250m左右。值钱的是数据&#xff0c;网站上传后直接可用&#xff0c;爽飞了 环境&#xff1a;NGINX1.18 mysql5.6 php7.2 代码下载

全球IP归属地查询-IP地址查询-IP城市查询-IP地址归属地-IP地址解析-IP位置查询-IP地址查询API接口

IP地址城市版查询接口 API是指能够根据IP地址查询其所在城市等地理位置信息的API接口。这类接口在网络安全、数据分析、广告投放等多个领域有广泛应用。以下是一些可用的IP地址城市版查询接口API及其简要介绍 1. 快证 IP归属地查询API 特点&#xff1a;支持IPv4 提供高精版、…

【零基础入门产品经理】学习准备篇 | 需要学一些什么呢?

前言&#xff1a; 零实习转行产品经理经验分享01-学习准备篇_哔哩哔哩_bilibili 该篇内容主要是对bilibili这个视频的观后笔记~谢谢美丽滴up主友情分享。 全文摘要&#xff1a;如何在0实习且没有任何产品相关经验下&#xff0c;如何上岸产品经理~ 目录 一、想清楚为什么…

k8s的简介和部署

一、k8s简介 在部署应用程序的方式上面&#xff0c;主要经历了三个阶段&#xff1a; 传统部署:互联网早期&#xff0c;会直接将应用程序部署在物理机上优点:简单&#xff0c;不需要其它技术的参与缺点:不能为应用程序定义资源使用边界&#xff0c;很难合理地分配计算资源&…