c++: AVL树

AVL树的基本概念

AVL树是二叉搜索树的一种思想进阶,它的结构是在二叉搜索树的结构前提下形成的。
基本特点:
1.它的左右子树的高度相差最多为一
2.为了更好的控制左右子树的平衡,它引出了平衡因子(balance factor)的概念方便统计。每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1,AVL树并不是必须要平衡因⼦,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡,就像⼀个⻛向标⼀样。
3.AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在log(N) ,那么增删查改的效率也可以控制在log(N),相⽐⼆叉搜索树有了本质的提升

AVL树的实现

在这里我们只实现AVL树的插入,能让我们理解这个树的结构的形成,删除这里不做实现,因为我们学习树的这种复杂结构本身难度就大,其实只要你会了插入就差不多能写出删除。有兴趣的可以去尝试

它的基本框架

cpp 复制代码
//AVL树的左右高度差最大为1;
//平衡因子= 右子树高度-左子树高度
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>& key_value= pair<K,V>())
		:_kv(key_value)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

template<class K, class V>
class AVLTree
{
        typedef AVLTreeNode<K, V> Node;
public:
        //...

AVL树的插入实现

基本的插入跟我们的二叉搜索树的思想相同

cpp 复制代码
bool insert(const pair<K, V>& key_value)
{
	if (!_root)
	{
		_root = new node(key_value);
		return true;
	}
	node* cur = _root;
	node* parent = nullptr;
	while (cur)
	{
		if (key_value.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key_value.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else {
			return false;
		}
	}
	cur = new node(key_value);
	if ( key_value.first < parent->_kv.first)
		parent->_left = cur;
	else 
		parent->_right = cur;
	cur->_parent = parent;

只是做完前面的逻辑之后,还需要考虑平衡因子是否是再-1/0/1这几个数,所以还要从插入的位置直到开始的根结点,判断每个结点的平衡因子是否为正确的范围,如果不在就要进行旋转处理


首先以这张图为例,如果是插入的结点为7,然后使它的父结点平衡因子为0,因为是在它的右边插入,又因为平衡因子=右子树高度-左子树高度,所以说明它以前为1,那它的祖先不受影响,因为没有改变它们的高度,
所以第一种情况: 插入结点之后如果插入结点的父结点平衡因子为0,就不用再向上检查它的祖先
很快可以以得出第二种情况: 如果插入结点之后如果它的父结点为1或-1那就证明也会影响它们的祖先结点,那就要再向上去判断它的祖先结点的平衡因子是否平衡,直到最后的根结点

如果是这种情况,向上判断有父结点平衡因子为2/-2的情况那就要旋转处理,将这个父结点的高度降下来,并且旋转之后不需要再向上判断

右旋转

特点: 由上图可知插入的-3为左边,那就构成了存粹的左边高,那就要进行右旋转,我们把它们转为抽象图表示更直观

操作为: 因为5<b<10,那我们可以将b作为10的左子树,将10作为5的右子树,这样5就成了新的部分树的根结点或者就是根结点,要看10是否有祖先,这样左右就平衡了

然后只需注意如果b不为空,b的_parent指针要指向新的父结点,以前接受的旋转的parent结点也就是10要更新它的parent,5的也要更新,并且如果10有父结点,那5的parent指针也要更新,最后要更新它们的平衡因子
代码实现:

cpp 复制代码
void RotateR(node*& parent)
{
	//先旋转
	node* subL = parent->_left;
	node* subLR = subL->_right;
	parent->_left = subLR;
	node* parent_parent = parent->_parent;
	if (subLR)
		subLR->_parent = parent;
	subL->_right = parent;
	parent->_parent = subL;
	//再处理旋转后parent的问题
	if (parent_parent)
	{
		if (parent_parent->_left == parent)
		{
			parent_parent->_left = subL;
			
		}
		else {
			parent_parent->_right = subL;
		}
		subL->_parent = parent_parent;
	}
	else {
		_root = subL;
		subL->_parent = nullptr;
	}
	//最后处理平衡因子的问题
	subL->_bf = parent->_bf = 0;
}

左旋转

有了右旋转的认识,左旋转就是存粹的要平衡的结点的右边高,因为5<b<10,所以b可以当5的右子树·,5可以当10的左子树,这样就平衡了,最后更新它们的parent结点和平衡因子

代码实现:

cpp 复制代码
void RotateL(node*& parent)
{
	node* subR = parent->_right;
	node* subRL = subR->_left;
	node* parent_parent = parent->_parent;
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;
	subR->_left = parent;
	parent->_parent = subR;
	if (parent_parent)
	{
		if (parent_parent->_left == parent)
		{
			parent_parent->_left = subR;
			subR->_parent = parent_parent;
		}
		else {
			parent_parent->_right = subR;
			subR->_parent = parent_parent;
		}
	}
	else {
		_root = subR;
		subR->_parent = nullptr;
	}
	subR->_bf = parent->_bf = 0;
}

左右双旋

对比只要左旋转,这个就不是存粹的左边高,它是在5的右边,你如果就只左旋转的话是不行的,所以需要在b的基础上再分出父结点和左右子树
左边⾼时,如果插⼊位置不是在a⼦树,⽽是插⼊在b⼦树,b⼦树⾼度从h变成h+1,引发旋转,右单旋⽆法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边⾼,但是插⼊在b⼦树中,10为跟的⼦树不再是单纯的左边⾼,对于10是左边⾼,但是对于5是右边⾼,需要⽤两次旋转才能解决,以5为旋转点进⾏⼀个左单旋,以10为旋转点进⾏⼀个右单旋,这棵树就平衡了。

然后将以5为一小部分要旋转的子树,将5和8进行左旋转,将树弄为存粹左边高,但是插入的数据可能在e上也可能为f上,但这没关系,最终影响的是平衡因子先将结构弄好在考虑,我这里展示的就放e上

然后再进行右旋转

最后更新平衡因子,在f的位置时,在原有的基础上改平衡因子就行
代码如下:

cpp 复制代码
void RotateLR(node* parent)
{
	node* subL = parent->_left;
	node* subLR = subL->_right;
	int bf = subLR->_bf;
	RotateL(subL);
	RotateR(parent);
	//调整平衡因子
	if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else {
		assert(false);
	}
}

右左双旋

跟左右双旋类似,下⾯我们将a/b/c⼦树抽象为⾼度h的AVL⼦树进⾏分析,另外我们需要把b⼦树的细节进⼀步展开为12和左⼦树⾼度为h-1的e和f⼦树,因为我们要对b的⽗亲15为旋转点进⾏右单旋,右单旋需要动b树中的右⼦树。b⼦树中新增结点的位置不同,平衡因⼦更新的细节也不同,通过观察12的平衡因⼦不同,这⾥我们要分三个场景讨论。

场景1:h>=1时,新增结点插⼊在e⼦树,e⼦树⾼度从h-1变为h并不断更新12->15->10平衡因⼦,引发旋转,其中12的平衡因⼦为-1,旋转后10和12平衡因⼦为0,15平衡因⼦为1。
•场景2:h>=1时,新增结点插⼊在f⼦树,f⼦树⾼度从h-1变为h并不断更新12->15->10平衡因⼦,引发旋转,其中12的平衡因⼦为1,旋转后15和12平衡因⼦为0,10平衡因⼦为-1。
• 场景3:h==0时,a/b/c都是空树,b⾃⼰就是⼀个新增结点,不断更新15->10平衡因⼦,引发旋转,其中12的平衡因⼦为0,旋转后10和12和15平衡因⼦均为0。

代码如下:

cpp 复制代码
void RotateRL(node* parent)
{
	node* subR = parent->_right;
	node* subRL = subR->_left;
	int bf = subRL->_bf;
	RotateR(subR);
	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);
	}
}

平衡检测

cpp 复制代码
int _Height(node* root)
{
	if (root == nullptr)
	{
		return 0;
	}
	int left = _Height(root->_left);
	int right = _Height(root->_right);
	return  left > right ? left + 1 : right + 1;
}
bool _IsBalanceTree(node* root)
{
	if (root == nullptr)
	{
		return true;
	}
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;
	if (abs(diff) >= 2 || diff != root->_bf)
	{
		cout << root->_kv.first << "->" << " 平衡因子异常";
		return false;
	}
	return  _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
相关推荐
进击的荆棘1 小时前
优选算法——优先级队列
数据结构·c++·算法·leetcode·优先级队列
不会C语言的男孩1 小时前
Linux 系统编程 · 第 9 章:进程创建
linux·c语言·开发语言
skywalk81631 小时前
段言项目推进6.15 @ Dumate+Trae
开发语言·学习·编程
我命由我123451 小时前
Android 开发问题:全局的主题颜色设置,导致 CheckBox 控件在勾选状态下不显示样式
android·java·开发语言·java-ee·intellij-idea·intellij idea·android jetpack
Cloud_Shy6181 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第七章 Item 51)
开发语言·人工智能·笔记·python·学习方法
AI+程序员在路上1 小时前
CSP、PP、PV、HM 在 CiA402 标准下的差异解析
linux·c语言·开发语言·嵌入式硬件
nix.gnehc1 小时前
Python 并发深度解析
服务器·开发语言·python
我是一颗柠檬1 小时前
【Java项目技术亮点】Leaf号段模式双Buffer优化
java·开发语言·分布式·后端·架构
Cx330❀1 小时前
【MySQL基础】详解MySQL数据类型:底层原理、越界测试与最佳实践
linux·开发语言·数据库·c++·mysql