c++-AVL树

news2024/11/15 19:41:44

文章目录

  • 前言
  • 一、AVL树
    • 1、AVL树概念
    • 2、AVL树模拟实现
    • 3、AVL树的旋转操作
      • 3.1 左单旋
      • 3.2 左单旋代码实现
      • 3.3 右单旋
      • 3.4 右单旋代码实现。
      • 3.5 什么时候调用左单旋和右单旋
      • 3.6 左右双旋
      • 3.7 左右双旋代码实现
      • 3.8 右左双旋
      • 3.9 右左双旋代码实现
      • 3.10 什么时候调用左右双旋和右左双旋
    • 4、测试
    • 5、总结
    • 6、AVL树的性能
    • 7、AVL树的结点删除
    • 8、代码


前言


一、AVL树

1、AVL树概念

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

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
  • 如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。

在这里插入图片描述

2、AVL树模拟实现

下面我们求AVL树的平衡因子采用的是右子树高度减左子树高度。
我们将AVL树的结点定义为下面这样

	template<class K, class V>
	class 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)
		{

		}
	};

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。下面我们先来实现二叉搜索树的插入结点的方法。

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 < cur->_kv.first)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			//将父结点也改变
			cur->_parent = parent;

			return true;
		}
	private:
		Node* _root = nullptr;
	};
}

上面的代码中我们就初步完成了二叉搜索树的结点插入,因为AVL树的插入需要保持平衡因子,所以我们在插入新结点时还需要来维护平衡因子。
当插入新结点cur后,我们就需要更新parent的平衡因子。在插入cur之前,parent的平衡因子分为三种情况:-1,0,1。
(1). 如果cur插入到parent的左侧,只需给parent的平衡因子-1即可。即parent->_bf- -;
(2). 如果cur插入到parent的右侧,只需给parent的平衡因子+1即可。即parent->_bf++;

当更新完parent的平衡因子后,可能会影响parent的祖先的平衡因子(全部祖先或部分祖先),parent的平衡因子可能会出现下面的三种情况:
(1). 如果parent的平衡因子为0,说明插入之前parent的平衡因子为正1或负1,插入后被调整为0,此时还满足AVL树的性质,所以当前cur结点插入成功。此时以parent为根的树的高度不变,不用继续往上更新。
(2). 如果parent的平衡因子为正1或负1,说明插入前parent的平衡因子一定为0,插入后被更新成正1或负1,此时以parent为根的树的高度增加,需要继续向上更新父结点的平衡因子。
(3). 如果parent的平衡因子为正2或负2,则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理。
在这里插入图片描述
下面我们来使用代码实现上面的分析过程。

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 < cur->_kv.first)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			//将父结点也改变
			cur->_parent = parent;

			//更新平衡因子
			while (parent)
			{
				//先判断cur插入到parent的左边或右边,然后更新parent的平衡因子
				if (cur == parent->_right)
				{
					parent->_bf++;
				}
				else
				{
					parent->_bf--;
				}
				//判断更新后parent的平衡因子为多少,是否还需要继续向上更新祖先的平衡因子
				if (parent->_bf == 1 || parent->_bf == -1)
				{
					//说明以parent为根的树的高度改变,需要更新parent祖先的平衡因子
					parent = parent->_parent;
					cur = cur->_parent;
				}
				else if (parent->_bf == 0)
				{
					//说明以parent为根的树的高度不变,不需要更新parent祖先的平衡因子,插入结点成功
					break;
				}
				else if (parent->_bf == 2 || parent->_bf == -2)
				{
					//说明parent的平衡因子违反AVL树的性质,需要对其进行旋转处理,使其符合AVL树性质

					break;
				}
				else
				{
					//如果不是上面的任何一种情况,也报错误。
					assert(false);
				}
			}
			return true;
		}

3、AVL树的旋转操作

3.1 左单旋

当新结点插入较高右子树的右侧时,即在c子树中插入新结点时就会引发30结点的平衡因子不符合AVL树性质。
在这里插入图片描述

