【数据结构】AVL树(C++实现)

news2024/11/28 10:38:10

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:数据结构
🎯长路漫漫浩浩,万事皆有期待

上一篇博客:【数据结构】搜索二叉树(C++实现)

文章目录

  • AVL树的概念
  • AVL树结点的定义
  • AVL树的插入
  • AVL树的旋转
    • 左单旋
    • 右单旋
    • 左右双旋
    • 右左双旋
  • AVL树的验证
  • AVL树的查找
  • AVL树的修改
  • AVL树的删除
  • AVL树的性能
  • 总结:

AVL树的概念

二叉搜索树虽然可以提高我们查找数据的效率,但如果插入二叉搜索树的数据是有序或接近有序的,此时二叉搜索树会退化为单支树,在单支树当中查找数据相当于在单链表当中查找数据,效率是很低下的。

因此,两位俄罗斯的数学家G.M.A delson-Velskii和E.M.Landis在1962年发明了解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

AVL树可以是一棵空树,也可以是具有以下性质的一棵二叉搜索树:

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

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

注意: 这里所说的二叉搜索树的高度是平衡的是指,树中每个结点左右子树高度之差的绝对值不超过1,因为只有满二叉树才能做到每个结点左右子树高度之差均为0。

AVL树结点的定义

我们这里直接实现KV模型的AVL树,为了方便后续的操作,这里将AVL树中的结点定义为三叉链结构,并在每个结点当中引入平衡因子(右子树高度-左子树高度)。除此之外,还需编写一个构造新结点的构造函数,由于新构造结点的左右子树均为空树,于是将新构造结点的平衡因子初始设置为0即可。

template<class K, class V>
struct AVLTreeNode
{
	//三叉链
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	//存储的键值对
	pair<K, V> _kv;

	//平衡因子(balance factor)
	int _bf; //右子树高度-左子树高度

	//构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

注意: 给每个结点增加平衡因子并不是必须的,只是实现AVL树的一种方式,不引入平衡因子也可以实现AVL树,只不过会麻烦一点。

AVL树的插入

AVL树插入结点时有以下三个步骤:

按照二叉搜索树的插入方法,找到待插入位置。
找到待插入位置后,将待插入结点插入到树中。
更新平衡因子,如果出现不平衡,则需要进行旋转。

因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:

待插入结点的key值比当前结点小就插入到该结点的左子树。
待插入结点的key值比当前结点大就插入到该结点的右子树。
待插入结点的key值与当前结点的key值相等就插入失败。

如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。

与二叉搜索树插入结点不同的是,AVL树插入结点后需要更新树中结点的平衡因子,因为插入新结点后可能会影响树中某些结点的平衡因子。

由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。

所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:

新增结点在parent的右边,parent的平衡因子++。
新增结点在parent的左边,parent的平衡因子−−。

每更新完一个结点的平衡因子后,都需要进行以下判断:

如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

判断理由说明:

parent更新后的平衡因子为 -1或1
分析:只有0经过−−/++操作后会变成-1/1,说明新结点的插入使得parent的左子树或右子树增高了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。

parent更新后的平衡因子为0
分析:只有-1/1经过++/−−操作后会变成0,说明新结点插入到了parent左右子树当中高度较矮的一棵子树,插入后使得parent左右子树的高度相等了,此操作并没有改变以parent为根结点的子树的高度,从而不会影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。

parent更新后的平衡因子为-2或2
分析:此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。

注意 : parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。

而在最坏情况下,我们更新平衡因子时会一路更新到根结点。例如下面这种情况:
在这里插入图片描述

说明一下: 由于我们插入结点后需要倒着往上进行平衡因子的更新,所以我们将AVL树结点的结构设置为了三叉链结构,这样我们就可以通过父指针找到其父结点,进而对其平衡因子进行更新。当然,我们也可以不用三叉链结构,可以在插入结点时将路径上的结点存储到一个栈当中,当我们更新平衡因子时也可以通过这个栈来更新祖先结点的平衡因子,但是相对较麻烦。

若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,这时我们需要对以该结点为根结点的树进行旋转处理,而旋转处理分为四种,在进行分类之前我们首先需要进行以下分析:

我们将插入结点称为cur,将其父结点称为parent,那么我们更新平衡因子时第一个更新的就是parent结点的平衡因子,更新完parent结点的平衡因子后,若是需要继续往上进行平衡因子的更新,那么我们必定要执行以下逻辑:

cur = parent;
parent = parent->_parent;

这里我想说明的是:当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

理由如下:
若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。
而cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:

1.其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
2.其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。

综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

根据此结论,我们可以将旋转处理分为以下四类:

当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。
当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。

并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。具体原因请看后面的旋转讲解。

代码如下:

//插入函数
bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) //若AVL树为空树,则插入结点直接作为根结点
	{
		_root = new Node(kv);
		return true;
	}
	//1、按照二叉搜索树的插入方法,找到待插入位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //待插入结点的key值等于当前结点的key值
		{
			//插入失败(不允许key值冗余)
			return false;
		}
	}

	//2、将待插入结点插入到树中
	cur = new Node(kv); //根据所给值构造一个新结点
	if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
	{
		//插入到parent的左边
		parent->_left = cur;
		cur->_parent = parent;
	}
	else //新结点的key值大于parent的key值
	{
		//插入到parent的右边
		parent->_right = cur;
		cur->_parent = parent;
	}

	//3、更新平衡因子,如果出现不平衡,则需要进行旋转
	while (cur != _root) //最坏一路更新到根结点
	{
		if (cur == parent->_left) //parent的左子树增高
		{
			parent->_bf--; //parent的平衡因子--
		}
		else if (cur == parent->_right) //parent的右子树增高
		{
			parent->_bf++; //parent的平衡因子++
		}
		//判断是否更新结束或需要进行旋转
		if (parent->_bf == 0) //更新结束(新增结点把parent左右子树矮的那一边增高了,此时左右高度一致)
		{
			break; //parent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
		}
		else if (parent->_bf == -1 || parent->_bf == 1) //需要继续往上更新平衡因子
		{
			//parent树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == -2 || parent->_bf == 2) //需要进行旋转(此时parent树已经不平衡了)
		{
			if (parent->_bf == -2)
			{
				if (cur->_bf == -1)
				{
					RotateR(parent); //右单旋
				}
				else //cur->_bf == 1
				{
					RotateLR(parent); //左右双旋
				}
			}
			else //parent->_bf == 2
			{
				if (cur->_bf == -1)
				{
					RotateRL(parent); //右左双旋
				}
				else //cur->_bf == 1
				{
					RotateL(parent); //左单旋
				}
			}
			break; //旋转后就一定平衡了,无需继续往上更新平衡因子(旋转后树高度变为插入之前了)
		}
		else
		{
			assert(false); //在插入前树的平衡因子就有问题
		}
	}

	return true; //插入成功
}

