【C++】详解AVL树并模拟实现

news/2024/11/17 1:27:32/

  前言:

  •   之前我们为了让数据存储效率提高,引进了二叉搜索树。
  •   但是我们发现,二叉搜索树的时间复杂度还是O(N),因为二叉搜索树并不是非常的平衡。
  •   并不是所有树都是满二叉树,可能出现单边书这样极端的情况,所以我们引进了查找效率更高的AVL树

目录

(一)AVL树的概念

(二)AVL树的模拟实现

(1)AVL树结点的定义

(2)AVL树部分功能的实现

1、查找

2、插入(重点!)

2.1插入结点后平衡因子的变化

2.2情况分析

2.3旋转操作的实现

2.3.1左单旋

2.3.2右单旋

 2.3.3左右双旋

2.3.4右左双旋

(三) 验证AVL树

1、中序遍历

2、判断树是否平衡

(四)总代码详解

 



(一)AVL树的概念

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

  • 它的左右子树都是AVL树
  • 任何一颗子树左右子树高度之差(简称平衡因子)的绝对值不超过 1(-1 / 0 / 1)

图示:

图中结点外数字代表平衡因子→右子树高度-左子树高度

AVL树又叫高度平衡二叉搜索树。

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


(二)AVL树的模拟实现

(1)AVL树结点的定义

~直接实现key_value的结构 – 三叉链的形式(带父节点)

图示如下: 

其中:

_bf-----balance factor,代表平衡因子

右子树与左子树的高度差


(2)AVL树部分功能的实现

1、查找

和二叉搜索树查找功能的实现几乎一致:

从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
最多查找高度次,走到到空,还没找到,这个值不存在。

2、插入(重点!)

  • 这里的插入思路和二叉搜索树中插入的思路一致,找到合适的位置之后再链接

这里链接是比较容易的,但是链接之后对各个结点中的平衡因子的调整则是比较费劲。

所以重点就在于分析各种可能出现平衡因子的情况,然后调整树来解决。

2.1插入结点后平衡因子的变化

1.首先我们考察插入结点和它父亲结点之间的关系变化:

  • 当一个结点的左或者右链接了一个结点,该结点为链接结点的父节点
  • 当该父节点的右边连接上孩子时,此时该父节点的右子树比左子树高了一层,平衡因子_bf + +
  • 当该父节点的左边连接上孩子时,此时该父节点的左子树比右子树高了一层,平衡因子_bf – –


2.然后我们再以此为基础考察其他结点的变化情况:

我们以上图为例,8结点作为父亲结点,平衡因子发生了变化,这就有可能继续导致8结点的父亲结点的平衡因子发生变化。

简而言之,插入一个结点真正会影响的是其祖先的平衡因子的改变。


3.处理方法

(1)向上更新:

  • 更新新插入节点祖先的平衡因子
  • 没有违反规则就结束了,违反规则,不平衡了就需要处理
  • 这里的处理是旋转处理(接下来会重点介绍)
  • 在更新的过程中只要是发现违反了AVL树规则的就需要旋转处理

(2)如何向上更新:

  • 更新的方式是沿着祖先路径更新(回溯)
  • 将parent结点更新到它的_parent位置上,将cur结点更新到它的_parent位置上
  • 在这个过程中一旦发现有违反AVL树规则的时即parent的平衡因子变成2或-2
  • 这时就需要进行旋转处理

具体过程如下:

  • 子树高度变了,就要继续往上更新
  • 子树的高度不变, 则更新完成
  • 子树违反平衡规则,则停止更新, 旋转子树

2.2情况分析

1.父亲结点平衡因子为0时,符合规则,break跳出:

2.父亲结点平衡因子是1或者-1时继续向上更新:

3.更新过程中,父亲结点平衡因子出现了2或者-2,说明子树不平衡了,需要上述说到的旋转处理(下文中会详解旋转的几种情况)

