平衡二叉搜索树之 AVL 树的模拟实现【C++】

平衡二叉搜索树之 AVL 树的模拟实现【C++】

文章目录

AVL树的简单介绍

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

AVL树就可以解决上述问题,让搜索树的查找效率在任何情况下都能稳定是O(logN)

AVL树解决上述问题的方法是:
保证每个结点的左右子树高度之差的绝对值不超过1
这样就能保证树中的节点分布接近满二叉树,高度非常接近logN【N为树中节点的个数】,进而让一次查找的效率为O(logN)

为什么是保证每个结点的左右子树高度之差的绝对值不超过1,而不是保证左右子树高度一样呢?
其实很简单:
因为在一些情况下绝对不可能做到左右子树高度一样,例如:

在这里插入图片描述


此时无论如何改变树的形态,都不可能做到每个结点的左右子树高度相等


综上:
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树

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

全部的实现代码放在了文章末尾

准备工作

创建两个文件,一个头文件AVLTree.hpp,一个源文件test.cpp

【因为模板的声明和定义不能分处于不同的文件中,所以把成员函数的声明和定义放在了同一个文件AVLTree.hpp中】

  1. AVLTee.hpp:存放包含的头文件,命名空间的定义,成员函数和命名空间中的函数的定义
  2. test.cpp:存放main函数,以及测试代码

包含头文件

  • iostream:用于输入输出
  • cmath:提供数学函数

类的成员变量

实现AVL树最主要的就是保证树中节点的左右子树高度差不超过1

而维护这一条件的方法并不是唯一的,我使用的是平衡因子来维护

平衡因子时每个节点都有的,它的值就是这个节点的左右子树高度之差【一般是右子树高度-左子树高度

在这里插入图片描述

构造函数和拷贝构造

构造函数没什么好说的,默认构造就行了

AVLTree():_root(nullptr){ }

拷贝构造:
因为节点都是从堆区new出来的,所以要深拷贝

使用递归实现深拷贝:
因为拷贝构造不能有多余的参数,但是递归函数又必须使用参数记录信息

在这里插入图片描述

然后在拷贝构造里面调用一下这个函数就行了

AVLTree(const AVLTree& obj){  根节点的父亲节点是nullptr _root =Copy(obj._root,nullptr);}

swap和赋值运算符重载

交换两颗二叉搜索树的本质就是交换两颗数的资源(数据),而它们的资源都是从堆区申请来的,然后用指针指向这些资源

在这里插入图片描述

并不是把资源存储在了二叉搜索树对象中

所以资源交换很简单,直接交换_root指针的指向即可

voidSwap(AVLTree& obj){  std::swap(_root, obj._root);}

赋值运算符重载

AVLTree&operator=(AVLTree obj){ this->Swap(obj);return*this;}

为什么上面的两句代码就可以完成深拷贝呢?
这是因为:

使用了传值传参,会在传参之前调用拷贝构造,再把拷贝构造出的临时对象作为参数传递进去

赋值运算符的左操作数,*this再与传入的临时对象obj交换,就直接完成了拷贝

在函数结束之后,存储在栈区的obj再函数结束之后,obj生命周期结束

obj调用析构函数,把指向的从*this那里交换来的不需要的空间销毁


析构函数

使用递归遍历,把所有从堆区申请的节点都释放掉:
因为析构函数不能有多余的参数,但是递归函数又必须使用参数记录信息
所以再封装一个成员函数,专门用来递归释放:

在这里插入图片描述

然后在拷贝构造里面调用一下这个函数就行了

~AVLTree(){ Destroy(_root); _root =nullptr;}

find

具体流程:
从根节点开始,将目标值(传入的key)与当前节点的key进行比较。
如果目标值小于当前节点值,则在左子树中继续查找;
如果目标值大于当前节点值,则在右子树中继续查找。

这个过程一直进行,直到找到与目标值或者到达空节点为止。

把上述过程转成代码:

在这里插入图片描述

insert[重要]

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

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

  1. 先按照二叉搜索树的方式插入新节点
  2. 再调整节点的平衡因子

因为新节点一定是插在叶子节点或者只有一个孩子节点的节点
所以插入节点后一定会影响新节点的父亲节点的平衡因子,可能会影响祖先节点

插入节点后具体分以下3种情况:

  1. 插入节点的父亲节点(以下简称parent)的平衡因子等于2/-2此时parent的左右子树高度差已经超过1,已经违反了AVL树的规则需要旋转进行处理

插入节点的父亲节点(以下简称parent)的平衡因子等于1/-1
那么就说明插入之前parent的平衡因子一定是0【如果插入前是2/-2的话,一定早就被旋转了】
所以parent在插入之前是叶子节点
即以parent为根的子树插入之前的高度就是1,插入之后高度就变成了2
所以插入前后以parent为根的子树的高度增加了,自然就影响parent的祖先的平衡因子