如果出现上面的情况时,我们就可以使用左单旋操作来将以30结点为根结点的这棵二叉树变为AVL树。下面的图中画了左单旋的具体操作。
在这里插入图片描述

3.2 左单旋代码实现

下面我们来实现左单旋操作的代码。
在这里插入图片描述

我们将左单旋的代码这样实现,逻辑上是对的,但是因为我们定义AVL树的结点为三叉链,而下面的代码中没有维护每个结点的 _ parent指针,这显然是不行的。
在这里插入图片描述
下面我们将每个结点的 _ parent指针也进行更新,但是代码中还是存在问题。即当h == 0时,subRL为nullptr,然后代码中的subRL -> _ parent就会出现错误。并且在实际中,parent不一定为根结点,所以我们还需要提前设置一个pparent指针记录parent结点的父结点,然后判断parent结点是否为根结点,如果不是根结点就需要将subR为pparent的孩子,pparent为subR的父结点。并且我们还需要记得更新每个结点的平衡因子,我们看到左单旋的过程中,parent指向的结点和subR指向的结点的平衡因子都变为0了,而其它结点的平衡因子没有变化。
在这里插入图片描述
在这里插入图片描述
下面就是左单旋的完整代码,可以体会到左单旋的代码实现还是要注意很多细节的。

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

			parent->_right = subRL;
			//如果subRL不为nullptr,才将subRL指向的结点的_parent指针更新
			if (subRL != nullptr)
			{
				subRL->_parent = parent;
			}
			//提前记录parent指向结点的父结点的指针。
			Node* pparent = parent->_parent;

			subR->_left = parent;
			parent->_parent = subR;
			
			//判断parent是否为根结点,因为只有根结点没有父亲
			if (pparent == nullptr)
			{
				//如果parent为根结点,那么左单旋后subR为新的根结点
				_root = subR;
				_root->_parent = nullptr;
			}
			else
			{
				//parent不为根结点,那么就将subR更新为pparent的孩子
				//判断将subR为pparent的左孩子还是右孩子
				if (pparent->_left = parent)
				{
					pparent->_left = subR;
				}
				else
				{
					pparent->_right = subR;
				}
				subR->_parent = pparent;
			}

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

3.3 右单旋

下面我们再来分析右单旋的情况。当新节点插入较高左子树的左侧时,即在a子树中插入新结点时就会引发60结点的平衡因子不符合AVL树性质。
在这里插入图片描述
如果出现上面的情况时,我们就可以使用右单旋操作来将以60结点为根结点的这棵二叉树变为AVL树。下面的图中画了右单旋的具体操作。
在这里插入图片描述

3.4 右单旋代码实现。

前面我们已经实现了左单旋的代码,那么右单旋的代码实现和左单旋类似,我们一样需要注意上面的一些细节。
在这里插入图片描述

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

			parent->_left = subLR;
			//判断subLR是否为空结点,如果为空结点就不需要更新_parent指针
			if (subLR != nullptr)
			{
				subLR->_parent = parent;
			}
			//提前记录parent指向的结点的父结点指针
			Node* pparent = parent->_parent;

			subL->_right = parent;
			parent->_parent = subL;

			//我们换一个方法判断parent是否为根结点
			//如果parent为根结点,就将subL设置为新的根结点
			if (parent == _root)
			{
				_root = subL;
				_root->_parent = nullptr;
			}
			else
			{
				//判断subL应该作为pparent指向结点的左孩子还是右孩子
				if (pparent->_left == parent)
				{
					pparent->_left = subL;
				}
				else
				{
					pparent->_right = subL;
				}
				subL->_parent = pparent;  //更新subL_parent指针
			}
			//更新subL和parent结点的平衡因子
			subL->_bf = 0;
			parent->_bf = 0;
		}

3.5 什么时候调用左单旋和右单旋