看了如上的代码有点抽象,我们下面用具体例子来讲解此情况下不同的情景:

我们也可以明晰的看出上述代码有四个条件语句,每个if其实代表的就是一种情景,我们详细分析:

情景一:

这时我们发现8作为父亲结点时出现了平衡因子为2的情况,此时cur结点平衡因子为1


 


情景二:

这时我们发现8作为父亲结点时出现了平衡因子为2的情况,此时cur结点平衡因子为-1


情景三,情景四大家可以自己画图啦!其实就是cur所在子树变为parent左子树的一些情况,下面详解旋转操作中将会对这几种情景解释并解决!

4.一定要检查 -- 不保证其他地方不会出现错误
            
    比如插入之前AVL数就存在平衡子树,|平衡因子| >= 2结点。


2.3旋转操作的实现

上述我们已经阐述了,在什么情况下需要对AVL树进行旋转操,接下来我们就来讲一下具体的旋转步骤。

旋转原则:

  • 保持搜索树的规则
  • 子树变平衡

旋转一共分为四种旋转方式:

  •  左单旋、右单旋
  •  左右双旋、右左双旋

2.3.1左单旋

当右子树高的时候,这时就要向左旋转。

旋转过程:

  • 将要旋转的子树的根节点设为parent,根结点的右子树为subR,subR的左节点为subRL
  • 将subRL给parent的右,再将parent给subR的左
  • 改变其链接关系即可
  • 这样一来subR做了子树的根,根结点的左右子树高度差从2变成了0

图示:

代码图解:(后续代码统一给)

2.3.2右单旋

 有左单旋的基础,我们知道右单旋的情况:
当左子树高的时候,这时就要向右旋转。

旋转过程:

  • 将要旋转的子树的根节点设为parent,根结点的左子树为subL,subL的右节点为subLR
  • 将subLR给parent的左,再将parent给subL的右
  • 改变其链接关系即可
  • 这样一来subL做了子树的根,根结点的左右子树高度差从2变成了0

图示:

代码图解:

 2.3.3左右双旋

在一些情况中,左单旋和右单旋是无法解决问题的,需要二次旋转来降低子树高度,

比如下面的情况:

这时候无论怎么单旋,我们都无法降低子树的高度。

所以这里我们要用到双旋。

图示:

这样一来,我们就把单次旋转无法降低高度的情况解决了!

详细过程可以跟着图示来理解。

代码图解: 

这里主要是根据subLR平衡因子的不同情况来给降低高度后子树中不同节点的平衡因子赋值,大家可以模仿上面的图示把不同情况画下来!

  •  我们在实现双旋的时候可以复用单旋
  •  但是单旋有个坑,会出现将平衡因子搞成0的情况

两种解决方案:

  • 将单旋中更新的平衡因子拿出来
  • 旋转之前将位置记录下来

我们采用第一种方法,单独将平衡因子拿出来处理。

2.3.4右左双旋

和上面的左右双旋的情况类似:

图示:

代码图解:
 


(三) 验证AVL树

上面我们基本完成了AVL树的两大重要的功能实现(删除操作有困难,本文暂不实现,后续深入会讨论),下面我们要验证我们写的树是不是AVL树。

主要验证两大方面:

  • 是不是二叉搜索树(AVL树是特殊的二叉搜索树,中序遍历应该是有顺序的)
  • 子树的高度差是不是符合条件(-2<右子树高度-左子树高度<2)

1、中序遍历

介绍前,我们先说明一点:

  • 我们平时调用类中的成员函数一般是对象.成员函数
  • 比如一个对象t中序历遍就是t.InOrder()
  • 这样符合库中其他类成员函数的使用习惯
  • 但是我们要想中序历遍必须传根节点
  • 所以我们叠加一层嵌套,巧妙解决这个问题:

多套一层,这样就可以符合我们使用这些接口函数的习惯了!