在这里插入图片描述

所以需要再向上继续更新祖先节点的平衡因子

插入节点的父亲节点(以下简称parent)的平衡因子等于0
那么就说明插入之前parent的平衡因子一定是1/-1
所以parent在插入之前是只有一个孩子节点的节点
即以parent为根的子树插入之前的高度就是2,插入之后高度还是2
所以插入前后以parent为根的子树的高度没有改变,自然就不会影响parent的祖先的平衡因子

在这里插入图片描述

所以不需要再向上取跟新祖先节点的平衡因子
直接插入结束

在这里插入图片描述

当parent的平衡因子为1/-1时,如何向上更新祖先节点的平衡因子呢?

其实也简单:
就是把原来的parent当做新的cur,把parent的父节点作为新的parent

再判断新的cur是父亲节点的左还是右,据此再更新新的parent的平衡因子

cur = parent; parent = parent->_parent; 因为以之前的parent为根的子树,高度增加了1 右因为平衡因子=右子树高度-左子树高度 所以: if(cur==parent->_left){  parent->_bf--;}else{  parent->_bf++;}

然后,再重复判断一下新的parent的平衡因子的3种情况,进行处理

  1. 新的parent的平衡因子变成了0,插入就结束
  2. 新的parent的平衡因子变成了1/-1,就再重复这个过程,继续向上更新祖先节点的平衡因子
  3. 新的parent的平衡因子变成了2/-2,就旋转

怎么旋转?

旋转分以下4种:

左单旋

所有的需要左单旋的情况,都可以画成下图的抽象图

在这里插入图片描述

具体情况描述:

  1. 插入前paernt的平衡因子为1,并且它的右边(PR)的平衡因子为0
  2. 新插入的节点插在子树c上,并使子树c的高度增加1
  3. 插入后并向上跟新后:paernt的平衡因子变成2,并且它的右边(PR)的平衡因子变成了1

此时就使用左单旋:
把(下图中的)PRL链接在parent的右子树上,再把parent连接在PR的左子树上
把PR作为这颗子树新的根

这样就可以做到:在不违反搜索树规则【所有左子树上的值<根<右子树】的基础上,尽可能地让树平衡

将上述过程转换成代码:

在这里插入图片描述

右单旋

所有的需要右单旋的情况,都可以画成下图的抽象图

在这里插入图片描述


具体情况描述:

  1. 插入前paernt的平衡因子为-1,并且它的左边(PL)的平衡因子为0
  2. 新插入的节点插在子树a上,并使子树a的高度增加1
  3. 插入后并向上跟新后:paernt的平衡因子变成-2,并且它的左边(PL)的平衡因子变成了-1

此时就使用左单旋:
把(下图中的)PLR链接在parent的左子树上,再把parent连接在PL的右子树上
把PL作为这颗子树新的根

这样就可以做到:在不违反搜索树规则【所有左子树上的值<根<右子树】的基础上,尽可能地让树平衡

将上述过程转换成代码:

在这里插入图片描述

左右双旋

所有的需要左右双旋的情况,都可以画成下图的抽象图

在这里插入图片描述

具体情况描述:

  1. 插入前paernt的平衡因子为-1,并且它的左边(PL)的平衡因子为0
  2. 新插入的节点插在子树b或者c上,并使子树b或者c的高度增加1
  3. 插入后并向上跟新后:paernt的平衡因子变成-2,并且它的左边(PL)的平衡因子变成了1

此时使用一次单旋,是解决不了的,旋了之后还是有平衡因子为2/-2的节点

但是如果我们对PL进行左单旋之后,就可以发现可以对parent使用右旋来使树,在不违反搜索树规则【所有左子树上的值<根<右子树】的基础上,尽可能地接近平衡

RotateL(parent->_left);RotateR(parent);

右左双旋

所有的需要右左双旋的情况,都可以画成下图的抽象图

在这里插入图片描述

具体情况描述:

  1. 插入前paernt的平衡因子为1,并且它的右边(PR)的平衡因子为0
  2. 新插入的节点插在子树b或者c上,并使子树b或者c的高度增加1
  3. 插入后并向上跟新后:paernt的平衡因子变成2,并且它的右边(PR)的平衡因子变成了-1

此时使用一次单旋,是解决不了的,旋了之后还是有平衡因子为2/-2的节点

但是如果我们对PR进行右单旋之后,就可以发现可以对parent使用左单旋来使树,在不违反搜索树规则【所有左子树上的值<根<右子树】的基础上,尽可能地接近平衡

RotateR(parent->_right);RotateL(parent);

旋转的平衡因子更新

左单旋和右单旋

因为插入的情况都只有一种,所以平衡因子的更新很简单,看上面画的示意图就行


