c++AVL树的模拟实现

前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个
共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中
插入的元素有序或者接近有序,二叉搜索树就会退化成单支树
,时间复杂度会退化成O(N),因此
map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现
 

目录

1. AVL树

1.1 概念

1.2 AVL树节点的定义

1.3 AVL树的插入(健康版) 

1.4 AVL树的旋转(非健康版)

1. 右旋图解

右旋代码 

 2. 左旋图解

左旋代码 

3. 右左双旋图解

右左双旋代码 

4. 左右双旋图解

左右双旋代码

总结 

1.5 AVL树的验证 

1.5.1 二叉搜索树的验证

1.5.2 AVL树的验证 

验证代码

1.6 AVL树的删除 

1.7 AVL树的性能

 


1. AVL树

1.1 概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查
找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis在1962年发明了一种解决上述问题的方法:

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树。
 它的左右子树都是AVL树
左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

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

1.2 AVL树节点的定义

AVL与红黑树中存放的数据都是pair<k,v>结构。 

struct AVLTreeNode
{AVLTreeNode<K, V>* _left;//左孩子AVLTreeNode<K, V>* _right;//右孩子AVLTreeNode<K, V>* _parent;//父亲pair<K, V> _kv;int _bf;//平衡因子,健康的平衡因子绝对值不大于1AVLTreeNode(const pair<K, V>& kv):_bf(0),_kv(kv), _left(nullptr), _right(nullptr),_parent(nullptr){}
};

这里可以看到,AVL树的节点与平衡二叉树的节点区别在于AVL树节点有一个平衡因子的存在,也即1.1概念中所述。同时AVL树采用了三叉链结构,好处在于在对树进行操作时,不必额外存储节点的父亲。

1.3 AVL树的插入(健康版) 

AVL树的插入规则与平衡二叉树一样,不过在插入时要兼顾平衡因子绝对值不大于1的规则,如果某节点的平衡因子绝对值大于1,则需要进行处理。

与平衡二叉树一样,在插入节点时需要先找到插入的位置,这里我们可以复刻平衡二叉树的代码。

插入之后需要对其平衡因子(后面简称bf)进行判断。

那么插入一个节点其bf应该怎么判断呢?

很简单,bf是以该节点为根的树左右子树高度差,我们可以规定,左边插入节点bf--,右边插入节点bf++,新增的节点bf当然是为0的。

 

请看上图,上图只是AVL树的具象图之一,但我们所讲的特性已经足够展示。

左边插入,bf--;右边插入,bf++。我们发现当bf等于0时,对树的影响终止,当bf等于1或-1时,影响会继续向上扩散。那么为什么没有bf==2||bf==-2呢?因为如果发生这种状况,说明树有了问题,需要进行处理(在后面的非健康版讲解)。

根据这个逻辑我们先来实现一段代码。

bool insert(const pair<K, V>& kv)
{if (_root == nullptr)//如果树为空,建立根节点_root = new Node(kv);else//插入{Node* cur = _root,*parent=nullptr;//虽然是三叉链结构,但这里的parent仍需要,因为我们找到的位置是nullptrwhile (cur)//找到需要插入位置的父亲{if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}	else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}	elsereturn false;}cur = new Node(kv);if (kv.first < parent->_kv.first)//判断插入的位置是父亲的左还是右{parent->_left = cur;parent->_bf--;}	else{parent->_right = cur;parent->_bf++;}while (parent)//进行bf的处理{if (parent->_bf == 0)//如果父亲bf==0,终止影响break;else if (parent->_bf == 1 || parent->_bf == -1)//父bf 1 or-1{//继续向上影响,更新父子节点的位置cur = parent;parent = cur->_parent;}else if (parent->_bf == 2 || parent->_bf == -2)//需旋转{//树结构已经发生问题,不满足AVL树的特性,需要进行处理} }}return true;
}

1.4 AVL树的旋转(非健康版)

前面的插入是插入后树中节点bf绝对值都不大于1的情况,那么如果有节点的bf绝对值大于1怎么办呢?