AVL树的旋转

左单旋

说明:
由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。

旋转示意图如下:
在这里插入图片描述

左单旋的步骤如下:

让subR的左子树作为parent的右子树。
让parent作为subR的左子树。
让subR作为整个子树的根。
更新平衡因子。

左单旋后满足二叉搜索树的性质:

subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。

平衡因子更新如下:
在这里插入图片描述

可以看到,经过左单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左单旋后无需继续往上更新平衡因子。

代码如下:

//左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;

	//1、建立subR和parent之间的关系
	parent->_parent = subR;
	subR->_left = parent;

	//2、建立parent和subRL之间的关系
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	//3、建立parentParent和subR之间的关系
	if (parentParent == nullptr)
	{
		_root = subR;
		subR->_parent = nullptr; //subR的_parent指向需改变
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else //parent == parentParent->_right
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}

	//4、更新平衡因子
	subR->_bf = parent->_bf = 0;
}

注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。

右单旋

说明:
由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。

旋转示意图如下:
在这里插入图片描述

右单旋的步骤如下:

让subL的右子树作为parent的左子树。
让parent作为subL的右子树。
让subL作为整个子树的根。
更新平衡因子。

右单旋后满足二叉搜索树的性质:

subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
parent及其右子树当中结点的值本身就比subL的值大,因此可以作为subL的右子树。

平衡因子更新如下:
在这里插入图片描述

可以看到,经过右单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右单旋后无需继续往上更新平衡因子。

代码如下:

//右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;

	//1、建立subL和parent之间的关系
	subL->_right = parent;
	parent->_parent = subL;

	//2、建立parent和subLR之间的关系
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	//3、建立parentParent和subL之间的关系
	if (parentParent == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else //parent == parentParent->_right
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}

	//4、更新平衡因子
	subL->_bf = parent->_bf = 0;
}

注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。

左右双旋

说明:
由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。
图中,在b子树当中新增结点,或是在c子树当中新增结点,均会引发左右双旋,动图中以在b子树当中新增结点为例。
动图当中的旋转示意图如下:

1、插入新结点。
在这里插入图片描述

2、以30为旋转点进行左单旋。
在这里插入图片描述

3、以90为旋转点进行右单旋。
在这里插入图片描述