我们已经实现了左单旋和右单旋的代码,那么我们应该在什么使用调用左单旋函数,什么时候调用右单旋函数呢?
当新节点插入较高右子树的右侧时,如下图所示的情况,我们此时调用左单旋。
在这里插入图片描述
当新节点插入较高左子树的左侧时,如下图所示的情况,我们此时调用右单旋。
在这里插入图片描述

代码中实现判断
在这里插入图片描述

3.6 左右双旋

下面我们再来分析左右双旋的情况。前面我们分析了当新节点插入较高左子树的左侧时,即在a子树中插入新结点时就会引发下图中90结点的平衡因子不符合AVL树性质,此时需要将30结点进行右单旋。而当我们将新节点插入较高左子树的右侧时,此时将30结点进行右单旋后并不能将这棵树变为AVL树。
在这里插入图片描述
如果出现上面的情况时,我们就需要使用左右双旋操作来将以90结点为根结点的这棵二叉树变为AVL树,即我们先将30结点左单旋,然后再将90结点右单旋。下面的图中画了左右双旋的具体操作。
在这里插入图片描述
从上面的过程中我们可以看到左右双旋其实就是让60结点去上面做根结点,然后60结点的左右孩子分别给30结点和90结点。

3.7 左右双旋代码实现

因为左右双旋分开的话其实就是第一步左单旋,第二步右单旋,所以我们可以复用前面写的左单旋和右单旋函数来实现左右双旋。
在这里插入图片描述

那么只需要这样就实现了左右双旋吗?
显然下面的代码不完整,因为我们通过上面的分析看到了左右双旋中最麻烦的其实是对结点的平衡因子进行更新,因为我们发现了这三个结点的平衡因子共出现了三种情况。
在这里插入图片描述
当在c子树中插入新结点时,此时subLR的平衡因子为1,左右双旋后三个结点的平衡因子分别为:

parent->_bf = 0
subL->_bf = -1
subLR->_bf = 0

在这里插入图片描述

当在b子树中插入新结点时,此时subLR的平衡因子为-1,左右双旋后三个结点的平衡因子分别为:

parent->_bf = 1
subL->_bf = 0
subLR->_bf = 0

在这里插入图片描述
然后还有最后一种情况,就是h为0时,此时60结点就是新插入的结点,subLR的平衡因子为0。左右双旋后三个结点的平衡因子分别为:

parent->_bf = 0
subL->_bf = 0
subLR->_bf = 0

在这里插入图片描述
下面我们来写代码实现。

//左右双旋
		void RotateLR(Node* parent)
		{
			Node* subL = parent->_left;
			Node* subLR = subL->_right;
			//用来判断平衡因子为哪种情况
			int bf = subLR->_bf;

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

			if (bf == 1)
			{
				parent->_bf = 0;
				subL->_bf = -1;
				subLR->_bf = 0
			}
			else if (bf == -1)
			{
				parent->_bf = 1;
				subL->_bf = 0;
				subLR->_bf = 0;
			}
			//我们不建议直接将bf==0的情况写在else里面,因为我们可以将else后使用一个断言,用来发现其它不可预知的情况
			//即判断走到了else时,说明程序出现了大问题,断言可以更好的帮我们检查出问题。
			else if (bf == 0)
			{
				parent->_bf = 0;
				subL->_bf = 0;
				subLR->_bf = 0;
			}
			else
			{
				assert(false);
			}
		}

3.8 右左双旋

我们前面分析了当新结点插入较高右子树的右侧时,即在d子树中插入新结点时就会引发30结点的平衡因子不符合AVL树性质,此时30结点可以通过左单旋来进行调整。而当我们将新节点插入较高右子树的左侧时,即在b或c子树中插入新结点,此时左单旋并不能将这棵子树调整为AVL树,此时就需要进行右左双旋操作来调整AVL树。
在这里插入图片描述
如果出现上面的情况时,我们就需要使用右左双旋操作来将以30结点为根结点的这棵二叉树变为AVL树,即我们先将90结点右单旋,然后再将30结点左单旋。下面的图中画了右左双旋的具体操作。(纠正:下图中的60结点右单旋都改为90结点右单旋)。
在这里插入图片描述