parentPR(PL)的平衡因子最后都变成0,其他节点的平衡因子没有变化


左右双旋和右左双旋

因为左右双旋和右左双旋的新节点既可以插在子树b上,又可以插在子树c上
而插在这两颗子树上的平衡因子更新是不同的

下图是左右双旋新节点插在子树b上的
最后:parent的平衡因子=1,PL的平衡因子=0,PLR的平衡因子=0

在这里插入图片描述


下图是左右双旋新节点插在子树c上的
最后:parent的平衡因子=0,PL的平衡因子=-1,PLR的平衡因子=0

在这里插入图片描述

而且当h=0时,还有一种情况:
下图是左右双旋的h=0的旋转图
最后:parent的平衡因子=0,PL的平衡因子=0,PLR的平衡因子=0

在这里插入图片描述

综上:
左右双旋代码

在这里插入图片描述

右左双旋同理

在这里插入图片描述

insert的全部代码

boolInsert(const T& data){ if(_root ==nullptr) 树为空,则直接新增节点 {  赋值给二叉搜索树的成员变量`_root`指针 _root =newNode(data);returntrue; 返回true,代表插入成功 } Node* cur = _root; 从根节点开始 定义parent来保存cur的父亲节点 假设根节点的父亲节点为nullptr Node* parent =nullptr;while(cur){ 

Read more

时序数据库选型指南:用工程视角理解 Apache IoTDB

时序数据库选型指南:用工程视角理解 Apache IoTDB

摘要:在工业物联网(IIoT)数据爆发式增长的今天,通用数据库已难以应对海量测点的高频写入与复杂聚合查询。本文将从工程落地的角度出发,探讨时序数据库(TSDB)的选型关键维度,并深入解析 Apache IoTDB 在架构设计、数据模型及端边云协同方面的技术特性。 文章目录 * 一、 引言:为什么我们需要专用的时序数据库? * 二、 选型核心维度与 IoTDB 的设计哲学 * 2.1 数据模型:树形结构 vs 标签模型 * 2.2 存储引擎:LSM Tree 与 TsFile 的深度优化 * 核心技术拆解 * 架构流程图:IoTDB 写入与压缩流程 * 2.3 分布式架构:MPP 与 共识协议 * 三、 实战演练:从定义到分析 * 3.

By Ne0inhk

告别VNC!Ubuntu 22.04原生RDP远程桌面配置全攻略(含高分屏适配技巧)

告别VNC!Ubuntu 22.04原生RDP远程桌面配置全攻略(含高分屏适配技巧) 如果你和我一样,长期在Windows和Linux双系统之间切换,或者需要远程管理一台Ubuntu桌面服务器,那么“远程桌面”这个需求一定不陌生。过去,我们通常会选择VNC方案,比如TigerVNC、RealVNC,但体验过的人都知道,VNC在跨平台、网络带宽占用、尤其是高分屏支持上,总是差那么点意思——画面卡顿、色彩失真、缩放模糊,这些问题在需要精细操作的设计或开发工作中尤为恼人。 好消息是,从Ubuntu 22.04 LTS “Jammy Jellyfish”开始,GNOME桌面环境正式集成了对微软RDP(Remote Desktop Protocol) 协议的原生支持。这意味着,你现在可以直接使用Windows系统自带的“远程桌面连接”(mstsc)或macOS上的Microsoft Remote Desktop,像连接另一台Windows电脑一样,无缝接入你的Ubuntu桌面。这不仅仅是换了个协议那么简单,它带来的是更低的延迟、更好的图形压缩效率、原生剪贴板共享、

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(自旋锁实现 LED 设备互斥访问)--- Ubuntu20.04自旋锁实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(自旋锁实现 LED 设备互斥访问)--- Ubuntu20.04自旋锁实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言 一、实验基础说明 1.1、自旋锁简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 自旋锁 LED 驱动代码(spinlock.c) 3.2、驱动代码分段解析 3.2.

By Ne0inhk
分享个人制作的Openclaw 2026.3.7 Docker离线部署方案

分享个人制作的Openclaw 2026.3.7 Docker离线部署方案

分享个人制作的Openclaw 2026.3.7 Docker离线部署方案 文档编辑时间:2026-3-8 1、下载镜像 个人分享的镜像,保证无毒无木马,基于node:22-bookworm镜像制作。 网盘地址: https://pan.baidu.com/s/1RqyskudGPxCPdpxvCQ7mzQ?pwd=c1us 提取码: c1us 2、导入镜像 docker load --input openclaw-2026.3.7.images 3、修改配置 在linux服务器/home/openclaw/docker/default/目录下面创建一个.openclaw文件夹,在里面创建openclaw.json文件,当然这个目录你可以自己指定,内容如下: {"meta":{"

By Ne0inhk