左右双旋的步骤如下:

以subL为旋转点进行左单旋。
以parent为旋转点进行右单旋。
更新平衡因子。

左右双旋后满足二叉搜索树的性质:
左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subL和parent的右子树和左子树,再让subL和parent分别作为subLR的左右子树,最后让subLR作为整个子树的根(结合图理解)。

subLR的左子树当中的结点本身就比subL的值大,因此可以作为subL的右子树。
subLR的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。
经过步骤1/2后,subL及其子树当中结点的值都就比subLR的值小,而parent及其子树当中结点的值都就比subLR的值大,因此它们可以分别作为subLR的左右子树。

左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:
1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。
在这里插入图片描述

2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。
在这里插入图片描述

1、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。
在这里插入图片描述

可以看到,经过左右双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左右双旋后无需继续往上更新平衡因子。

代码如下:

//左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf; //subLR不可能为nullptr,因为subL的平衡因子是1

	//1、以subL为旋转点进行左单旋
	RotateL(subL);

	//2、以parent为旋转点进行右单旋
	RotateR(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}

右左双旋

说明:
由于插入新结点后,可能并不会立即进行旋转操作,而可能是在更新其祖先结点的过程中出现了不平衡而进行的旋转操作,因此用长方形表示下面的子树。
图中,在b子树当中新增结点,或是在c子树当中新增结点,均会引发右左双旋,动图中以在c子树当中新增结点为例。
动图当中的旋转示意图如下:

1、插入新结点。
在这里插入图片描述

2、以90为旋转点进行右单旋。
在这里插入图片描述

3、以30为旋转点进行左单旋。
在这里插入图片描述

右左双旋的步骤如下:

以subR为旋转点进行右单旋。
以parent为旋转点进行左单旋。
更新平衡因子。

右左双旋后满足二叉搜索树的性质:
右左双旋后,实际上就是让subRL的左子树和右子树,分别作为parent和subR的右子树和左子树,再让parent和subR分别作为subRL的左右子树,最后让subRL作为整个子树的根(结合图理解)。

subRL的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。
subRL的右子树当中的结点本身就比subR的值小,因此可以作为subR的左子树。
经过步骤1/2后,parent及其子树当中结点的值都就比subRL的值小,而subR及其子树当中结点的值都就比subRL的值大,因此它们可以分别作为subRL的左右子树。

右左双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:
1、当subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。
在这里插入图片描述

2、当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。
在这里插入图片描述

3、当subRL原始平衡因子是0时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。
在这里插入图片描述

可以看到,经过右左双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右左双旋后无需继续往上更新平衡因子。

代码如下:

//右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	//1、以subR为轴进行右单旋
	RotateR(subR);

	//2、以parent为轴进行左单旋
	RotateL(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subRL->_bf = 0;
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == -1)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 0)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}

AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。

代码如下:

//中序遍历
void Inorder()
{
	_Inorder(_root);
}
//中序遍历子函数
void _Inorder(Node* root)
{
	if (root == nullptr)
		return;
	_Inorder(root->_left);
	cout << root->_kv.first << " ";
	_Inorder(root->_right);
}

但中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。

采用后序遍历,变量步骤如下:
1.从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)
2.先判断左子树是否是平衡二叉树。
3.再判断右子树是否是平衡二叉树。
4.若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)

代码如下:

//判断是否为AVL树
bool IsAVLTree()
{
	int hight = 0; //输出型参数
	return _IsBalanced(_root, hight);
}
//检测二叉树是否平衡
bool _IsBalanced(Node* root, int& hight)
{
	if (root == nullptr) //空树是平衡二叉树
	{
		hight = 0; //空树的高度为0
		return true;
	}
	//先判断左子树
	int leftHight = 0;
	if (_IsBalanced(root->_left, leftHight) == false)
		return false;
	//再判断右子树
	int rightHight = 0;
	if (_IsBalanced(root->_right, rightHight) == false)
		return false;
	//检查该结点的平衡因子
	if (rightHight - leftHight != root->_bf)
	{
		cout << "平衡因子设置异常:" << root->_kv.first << endl;
	}
	//把左右子树的高度中的较大值+1作为当前树的高度返回给上一层
	hight = max(leftHight, rightHight) + 1;
	return abs(rightHight - leftHight) < 2; //平衡二叉树的条件
}

AVL树的查找

AVL树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:
1.若树为空树,则查找失败,返回nullptr。
2.若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
3.若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
4.若key值等于当前结点的值,则查找成功,返回对应结点。
代码如下:

//查找函数
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_kv.first) //key值小于该结点的值
		{
			cur = cur->_left; //在该结点的左子树当中查找
		}
		else if (key > cur->_kv.first) //key值大于该结点的值
		{
			cur = cur->_right; //在该结点的右子树当中查找
		}
		else //找到了目标结点
		{
			return cur; //返回该结点
		}
	}
	return nullptr; //查找失败
}

AVL树的修改

方法一

实现修改AVL树当中指定key值结点的value,我们可以实现一个Modify函数,该函数当中的逻辑如下:

调用查找函数获取指定key值的结点。
对该结点的value进行修改。

代码如下:

//修改函数
bool Modify(const K& key, const V& value)
{
	Node* ret = Find(key);
	if (ret == nullptr) //未找到指定key值的结点
	{
		return false;
	}
	ret->_kv.second = value; //修改结点的value
	return true;
}

方法二

还有一种方法就是模仿C++STL库当中map的实现方式,将插入函数的返回值设置为pair类型的,插入函数的返回值逻辑如下:

若待插入结点的键值key在map当中不存在,则结点插入成功,并返回插入后结点的指针和true。
若待插入结点的键值key在map当中已经存在,则结点插入失败,并返回map当中键值为key的结点的指针和false。

我们只需要对插入函数的返回值做一点点修改即可,代码如下:

//插入函数
pair<Node*, bool> Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) //若AVL树为空树,则插入结点直接作为根结点
	{
		_root = new Node(kv);
		return make_pair(_root, true); //插入成功,返回新插入结点和true
	}
	//1、按照二叉搜索树的插入方法,找到待插入位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //待插入结点的key值等于当前结点的key值
		{
			//插入失败(不允许key值冗余)
			return make_pair(cur, false); //插入失败,返回已经存在的结点和false
		}
	}

	//2、将待插入结点插入到树中
	cur = new Node(kv); //根据所给值构造一个新结点
	Node* newnode = cur; //记录新插入的结点
	if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
	{
		//插入到parent的左边
		parent->_left = cur;
		cur->_parent = parent;
	}
	else //新结点的key值大于parent的key值
	{
		//插入到parent的右边
		parent->_right = cur;
		cur->_parent = parent;
	}

	//3、更新平衡因子,如果出现不平衡,则需要进行旋转
	while (cur != _root) //最坏一路更新到根结点
	{
		if (cur == parent->_left) //parent的左子树增高
		{
			parent->_bf--; //parent的平衡因子--
		}
		else if (cur == parent->_right) //parent的右子树增高
		{
			parent->_bf++; //parent的平衡因子++
		}
		//判断是否更新结束或需要进行旋转
		if (parent->_bf == 0) //更新结束(新增结点把parent左右子树矮的那一边增高了,此时左右高度一致)
		{
			break; //parent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
		}
		else if (parent->_bf == -1 || parent->_bf == 1) //需要继续往上更新平衡因子
		{
			//parent树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == -2 || parent->_bf == 2) //需要进行旋转(此时parent树已经不平衡了)
		{
			if (parent->_bf == -2)
			{
				if (cur->_bf == -1)
				{
					RotateR(parent); //右单旋
				}
				else //cur->_bf == 1
				{
					RotateLR(parent); //左右双旋
				}
			}
			else //parent->_bf == 2
			{
				if (cur->_bf == -1)
				{
					RotateRL(parent); //右左双旋
				}
				else //cur->_bf == 1
				{
					RotateL(parent); //左单旋
				}
			}
			break; //旋转后就一定平衡了,无需继续往上更新平衡因子(旋转后树高度变为插入之前了)
		}
		else
		{
			assert(false); //在插入前树的平衡因子就有问题
		}
	}

	return make_pair(newnode, true); //插入成功,返回新插入结点和true
}

然后我们再对运算符[ ]进行重载,[ ]的重载逻辑如下:

调用插入函数插入键值对。
拿出从插入函数获取到的结点。
返回该结点value的引用。

这样一来,当我们使用[key]时,其返回值逻辑如下:

如果key不在树中,则先插入键值对<key, V()>,然后返回该键值对中value的引用。
如果key已经在树中,则返回键值为key结点value的引用。

如此一来,我们既可以用[ ]来进行指定key值结点value的修改,又可以用[ ]进行数据的插入了,并且插入时更方便。

代码如下:

//operator[]重载
V& operator[](const K& key)
{
	//调用插入函数插入键值对
	pair<Node*, bool> ret = Insert(make_pair(key, V()));
	//拿出从插入函数获取到的结点
	Node* node = ret.first;
	//返回该结点value的引用
	return node->_kv.second;
}