3.9 右左双旋代码实现

前面我们已经实现了左右双旋的代码,右左双旋的实现也是需要注意每个结点的平衡因子的更新。
在这里插入图片描述
当我们在b子树中插入新结点时,此时subRL的平衡因子为-1,左右双旋后三个结点的平衡因子分别为:

parent->_bf = 0
subR->_bf = 1
subRL->_bf = 0

在这里插入图片描述
当我们在c子树中插入新结点时,此时subRL的平衡因子为1,左右双旋后三个结点的平衡因子分别为:

parent->_bf = -1
subR->_bf = 0
subRL->_bf = 0

在这里插入图片描述

还有最后一种情况,就是h为0时,此时60结点就是新插入的结点,subRL的平衡因子为0。左右双旋后三个结点的平衡因子分别为:

parent->_bf = 0
subR->_bf = 0
subRL->_bf = 0

在这里插入图片描述
下面我们使用代码来实现右左双旋。

//右左双旋
		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;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else if (bf == -1)
			{
				parent->_bf = 0;
				subR->_bf = 1;
				subRL->_bf = 0;
			}
			else if (bf == 0)
			{
				parent->_bf = 0;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else
			{
				assert(false);
			}
		}

3.10 什么时候调用左右双旋和右左双旋

我们实现了左右双旋和右左双旋的代码后,下面我们就要分析在什么情况下会调用这两个函数了。
当新节点插入较高左子树的右侧时,如下图所示的情况,我们使用左右双旋,即先让30结点进行左单旋,然后让90结点进行右单旋。
在这里插入图片描述

当新节点插入较高右子树的左侧时,如下图所示的情况,我们使用右左双旋,即先让90结点进行右单旋,然后让30结点进行左单旋。
在这里插入图片描述

代码中实现判断
在这里插入图片描述

4、测试

上面我们就实现了一个AVL树,下面我们来进行测试。
我们知道AVL树是一棵平衡二叉搜索树,下面我们写一个中序遍历,然后我们创建一个AVL树,并且插入数据,然后中序遍历这棵AVL树,看输出的结果是否为升序。
在这里插入图片描述
我们看到输出的结果和我们预期的一样为升序,但是这样就可以判断我们实现的AVL树没有问题了吗?
这样的测试肯定是不够的,而且AVL树是一棵平衡二叉搜索树,这个结果只能说明我们实现了一棵二叉搜索树,这个二叉搜索树是否为平衡的,我们不能得出结论。
在这里插入图片描述
所以我们需要写一个方法来判断这棵二叉搜索树是否为一棵平衡树。我们知道平衡二叉搜索树的每个结点的平衡因子的绝对值不大于1,那么如果我们直接检查每个结点的平衡因子是否有绝对值大于1的,这个方法可以不可以呢?这样做也是不严谨的,因为平衡因子是我们自己维护并且更新的,如果我们的逻辑错误,那么可能造成平衡因子正确,但是二叉搜索树不平衡的情况发生。所以我们还是需要通过一一检查每个结点的左右子树的高度差是否大于1这样的方法来判断。
我们先写一个函数来求一棵树的高度。
在这里插入图片描述
然后我们再写一个函数递归判断每一个结点是否都满足左右子树高度差小于2,如果每一个结点都满足那么这棵树就是平衡二叉搜索树了。
在这里插入图片描述
但是有时候可能我们的逻辑错误,使AVL树没问题,但是平衡因子更新时出现了问题,虽然此时还是一棵AVL树,但是因为平衡因子不对,就会导致以后再使用时出现问题。所以我们可以在判断每个结点是否都满足平衡树的同时也判断每个结点的平衡因子是否都正确。这样我们的判断才严谨。并且这样处理也方便以后出问题了调试时我们可以更快的定位到错误。
在这里插入图片描述
下面我们将右左双旋中的平衡因子更新代码注释掉,我们可以看到结果中显示6结点的平衡因子异常,并且此时二叉搜索树也不是平衡二叉搜索树了。
在这里插入图片描述
在这里插入图片描述
6结点出现错误并不一定就是插入6结点时引起的错误,此时我们需要一个结点一个结点的去排查,但是这样排查也会很慢,此时我们可以每插入一个结点就使用IsBalance函数判断当前的二叉搜索树是否为平衡二叉搜索树。这样我们就很快的看到了是14结点插入时出现了问题。
在这里插入图片描述
此时我们就可以自己手动写一个判断条件,然后在判断条件里面打一个断点,这样来快速的还原错误bug现场。这样我们的调试效率就可以加快了。
在这里插入图片描述
下面我们将右左双旋更新平衡因子的代码恢复,然后我们写一个生成随机数的代码来再次测试我们实现的AVL是否还有问题。这样多执行几次,就基本将所有的场景都测试到了。
在这里插入图片描述