其实通过上面的讲解后,中序遍历作者已经给出啦:
我们选择和之前中序历遍搜索二叉树一样的方法,采用递归的方式解决

2、判断树是否平衡

我们一同思考,如何判断左右子树高度差在2以内呢?

  • 首先,我们必须知道左右子树的高度
  • 其次,我们再判断高度差是否是2以内

求树的高度:

其实我们之前也讲解过,采用递归的方法更容易来求解。

思路:树的高度就是左子树或者右子树高的那一颗树的高度加上根节点高度(也就是+1)

判断树是否平衡:

我们求解左右子树的高度后记录下来,相减的绝对值看是否是二以内。

当然,对于树中每一个结点我们都要判断他的子树是否符合条件,所以也要用到递归:

(四)总代码详解


#include<iostream>
#include<assert.h>
#include<algorithm>
using namespace std;
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){}
};template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K, V> Node;
public: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->_right){parent->_bf++;}else{parent->_bf--;}if (parent->_bf == 1 || parent->_bf == -1){parent = parent->_parent;cur = cur->_parent;}else if (parent->_bf == 0){break;}else if (parent->_bf == 2 || parent->_bf == -2){// 需要旋转处理 -- 1、让这颗子树平衡 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)//左右双旋{RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1)//右左双旋{RotateRL(parent);}//旋转完之后ppNode为根的子树高度不变 -- 所以对ppNode的平衡因子没有影响break;}else // 一定要检查 -- 不保证其他地方不会出现错误{//插入之前AVL数就存在平衡子树,|平衡因子| >= 2结点assert(false);}}return true;}void InOrder(){_InOrder(_root);cout << endl;}bool IsBalance(){return _IsBalance(_root);}int Height(){return _Height(_root);}
private:bool _IsBalance(Node* root){if (root == nullptr)return true;int HeightL = _Height(root->_left);int HeightR = _Height(root->_right);if (HeightR - HeightL != root->_bf){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return abs(HeightL - HeightR) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);空树也是AVL树//if (nullptr == root)//	return true;计算pRoot节点的平衡因子:即pRoot左右子树的高度差//int leftHeight = _Height(root->_left);//int rightHeight = _Height(root->_right);求差值//int diff = rightHeight - leftHeight;如果计算出的平衡因子与pRoot的平衡因子不相等,或者pRoot平衡因子的绝对值超过1,则一定不是AVL树//if (abs(diff) >= 2)//{//	cout << root->_kv.first << "结点平衡因子异常" << endl;//	return false;//}平衡因子没有异常但是和结点的对不上//if (diff != root->_bf)//{//	//说明更新有问题//	cout << root->_kv.first << "结点平衡因子不符合实际" << endl;//	return false;//}pRoot的左和右如果都是AVL树,则该树一定是AVL树把自己和自己的左右子树都检查了,递归检查//return _IsBalance(root->_left)//	&& _IsBalance(root->_right);}int _Height(Node* root){if (root == NULL)return 0;int leftH = _Height(root->_left);int rightH = _Height(root->_right);return leftH > rightH ? leftH + 1 : rightH + 1;}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}//左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL){subRL->_parent = parent;}Node* ppnode = parent->_parent;subR->_left = parent;parent->_parent = subR;if (ppnode == nullptr){_root = subR;_root->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subR;}else{ppnode->_right = subR;}subR->_parent = ppnode;}parent->_bf = subR->_bf = 0;}//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* ppnode = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root){_root = subL;_root->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = subL;}else{ppnode->_right = subL;}subL->_parent = ppnode;}parent->_bf = subL->_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 == 1){parent->_bf = 0;subLR->_bf = 0;subL->_bf = -1;}else if (bf == -1){parent->_bf = 1;subLR->_bf = 0;subL->_bf = 0;}else if (bf == 0){parent->_bf = 0;subLR->_bf = 0;subL->_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 == 1){parent->_bf = -1;subRL->_bf = 0;subR->_bf = 0;}else if (bf == -1){parent->_bf = 0;subRL->_bf = 0;subR->_bf = 1;}else if (bf == 0){parent->_bf = 0;subRL->_bf = 0;subR->_bf = 0;}else{assert(false);}}private:Node* _root = nullptr;
};void Test_AVLTree1()
{//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };AVLTree<int, int> t1;for (auto e : a){/*	if (e == 14){int x = 0;}*/t1.Insert(make_pair(e, e));cout << e << "插入:" << t1.IsBalance() << endl;}t1.InOrder();cout << t1.IsBalance() << endl;
}void Test_AVLTree2()
{srand(time(0));const size_t N = 10;AVLTree<int, int> t;for (size_t i = 0; i < N; ++i){size_t x = rand() + i;t.Insert(make_pair(x, x));//cout << t.IsBalance() << endl;}t.InOrder();cout << t.IsBalance() << endl;cout << t.Height() << endl;
}


http://www.ppmy.cn/news/1094356.html

相关文章

后流量时代的跨境风口:Facebook广告

Facebook拥有超过25亿各个年龄段和人群的每月活跃用户&#xff0c;可以帮助您接触世界各地的相关消费者。无论您是需要吸引新的潜在客户还是吸引回头客访问您的在线商店&#xff0c;Facebook广告都可以为电子商务提供丰厚的投资回报&#xff1b;无论您是在沃尔玛、eBay、亚马逊…

【快应用】二级页面如何携带参数返回一级页面?

【关键词】 快应用、router、onBackPress 【问题背景】 页面栈有A、B两个页面&#xff0c;怎么在B页面中调A页面的方法或传参&#xff1f;场景是&#xff1a;A页面是列表页&#xff0c;B页面是详情页&#xff0c;B页面状态改变后返回A页面状态也要改变。 【解决方法】 在onB…

详解 sudo usermod -aG docker majn

这个命令涉及到几个 Linux 系统管理的基础概念&#xff0c;包括 sudo、usermod 和用户组管理。我们可以逐一地解析它们&#xff1a; sudo: sudo&#xff08;superuser do&#xff09;允许一个已经被授权的用户以超级用户或其他用户的身份执行一个命令。当使用 sudo 前缀一个命令…

网络安全攻防对抗之白加黑技术

目录 一、什么是白加黑技术&#xff1f; 二、怎么防范白加黑技术&#xff1f; 一、什么是白加黑技术&#xff1f; “白加黑”手法是一种利用DLL劫持技术的攻击方式。它通过在应用程序的导出目录中创建一个DLL文件&#xff0c;并通过LoadLibrary函数&#xff08;或者找一个已有…

video标签自动播放音视频并绘制波形图

html中的<video>标签可以用来播放常见的音视频格式&#xff0c;支持的格式包括:MP3、Ogg、WAV、AAC、MP4、WebM、AVI等&#xff0c;当然支持的格式也和浏览器和操作系统有关。这里以一个可以自动播放音视频并绘制波形图的页面为例说明一下<video>标签的用法。 vid…

一种借助MYSQL递归CTE生成所有组合情况的实现方法

需求说明 有如下表和数据&#xff1a; Nname1户口2查询机构数过多3危险驾驶4多头用信 需要输出name里的所有组合情况&#xff0c;即单个值&#xff0c;两两组合&#xff0c;三个组合、四个组合。结果为2的n次方-1中情况&#xff0c;这里是15。 预期结果为&#xff1a; Com…

【1++的数据结构】之哈希(一)

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的数据结构】 文章目录 一&#xff0c;什么是哈希&#xff1f;二&#xff0c;哈希冲突哈希函数哈希冲突解决 unordered_map与unordered_set 一&#xff0c;什么是哈希&#xff1f; 首先我们要…

Google 在 2023 开发者大会上的 AI 革命

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页 ——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…