AVL树的删除

要进行结点的删除,首先需要在树中找到对应key值的结点,寻找待删除结点的方法和二叉搜索树相同:

先找到待删除的结点。
若找到的待删除结点的左右子树均不为空,则需要使用替换法进行删除。

替换法删除指的是:让待删除结点左子树当中key值最大的结点,或待删除结点右子树当中值最小的结点代替待删除结点被删除(代码中以后者为例),然后再将待删除结点的key值以及value值都改为代替其被删除的结点的值即可。

也就是说,我们最终找到的实际被删除的结点的左右子树当中至少有一个为空树。

在找到实际需要被删除的结点后,我们先不进行实际的删除,而是先进行平衡因子的更新,不然后续更新平衡因子时特别麻烦(已经尝试过),而更新平衡因子时的规则与插入结点时的规则是相反的,更新规则如下:

删除的结点在parent的右边,parent的平衡因子−−。
删除的结点在parent的左边,parent的平衡因子++。

并且每更新完一个结点的平衡因子后,都需要进行以下判断:

如果parent的平衡因子等于-1或者1,表明无需继续往上更新平衡因子了。
如果parent的平衡因子等于0,表明还需要继续往上更新平衡因子。
如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

判断理由说明:
parent更新后的平衡因子为 -1或1
分析:只有0经过−−/++操作后会变成-1/1,说明原来parent的左子树和右子树高度相同,现在我们删除一个结点,并不会影响以parent为根结点的子树的高度,从而变化影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。

parent更新后的平衡因子为 0
分析:只有-1/1经过++/−−操作后会变成0,说明本次删除操作使得parent的左右子树当中较高的一棵子树的高度降低了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。

parent更新后的平衡因子为 -2或2
分析:此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。

注意:parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。

而在最坏情况下,删除结点后更新平衡因子时也会一路更新到根结点。例如下面这种情况:
在这里插入图片描述

在更新完平衡因子后,我们再进行实际删除结点的操作,因为实际删除结点的左右子树当中至少有一个为空树,因此我们实际删除结点时的逻辑如下:

若实际删除结点的左子树为空,则让其parent链接到实际删除结点的右子树,最后再删除结点即可。
若实际删除结点的右子树为空,则让其parent链接到实际删除结点的左子树,最后再删除结点即可。

但是要注意,因为结点是三叉链结构,因此在链接结点的过程中需要建立两个结点之间的双向关系。

在进行旋转处理时,我们将其分为以下六种情况:

当parent的平衡因子为-2,parent的左孩子的平衡因子为-1时,进行右单旋。
当parent的平衡因子为-2,parent的左孩子的平衡因子为1时,进行左右双旋。
当parent的平衡因子为-2,parent的左孩子的平衡因子为0时,也进行右单旋,但此时平衡因子调整与之前有所不同,具体看代码。
当parent的平衡因子为2,parent的右孩子的平衡因子为-1时,进行右左双旋。
当parent的平衡因子为2,parent的右孩子的平衡因子为1时,进行左单旋。
当parent的平衡因子为2,parent的右孩子的平衡因子为0时,也进行左单旋,但此时平衡因子调整与之前有所不同,具体看代码。

与插入结点不同的是,删除结点时若是进行了旋转处理,那么在进行旋转处理后我们必须继续往上更新平衡因子,因为旋转的本质就是降低树的高度,旋转后树的高度降低了,就会影响其父结点的平衡因子,因此我们还需要继续往上更新平衡因子。

更正: 上述旋转处理的六种情况当中,若属于情况三或情况六,那么在旋转后无需继续往上更新平衡因子,因为这两种情况旋转后树的高度并没有发生变化。(感谢可乐不解渴的指正,代码已更正)

代码如下:

//删除函数
bool Erase(const K& key)
{
	//用于遍历二叉树
	Node* parent = nullptr;
	Node* cur = _root;
	//用于标记实际的删除结点及其父结点
	Node* delParentPos = nullptr;
	Node* delPos = nullptr;
	while (cur)
	{
		if (key < cur->_kv.first) //所给key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_kv.first) //所给key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //找到了待删除结点
		{
			if (cur->_left == nullptr) //待删除结点的左子树为空
			{
				if (cur == _root) //待删除结点是根结点
				{
					_root = _root->_right; //让根结点的右子树作为新的根结点
					if (_root)
						_root->_parent = nullptr;
					delete cur; //删除原根结点
					return true; //根结点无祖先结点,无需进行平衡因子的更新操作
				}
				else
				{
					delParentPos = parent; //标记实际删除结点的父结点
					delPos = cur; //标记实际删除的结点
				}
				break; //删除结点有祖先结点,需更新平衡因子
			}
			else if (cur->_right == nullptr) //待删除结点的右子树为空
			{
				if (cur == _root) //待删除结点是根结点
				{
					_root = _root->_left; //让根结点的左子树作为新的根结点
					if (_root)
						_root->_parent = nullptr;
					delete cur; //删除原根结点
					return true; //根结点无祖先结点,无需进行平衡因子的更新操作
				}
				else
				{
					delParentPos = parent; //标记实际删除结点的父结点
					delPos = cur; //标记实际删除的结点
				}
				break; //删除结点有祖先结点,需更新平衡因子
			}
			else //待删除结点的左右子树均不为空
			{
				//替换法删除
				//寻找待删除结点右子树当中key值最小的结点作为实际删除结点
				Node* minParent = cur;
				Node* minRight = cur->_right;
				while (minRight->_left)
				{
					minParent = minRight;
					minRight = minRight->_left;
				}
				cur->_kv.first = minRight->_kv.first; //将待删除结点的key改为minRight的key
				cur->_kv.second = minRight->_kv.second; //将待删除结点的value改为minRight的value
				delParentPos = minParent; //标记实际删除结点的父结点
				delPos = minRight; //标记实际删除的结点
				break; //删除结点有祖先结点,需更新平衡因子
			}
		}
	}
	if (delParentPos == nullptr) //delParentPos没有被修改过,说明没有找到待删除结点
	{
		return false;
	}

	//记录待删除结点及其父结点(用于后续实际删除)
	Node* del = delPos;
	Node* delP = delParentPos;

	//更新平衡因子
	while (delPos != _root) //最坏一路更新到根结点
	{
		if (delPos == delParentPos->_left) //delParentPos的左子树高度降低
		{
			delParentPos->_bf++; //delParentPos的平衡因子++
		}
		else if (delPos == delParentPos->_right) //delParentPos的右子树高度降低
		{
			delParentPos->_bf--; //delParentPos的平衡因子--
		}
		//判断是否更新结束或需要进行旋转
		if (delParentPos->_bf == 0)//需要继续往上更新平衡因子
		{
			//delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			delPos = delParentPos;
			delParentPos = delParentPos->_parent;
		}
		else if (delParentPos->_bf == -1 || delParentPos->_bf == 1) //更新结束
		{
			break; //delParent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
		}
		else if (delParentPos->_bf == -2 || delParentPos->_bf == 2) //需要进行旋转(此时delParentPos树已经不平衡了)
		{
			if (delParentPos->_bf == -2)
			{
				if (delParentPos->_left->_bf == -1)
				{
					Node* tmp = delParentPos->_left; //记录delParentPos右旋转后新的根结点
					RotateR(delParentPos); //右单旋
					delParentPos = tmp; //更新根结点
				}
				else if(delParentPos->_left->_bf == 1)
				{
					Node* tmp = delParentPos->_left->_right; //记录delParentPos左右旋转后新的根结点
					RotateLR(delParentPos); //左右双旋
					delParentPos = tmp; //更新根结点
				}
				else //delParentPos->_left->_bf == 0
				{
					Node* tmp = delParentPos->_left; //记录delParentPos右旋转后新的根结点
					RotateR(delParentPos); //右单旋
					delParentPos = tmp; //更新根结点
					//平衡因子调整
					delParentPos->_bf = 1;
					delParentPos->_right->_bf = -1;
					break; //更正
				}
			}
			else //delParentPos->_bf == 2
			{
				if (delParentPos->_right->_bf == -1)
				{
					Node* tmp = delParentPos->_right->_left; //记录delParentPos右左旋转后新的根结点
					RotateRL(delParentPos); //右左双旋
					delParentPos = tmp; //更新根结点
				}
				else if(delParentPos->_right->_bf == 1)
				{
					Node* tmp = delParentPos->_right; //记录delParentPos左旋转后新的根结点
					RotateL(delParentPos); //左单旋
					delParentPos = tmp; //更新根结点
				}
				else //delParentPos->_right->_bf == 0
				{
					Node* tmp = delParentPos->_right; //记录delParentPos左旋转后新的根结点
					RotateL(delParentPos); //左单旋
					delParentPos = tmp; //更新根结点
					//平衡因子调整
					delParentPos->_bf = -1;
					delParentPos->_left->_bf = 1;
					break; //更正
				}
			}
			//delParentPos树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			delPos = delParentPos;
			delParentPos = delParentPos->_parent;
			//break; //error
		}
		else
		{
			assert(false); //在删除前树的平衡因子就有问题
		}
	}
	//进行实际删除
	if (del->_left == nullptr) //实际删除结点的左子树为空
	{
		if (del == delP->_left) //实际删除结点是其父结点的左孩子
		{
			delP->_left = del->_right;
			if (del->_right)
				del->_right->_parent = parent;
		}
		else //实际删除结点是其父结点的右孩子
		{
			delP->_right = del->_right;
			if (del->_right)
				del->_right->_parent = parent;
		}
	}
	else //实际删除结点的右子树为空
	{
		if (del == delP->_left) //实际删除结点是其父结点的左孩子
		{
			delP->_left = del->_left;
			if (del->_left)
				del->_left->_parent = parent;
		}
		else //实际删除结点是其父结点的右孩子
		{
			delP->_right = del->_left;
			if (del->_left)
				del->_left->_parent = parent;
		}
	}
	delete del; //实际删除结点
	return true;
}

AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即logN。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但当一个结构经常需要被修改时,AVL树就不太适合了。

总结:

今天我们比较详细地学习了AVL树的相关知识,了解了一些有关的底层原理。接下来,我们将进行红黑树的学习。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

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

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

相关文章

性格敏感怎么办?改变性格敏感的方法有哪些?

有这么一群人&#xff0c;他们的情绪很容易受到外界的影响&#xff0c;就像一汪宁静的湖水&#xff0c;被风轻易地吹出皱纹。他们有着高度敏感的神经&#xff0c;外界稍微一点风吹草动&#xff0c;就会牵动他们紧张的情绪。 他们的思维就像脱缰的野马&#xff0c;生活中任何一…

String 进阶

字符串拼接 // 常量与常量的拼接结果放在常量池 // 常量池中不会存在相同的常量 String str1 "a" "b"; System.out.println(str1 "ab");// 拼接时有一个为变量&#xff0c;则结果会放在堆中。 // 变量拼接的原理是 StringBuilder append 最后…

C++代码示例:进制数简单生成工具

文章目录 前言代码仓库内容代码&#xff08;有详细注释&#xff09;编译和运行命令结果总结参考资料作者的话 前言 C代码示例&#xff1a;进制数简单生成工具。 代码仓库 yezhening/Programming-examples: 编程实例 (github.com)Programming-examples: 编程实例 (gitee.com) …

VSCode 在部分 Linux 设备上终端和文本编辑器显示文本不正常的解决方法

部分Linux设备上运行VSCode时&#xff0c;发现文本编辑器的缩放不明显&#xff0c;终端字体间距过大等。 这里以Kali Linux为例&#xff0c;其他Linux发行版请选择对应的系统内置的等宽字体 我们依次打开 设置 -> 外观 -> 字体 这里我们可以发现&#xff0c;Kali Linux默…

华为云云耀云服务器L实例评测|Elasticsearch的可视化Kibana工具安装 IK分词器的安装和使用

前言 最近华为云云耀云服务器L实例上新&#xff0c;也搞了一台来玩&#xff0c;期间遇到各种问题&#xff0c;在解决问题的过程中学到不少和运维相关的知识。 本篇博客介绍Elasticsearch的可视化Kibana工具安装&#xff0c;以及IK分词器的安装和使用。 其他相关的Elasticsea…

为什么需要工业物联网 (IIoT)?如何实施?

制造业数字化、网络化、智能化已经是大势所趋。这些特性都在改变着制造业的格局&#xff0c;进而影响着我们生活和工作的方式。工业物联网作为一种利用传感器、云计算、大数据和人工智能等技术&#xff0c;实现了工业设备、流程和服务的智能化&#xff0c;正逐渐成为制造业的发…

【U8+】查看余额表只有科目,没有借贷方发生额以及余额。

【问题描述】 使用用友U8时&#xff0c; 查询发生额及余额表过程中&#xff0c; 打开报表后&#xff0c;只有科目列&#xff0c;所有金额列都没有。 并且点击【还原列宽】没有没有任何反应&#xff1b;点击【栏目】后&#xff0c;发现栏目设置中全部为空。 【解决方法】 跟踪…

转转闲鱼交易猫链接源码搭建 独立后台

转转交易猫闲鱼后台源码&#xff0c;功能强大包含了多种模板和功能等等 后台一键生成链接&#xff0c;独立后台管理 教程&#xff1a;修改数据库账号 不会可以看源码里有教程 下载源码&#xff1a;https://pan.baidu.com/s/16lN3gvRIZm7pqhvVMYYecQ?pwd6zw3