5、总结

AVL树插入结点引起的这棵树不平衡情况可以通过下面的图来记忆。
在这里插入图片描述

6、AVL树的性能

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

7、AVL树的结点删除

AVL树的结点删除后也可能会使这棵树变得不平衡,此时也需要通过旋转操作来重新使这棵树满足平衡二叉搜索树的性质。
与AVL树插入结点不同的是,AVL树中删除结点后,如果父结点平衡因子更新为1或-1时,此时不需要继续向上更新祖先结点的平衡因子,直接就是成功删除结点了。因为说明父结点之前的平衡因子为0,此时删除了结点后子树高度没有变化,就不会使AVL树变得不平衡了。
在这里插入图片描述

如果删除结点后,该结点的父结点平衡因子更新为0,那么就需要继续向上更新祖先结点的平衡因子,因为说明原来父结点的平衡因子为1或-1,删除结点后父结点的平衡因子才变为0。子树高度发生变化了。并且在向上继续更新祖先结点的平衡因子的时候,如果祖先结点的平衡因子不满足AVL树的要求,那么就需要通过旋转操作来将这棵树修正为平衡二叉搜索树。
在这里插入图片描述

8、代码

#pragma once

#include<iostream>
#include<map>
#include<utility>
#include<assert.h>
#include<utility>
#include<time.h>

using std::pair;
using std::cout;
using std::endl;