这时就需要对其进行处理,所谓处理就是将树的部分进行旋转,使其满足AVL树的特性。

我们来看看。

这里是右旋转。可以看到,右旋转的发生是当cur->bf==-1&&parent->bf==-2时。

1. 右旋图解

右旋代码 

void RotateR(Node* parent)//右单旋
{//记录会进行迁移的节点Node* cur = parent->left;Node* CR = cur->_right;parent->_left = CR;if (CR)CR->_parent = parent;cur->_right = parent;Node* grandfather = parent->_parent;//记录parent的父亲parent->_parent = cur;if (parent == _root)//parent为根{_root = cur;cur->_parent = nullptr;}else//不为根{if (parent == grandfather->_left)//判断parent的位置grandfather->_left = cur;elsegrandfather->_right = cur;cur->parent=grandfather;}cur->_bf = 0;//更新bfparent->_bf = 0;}

 2. 左旋图解

这里是左旋转,左旋转的发生是当cur->bf==1&&parent->bf==2时的。

左旋代码 

void RotateL(Node* parent)//左单旋{//记录会进行迁移的节点Node* cur = parent->_right;//cur节点Node* CL = cur->_left;//cur的左孩子parent->_right = CL;//cur->_left变成parent->_rightif (CL)//如果CL存在,更新其父亲,以免触发空指针的解引用CL->_parent = parent;cur->_left = parent;//parent给cur->_leftNode* grandfather = parent->_parent;//先记录parent的父亲parent->_parent = cur;//更新parent的父亲if (parent == _root)//parent为根,将cur置为_root,其父亲为空{_root = cur;cur->_parent = nullptr;}else//不为根,更新其父亲{if (parent == grandfather->_left)grandfather->_left = cur;elsegrandfather->_right = cur;cur->parent=grandfather;}cur->_bf = 0;parent->_bf = 0;}

难道就仅限于此了吗?当然不是,我们接着看。

当类似以下结构就需要双旋转了,即parent->bf绝对值为2,cur->bf绝对值为1,且二者一正一负时就需要双旋转。

下图为parent==2&&cur->bf==-1,需要右左双旋。

也就是说当parent==-2&&cur->bf==1需要左右双旋。

3. 右左双旋图解

cur->left->bf==1时

而图中cl即cur->left的bf不同,会产生不同的效果,因为cur->left的子树最终会分别成为parent与cur的子树 .

cur->left->bf==1时

右左双旋代码 

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

4. 左右双旋图解

cur->right->bf==1时

cur->right->bf==-1时 

左右双旋代码
void RotateLR(Node* parent)//左右双旋
{Node* cur = parent->_left;Node* CR = cur->_right;int bf = CR->_bf;RotateL(cur);RotateR(parent);CR->bf = 0;if (bf == -1){cur->_bf = 0;parent->bf = 1;}else if (bf == 1){cur->_bf = -1;parent->_bf = 0;}else{cur->_bf = 0;parent->_bf = 0;}

总结 

假如以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为根的子树个高度降低,已经平衡,不需要再向上更新。
 

1.5 AVL树的验证 

1.5.1 二叉搜索树的验证

只要中序遍历可以得到一个有序序列,就说明是二叉搜索树。

1.5.2 AVL树的验证 

AVL树的验证:

每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
节点的平衡因子是否计算正确

验证代码
bool _IsBanlance(Node* cur, int& height)
{if (cur == nullptr){return true;height = 0;}int leftheight = 0, rightheight = 0;//记录每棵树的左树与右树高度if (!_IsBanlance(cur->_left, leftheight) || !_IsBanlance(cur->_right, rightheight))//有任何一棵子树不满足条件return false;int SubHeight = rightheight - leftheight;//每个节点其下树的高度差都是他的平衡因子if (abs(SubHeight) > 1)//高度差{cout << cur->_kv.first <<":"<<cur->_bf<< endl;cout << "高度差错误,不平衡" << endl;return false;}height = leftheight > rightheight ? leftheight + 1 : rightheight + 1;//更新高度return true;
}

1.6 AVL树的删除 

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置

1.7 AVL树的性能

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

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

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

相关文章

k8s 二进制安装 优化架构之 部署负载均衡,加入master02

目录 一 实验环境 二 部署 CoreDNS 1&#xff0c;所有node加载coredns.tar 镜像 2&#xff0c;在 master01 节点部署 CoreDNS 3&#xff0c; DNS 解析测试 4&#xff0c; 报错分析 5&#xff0c;重新 DNS 解析测试 三 master02 节点部署 1&#xff0…

什么是最大路径?什么是极大路径?

最近学习中&#xff0c;在这两个概念上出现了混淆&#xff0c;导致了一些误解&#xff0c;在此厘清。 最大路径 在一个简单图G中&#xff0c;u、v之间的距离 d ( u , v ) min ⁡ { u 到 v 的最短路的长度 } d(u,v) \min \{ u到v的最短路的长度 \} d(u,v)min{u到v的最短路的…

Redis 的主从复制

Redis 的主从复制 1、主从复制的实现2、主从复制的同步功能(PSYNC)2.1、部分重同步 本文讲解的Redis 主从复制机制&#xff0c;是基于 2.8及以后的版本而言&#xff0c;2.8以前的版本主从复制机制与此有所不同&#xff0c;请知悉。 Redis的复制功能分为 同步 (psync) 和 命令传…

vm16安装最新版本的ubuntu虚拟机,并安装g++的步骤记录

背景 低版本的ubuntu安装G一直不成功&#xff0c;干脆安装最新版的 官网下载 bing搜索ubuntu 下载完成 vm16新建虚拟机 一直下一步&#xff0c;安装完成 终端输入命令 sudo apt-get update ᅟᅠ       sudo apt install gcc ᅟᅠ      sudo apt install g

【C/C++】设计模式——工厂模式:简单工厂、工厂方法、抽象工厂

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

cdn引入vue的项目嵌入vue组件——http-vue-loader 的使用——技能提升

最近在写MVC的后台&#xff0c;看到全是jq的写法&#xff0c;但是对于用惯了vue的我&#xff0c;真是让我无从下手。。。 vue的双向绑定真的很好用。。。 为了能够在cdn引入的项目中嵌入vue组件&#xff0c;则可以使用http-vue-loader了 步骤1&#xff1a;下载http-vue-loader…

OPC-UA open62541 C++测试代码

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 这是之前写的open62541测试代码…

数据结构------二叉树经典习题1

博主主页: 码农派大星. 关注博主带你了解更多数据结构知识 1判断相同的树 OJ链接 这道题相对简单,运用我们常规的递归写法就能轻松写出 所以我们解题思路应该这样想: 1.如果p为空&#xff0c;q为空&#xff0c;那么就是两颗空树肯定相等 2.如果一个树为空另一棵树不为空那么…

ROS学习笔记(15)小车巡墙驾驶

0.前提 前一章我讲解了拉氏变换和PID&#xff0c;这一章我来讲解一下小车巡墙驾驶的理论和部分代码。 1.前情回顾 1.拉氏变换 拉普拉斯变换是要将时域问题转换成频域问题来处理。 2.PID控制器 转向角&#xff1a; 误差牺牲&#xff1a; 3.具体参看上一篇文章 2.巡墙驾驶…

游戏理解入门:Rust+Bracket开发一个小游戏

1. Game loop 使用game loop可以使得游戏运行更加流畅和顺滑&#xff0c;它可以&#xff1a; 初始化窗口、图形和其他资源&#xff1b;每当屏幕刷新他都会运行(通常是每秒30,60 )&#xff1b;每次通过循环&#xff0c;他都会调用游戏的tick()函数。 大致的原理流程如下&…

STK中的光照计算模型

本文简要阐述STK中光照计算的模型。 在航天任务中&#xff0c;通常需要分析地面站、飞行器在一定时间内的光照情况&#xff0c;具体包括&#xff1a; 地面站处在光照区和阴影区的具体时间范围&#xff1b;考虑地形遮挡后&#xff0c;地面站的光照区和阴影区的变化情况&#x…

MYSQL-9.问题排查

问题排查的思路与方向 问题排查思路 分析问题&#xff1a;根据理论知识经验分析问题&#xff0c;判断问题可能出现的位置或可能引起问题的原因&#xff0c;将目标缩小到一定范围&#xff1b;排查问题&#xff1a;基于上一步的结果&#xff0c;从引发问题的“可疑性”角度出发…

低空经济:无人机竞赛详解

无人机竞赛市场近年来呈现出蓬勃发展的态势&#xff0c;其市场价值不仅体现在竞赛本身&#xff0c;还体现在推动无人机技术创新、拓展应用场景以及促进产业链发展等多个方面。 一、比赛项目介绍 无人机竞赛通常分为多个项目&#xff0c;包括竞速赛、技巧赛、航拍赛等。每个项目…

新手也能看懂的前端单元测试框架:Vitest

单元测试的概念及作用 1.什么是单元测试&#xff1f; 单元测试是测试中的一个重要环节&#xff0c;它针对软件中的最小可测试单元进行验证&#xff0c;通常是指对代码中的单个函数、方法或模块进行测试。 单元测试旨在确定特定部分代码的行为是否符合预期&#xff0c;通过针…

【谷粒商城】01-环境准备

1.下载和安装VirtualBox 地址&#xff1a;https://www.virtualbox.org/wiki/Downloads 傻瓜式安装VirtualBox 2.下载和安装Vagrant官方镜像 地址&#xff1a;https://app.vagrantup.com/boxes/search 傻瓜式安装 验证是否安装成功 打开CMD,输入vagrant命令&#xff0c;是否…

Linux的常用指令 和 基础知识穿插巩固(巩固知识必看)

目录 前言 ls ls 扩展知识 ls -l ls -a ls -al cd cd 目录名 cd .. cd ~ cd - pwd 扩展知识 路径 / cp [选项] “源文件名” “目标文件名” mv [选项] “源文件名” “目标文件名” rm 作用 用法 ./"可执行程序名" mkdir rmdir touch m…

单位个人怎样向报社的报纸投稿?

作为一名单位的信息宣传员,我肩负着每月定期在媒体上投稿发表文章的重任。然而,在投稿的道路上,我经历了不少波折和挫折。 一开始,我天真地以为只要将稿件发送到报社的投稿邮箱,就能轻松完成任务。然而,现实却远比我想象的复杂。邮箱投稿的竞争异常激烈,编辑们会在众多稿件中挑…

什么是直接内存(NIO)

直接内存不受 JVM 内存回收管理&#xff0c;是虚拟机的系统内存&#xff0c;常见于 NIO 操作时&#xff0c;用于数据 缓冲区&#xff0c;分配回收成本较高&#xff0c;但读写性能高&#xff0c;不受 JVM 内存回收管理。 举例 当上传一个较大文件&#xff08;200M&#xff09;…

【传知代码】VRT: 关于视频修复的模型(论文复现)

前言&#xff1a;随着数字媒体技术的普及&#xff0c;制作和传播视频内容变得日益普遍。但是&#xff0c;视频中由于多种因素&#xff0c;例如传输、存储和录制设备等&#xff0c;经常出现质量上的问题&#xff0c;如图像模糊、噪声干扰和低清晰度等。这类问题对用户的体验和观…

【Ubuntu20.04安装java-8-openjdk】

1 下载 官网下载链接&#xff1a; https://www.oracle.com/java/technologies/downloads/#java8 下载 最后一行 jdk-8u411-linux-x64.tar.gz&#xff0c;并解压&#xff1a; tar -zxvf jdk-8u411-linux-x64.tar.gz2 环境配置 1、打开~/.bashrc文件 sudo gedit ~/.bashrc2、…