2023年汉字小达人市级比赛题型示例和全真在线练习题

上一篇文章中&#xff0c;六分成长介绍了2023年汉字小达人市级比赛的相关安排和常见的问题解答&#xff0c;这篇文章我来给大家介绍具体的题型和典型试题&#xff0c;并且将这些模拟题做成了电子版&#xff0c;每次抽题150道&#xff0c;和市级比赛完全一样&#xff0c;尽早熟悉…

PHP 反序列化漏洞:身份标识

文章目录 参考环境访问修饰符访问修饰符PHP 与访问修饰符 手写身份标识身份标识定义身份标识控制字符 NUL在 PHP 中如何表示空字符&#xff1f; 通过空字符尝试构建包含非公共属性对象的序列化文本 空字符的传输控制字符的不可打印性结论另辟蹊径URL 字符编码将非 ASCII 字符文…

c#设计模式-结构型模式 之 组合模式

&#x1f680;简介 组合模式又名部分整体模式&#xff0c;是一种 结构型设计模式 &#xff0c;是用于把一组相似的对象当作一个 单一的对象 。组合模式 依据树形结构来组合对象 &#xff0c;用来表示部分以及整体层&#xff0c;它可以让你将对象组合成树形结构&#xff0c;并且…

UE5报错及解决办法

1、编译报错&#xff0c;内容如下&#xff1a; Unable to build while Live Coding is active. Exit the editor and game, or press CtrlAltF11 if iterating on code in the editor or game 解决办法 取消Enable Live Coding勾选

摆脱推荐算法,实现万物皆可『RSS』

前言 相信各位对推荐算法已经很熟悉了&#xff0c;平台基于推荐算法不断推送我们感兴趣的信息&#xff0c;但是身处推荐算法中心&#xff0c;有时我们可能感觉视野越来越闭塞&#xff0c;原来节约我们时间的推荐系统&#xff0c;这时却成了困住我们的信息茧房 那么也许 RSS&a…

007:连续跌三天,第四天上涨的概率--可视化优化1

接着006&#xff0c;有一些问题&#xff0c;要手动改文件&#xff0c;麻烦&#xff0c;直接出来一个按钮&#xff0c;点击出现弹窗可以选择文件。 二来就是&#xff0c;所统计的数据&#xff0c;没有展示细节。应该展示的细节包括&#xff1a;股票代码&#xff0c;K线周期&…

JDK11优化了哪些功能以及新增了哪些特性功能|JDK各个版本的特性分析

一、前言 上一期讲了JDK10的一些新特性&#xff0c;需要回顾的朋友们可以去该专栏回顾一下 这一期讲一讲JDK11的一些新功能 二、新增特性 以下是JDK 11的一些新增或变化的特性&#xff1a; 1. 纯字符串类型的 HTTP 客户端: JDK 11 引入了一个新的 HTTP 协议的客户端 API&…

【STM32基础 CubeMX】定时器的使用

文章目录 前言一、定时器是什么二、CubeMX配置定时器三、代码分析3.1 CubeMX代码分析3.2 几个库函数以中断的方式开启定时器定时器中断函数 四、定时器应用4.1 定时器闪烁LED 总结 前言 在嵌入式系统开发中&#xff0c;精确地控制时间和时序是至关重要的。STM32微控制器提供了…

错误:F13 is an invalid placement site

在vivado中绑定引脚时提示&#xff1a;F13 is an invalid placement site f13引脚在板子上是接千兆网的rxclk端的。在进一步不排查出现这样的问题提示 Illegal to place instance u_gmii_to_rgmii/u_rgmii_rx/BUFIO_inst on site TIEOFF_X0Y326. The location site type (TIEOF…

14:00面试测试岗,14:06就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到9月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%,…

Java编程技巧:跨域

目录 1、跨域概念2、后端CORS&#xff08;跨域资源共享&#xff09;配置原理3、既然请求跨域了&#xff0c;那么请求到底发出去没有&#xff1f;4、通过后端CORS&#xff08;跨域资源共享&#xff09;配置解决跨域问题代码4.1、SpringBoot&#xff08;FilterRegistrationBean&a…

鼠标移动视差玻璃态效果

效果展示 页面结构组成 从页面上可以看到页面的结构比较简单&#xff0c;主要的元素如下&#xff1a; 背景图片文字带有清晰中心圆的毛玻璃 但是在怎么组织这几个元素的层次关系就需要需要考虑一下&#xff0c;并且中间的圆怎么实现也是需要考虑的。 CSS 知识点 clip-path…