namespace dong
{
	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)
			{
				//先判断cur插入到parent的左边或右边,然后更新parent的平衡因子
				if (cur == parent->_right)
				{
					parent->_bf++;
				}
				else
				{
					parent->_bf--;
				}
				//判断更新后parent的平衡因子为多少,是否还需要继续向上更新祖先的平衡因子
				if (parent->_bf == 1 || parent->_bf == -1)
				{
					//说明以parent为根的树的高度改变,需要更新parent祖先的平衡因子
					parent = parent->_parent;
					cur = cur->_parent;
				}
				else if (parent->_bf == 0)
				{
					//说明以parent为根的树的高度不变,不需要更新parent祖先的平衡因子,插入结点成功
					break;
				}
				else if (parent->_bf == 2 || parent->_bf == -2)
				{
					//说明parent的平衡因子违反AVL树的性质,需要对其进行旋转处理,使其符合AVL树性质
					//需要旋转处理: 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);
					}
					else
					{
						assert(false);
					}
					break;
				}
				else
				{
					//如果不是上面的任何一种情况,也报错误。
					assert(false);
				}
			}
			return true;
		}
		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		bool IsBalance()
		{
			return _IsBalance(_root);
		}
		
	protected:
		//左单旋
		void RotateL(Node* parent)
		{
			Node* subR = parent->_right;
			Node* subRL = subR->_left;

			parent->_right = subRL;
			//如果subRL不为nullptr,才将subRL指向的结点的_parent指针更新
			if (subRL != nullptr)
			{
				subRL->_parent = parent;
			}
			//提前记录parent指向结点的父结点的指针。
			Node* pparent = parent->_parent;

			subR->_left = parent;
			parent->_parent = subR;
			
			//判断parent是否为根结点,因为只有根结点没有父亲
			if (pparent == nullptr)
			{
				//如果parent为根结点,那么左单旋后subR为新的根结点
				_root = subR;
				_root->_parent = nullptr;
			}
			else
			{
				//parent不为根结点,那么就将subR更新为pparent的孩子
				//判断将subR为pparent的左孩子还是右孩子
				if (pparent->_left == parent)
				{
					pparent->_left = subR;
				}
				else
				{
					pparent->_right = subR;
				}
				subR->_parent = pparent;
			}

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

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

			parent->_left = subLR;
			//判断subLR是否为空结点,如果为空结点就不需要更新_parent指针
			if (subLR != nullptr)
			{
				subLR->_parent = parent;
			}
			//提前记录parent指向的结点的父结点指针
			Node* pparent = parent->_parent;

			subL->_right = parent;
			parent->_parent = subL;

			//我们换一个方法判断parent是否为根结点
			//如果parent为根结点,就将subL设置为新的根结点
			if (parent == _root)
			{
				_root = subL;
				_root->_parent = nullptr;
			}
			else
			{
				//判断subL应该作为pparent指向结点的左孩子还是右孩子
				if (pparent->_left == parent)
				{
					pparent->_left = subL;
				}
				else
				{
					pparent->_right = subL;
				}
				subL->_parent = pparent;  //更新subL_parent指针
			}
			//更新subL和parent结点的平衡因子
			subL->_bf = 0;
			parent->_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;
				subL->_bf = -1;
				subLR->_bf = 0;
			}
			else if (bf == -1)
			{
				parent->_bf = 1;
				subL->_bf = 0;
				subLR->_bf = 0;
			}
			//我们不建议直接将bf==0的情况写在else里面,因为我们可以将else后使用一个断言,用来发现其它不可预知的情况
			//即判断走到了else时,说明程序出现了大问题,断言可以更好的帮我们检查出问题。
			else if (bf == 0)
			{
				parent->_bf = 0;
				subL->_bf = 0;
				subLR->_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;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else if (bf == -1)
			{
				parent->_bf = 0;
				subR->_bf = 1;
				subRL->_bf = 0;
			}
			else if (bf == 0)
			{
				parent->_bf = 0;
				subR->_bf = 0;
				subRL->_bf = 0;
			}
			else
			{
				assert(false);
			}
		}

		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_InOrder(root->_left);
			cout << root->_kv.first << " ";
			_InOrder(root->_right);
		}
		
		//得到一棵树的高度
		int _Height(Node* root)
		{
			if (root == nullptr)
			{
				return 0;
			}
			int leftH = _Height(root->_left);
			int rightH = _Height(root->_right);
			return leftH > rightH ? leftH + 1 : rightH + 1;
		}

		bool _IsBalance(Node* root)
		{
			//如果是空树则也为平衡树
			if (root == nullptr)
			{
				return true;
			}
			//求结点的左子树高度
			int leftH = _Height(root->_left);
			//求结点的右子树高度
			int rightH = _Height(root->_right);
			//判断平衡因子是否正确
			if (rightH - leftH != root->_bf)
			{
				cout << root->_kv.first << "结点平衡因子异常" << endl;
				return false;
			}

			//每一个结点都检查是否满足平衡树
			return abs(leftH - rightH) < 2
				&& _IsBalance(root->_left)
				&& _IsBalance(root->_right);
		}
		

	private:
		Node* _root = nullptr;
	};
}

void Test01()
{
	//int a[] = { 16,3,7,11,9,26,18,14,15 };
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	dong::AVLTree<int, int> t1;
	for (auto e : a)
	{
		if (e == 14)
		{
			//只是用来打断点用
			int x = 0;
		}
		t1.Insert(std::make_pair(e,e));
		cout << e << "插入:" << t1.IsBalance() << endl;
	}
	t1.InOrder();
	cout << t1.IsBalance() << endl;
}

void Test02()
{
	srand(time(0));
	const size_t N = 100000;
	dong::AVLTree<int, int> t;
	//生成随机数插入到AVL树中
	for (size_t i = 0; i < N; ++i)
	{
		size_t x = rand();
		t.Insert(std::make_pair(x, x));
	}
	//看最后是否还满足AVL树
	cout << t.IsBalance() << endl;
}

int main()
{
	//Test01();
	Test02();


	return 0;
}

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

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

相关文章

【C++】C++11【上】列表初始化|声明|新容器|右值引用|完美转发|新的类功能

目录 1、 C11简介 2、 统一的列表初始化 2.1 &#xff5b;&#xff5d;初始化 2.2 std::initializer_list 3、声明 3.1 auto和范围for 3.1decltype 3.3 nullptr 4、新容器 5、 右值引用 5.1左值引用和右值引用 5.2 左值引用与右值引用比较 5.3 左值和右值引用使用场景及意义 6…

怎么让重要文件自动备份到OneDrive?

可以让文件自动备份到OneDrive吗&#xff1f; OneDrive是比较受欢迎的云存储之一&#xff0c;主要用于存储文件和个人数据&#xff0c;随时随地都能够在多个设备&#xff08;例如Android、台式机或笔记本电脑、平板电脑等&#xff09;之间同步和共享文件。 因此&…

Python实验五 异常处理

实验 1&#xff1a;为下列代码添加异常处理。 xint(input(请输入一个整数)) print(100/x) # 实验 1&#xff1a;为下列代码添加异常处理。 try:xint(input(请输入一个整数&#xff1a;))print(100/x) except ValueError:print(请输入一个整数) except ZeroDivisionError:print…

操作系统 day05(体系结构、开机过程、虚拟机)

一&#xff0c;操作系统的体系结构 内核 内核是操作系统最基本、最核心的部分&#xff0c;实现操作系统内核功能的那些程序就是内核程序这其中&#xff1a;时钟、中断、原语是与硬件关联最紧密的模块&#xff0c;而进程管理、存储器管理、设备管理更多的是对数据结构的操作&…

【Midjourney入门教程1】Midjourney的注册、订阅

文章目录 前言一、Midjourney是什么二、Midjourney注册三、新建自己的服务器四、开通订阅 前言 AI绘画即指人工智能绘画&#xff0c;是一种计算机生成绘画的方式。是AIGC应用领域内的一大分支。 AI绘画主要分为两个部分&#xff0c;一个是对图像的分析与判断&#xff0c;即“…

【Redis】的简介和安装配置(Linux和windows)及操作命令

目录 一、概述 1.介绍 2.特点 二、安配 1. 安装 2. 配置 3. 主机连接 1.Linux连接 2.windows连接 三、命令 1. 字符串(String) 2. 哈希(Hash) 3. 列表&#xff08;List&#xff09; 4. 集合&#xff08;Set&#xff09; 一、概述 1.介绍 Redis是一个开源的、基…

EViews| 基础操作 备战下周机考

目录 一、创建工作文件 1、非时间序列数据 2、时间序列数据 二、导入数据 1、导入数据 2、保存数据组合或方程结果 三、估计回归模型 1、估计回归模型 2、回归结果名词解读 四、检验模型设定错误 1、检验是否遗漏变量 2-1、检验是否加入了不相干变量 2-2、惩罚新增…

2023年最新版潮乎盲盒源码含搭建教程

后台开发语言&#xff1a;后端 Laravel 框架开发 前端开发框架&#xff1a;uniappvue 环境配置: php7.4 mysql5.6 nginx1.22 redis&#xff08;建议宝塔面板或 lnmp&#xff09; 源码获取请自行百度&#xff1a;一生相随博客 一生相随博客致力于分享全网优质资源&#x…

大数据与健康:技术助力医疗卫生事业腾飞

大数据与健康&#xff1a;技术助力医疗卫生事业腾飞 随着科技的飞速发展&#xff0c;大数据技术已经渗透到我们生活的方方面面&#xff0c;包括医疗卫生领域。本文将对大数据在健康医疗领域的应用进行分析&#xff0c;并通过数据图表展示其发展趋势和前景。 一、背景介绍 近…

IPv4首部格式

IPv4首部格式 IPv4数据报的首部格式及其内容是实现IPv4协议各种功能的基础。 在TCPIP标准中&#xff0c;各种数据格式常常以32比特(即4字节)为单位来描述。 IPv4首部格式图 ## IPv4数据报的组成 主要由固定部分(20字节)可变部分(最大40字节) - 固定部分是指每个IPv4数据报都必…

Jmeter调用测试片段 —— 模块控制器

可以使用模块控制器调用测试片段。模块控制器提供了一种在运行时将测试片段替换为当前测试计划的机制。测试片段可以位于任何线程组中。 1、打开一个Jmeter窗口&#xff0c;添加好线程组、用户定义变量、模块控制器、测试片段、察看结果树。 2、用户定义变量同样定义好访问ip及…

ETHERNET/IP转RS485/RS232自由协议网关连接AB系统的简单配置方法

想将ETHERNET/IP网络和RS485/RS232总线连接起来吗&#xff1f;来看这款通讯网关&#xff01; 捷米特JM-EIP-RS485/232是一款通讯网关&#xff0c;它具有将ETHERNET/IP网络和RS485/RS232总线连接起来的功能。作为ETHERNET/IP网络的从站&#xff0c;它可以连接AB、欧姆龙等品牌的…

游戏反Xposed框架解决方案

在游戏安全对抗过程中&#xff0c;除了常见的内存修改、加速、破解等作弊手段&#xff0c;还有一类危害严重的外挂——「注入挂」。 据FairGuard游戏安全数据统计&#xff0c;在游戏面临的众多安全风险中&#xff0c;注入挂的占比高达17% 。如此高的占比&#xff0c;可见注入挂…

软件测试工程师必须掌握的Linux常用命令

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

msvcp120.dll下载修复详细方法与解决方法

MSVCP120.dll文件丢失是一个常见的问题&#xff0c;它通常会导致某些程序无法正常运行。这个问题可能是由于多种原因引起的&#xff0c;例如系统更新、软件卸载或病毒感染等。在这篇文章中&#xff0c;我将为您提供四种解决MSVCP120.dll文件丢失的方法&#xff0c;帮助您快速恢…

【Redis】入门篇--安装以及常用命令

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Redis的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Redis是什么 二. Redis有什么优势 三…

一秒文件搜索神器—Everything,结合内网穿透成为在线搜索神器!

Everythingcpolar搭建在线资料库&#xff0c;实现随时随地访问 文章目录 Everythingcpolar搭建在线资料库&#xff0c;实现随时随地访问前言1.软件安装完成后&#xff0c;打开Everything2.登录cpolar官网 设置空白数据隧道3.将空白数据隧道与本地Everything软件结合起来总结 前…

快速灵敏的 Flink1

一、flink单机安装 1、解压 tar -zxvf ./flink-1.13.2-bin-scala_2.12.tgz -C /opt/soft/ 2、改名字 mv ./flink-1.13.2/ ./flink1132 3、profile配置 #FLINK export FLINK_HOME/opt/soft/flink1132 export PATH$FLINK_HOME/bin:$PATH 4、查看版本 flink --version 5、…

[GitLab] 安装Git 指定版本

卸载旧版本 检查是否已经安装 git --version如果已经安装&#xff0c;先卸载 yum -y remove git安装新版本 在GitHub上选择需要下载的版本 Git版本 在/usr/local/目录下新建文件夹&#xff1a;git&#xff0c;并在/usr/local/git/文件夹内下载压缩包 wget https://github…

llava1.5模型安装、预测、训练详细教程

引言 本博客介绍LLava1.5多模态大模型的安装教程、训练教程、预测教程&#xff0c;也会涉及到hugging face使用与wandb使用。 源码链接:点击这里 demo链接:点击这里 论文链接:点击这里 一、系统环境 ubuntu 20.04 gpu: 2*3090 cuda:11.6 二、LLava环境安装 1、代码下载…