【C++】AVL树实现

1. AVL树的概念

  • AVL树是最先发明的⾃平衡⼆叉查找树,AVL是⼀棵空树,或者具备下列性质的⼆叉搜索树:它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树,通过控制高度差去控制平衡。
  • AVL树实现这里我们引入⼀个平衡因子(balance factor)的概念,每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于0/1/-1,AVL树并不是必须要平衡因子,但是有了平衡因子可以更⽅便我们去进⾏观察和控制树是否平衡,就像⼀个风向标⼀样。
  • 思考⼀下为什么AVL树是高度平衡搜索⼆叉树,要求高度差不超过1,⽽不是高度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,⽽是有些情况是做不到⾼度差是0的。比如⼀棵树是2个结点,4个结点等情况下,⾼度差最好就是1,⽆法做到⾼度差是0
  • AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 logN ,那么增删查改的效率也可以控制在 O(logN) ,相⽐⼆叉搜索树有了本质的提升。

一般完全二叉树的结点数是N=2^h-1,2^h-常数=N。

2. AVL树的实现

2.1 AVL树的结构

cpp 复制代码
template<class K, class V>
struct AVLTreeNode
{
	// 需要parent指针,后续更新平衡因子可以看到
	pair<K, V> _kv;  // 结点
	AVLTreeNode<K, V>* _parent;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	int _bf;  // 平衡因子

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

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

private:
	Node* _root = nullptr;
};

2.2 AVL树的插入

2.2.1 AVL树插入⼀个值的大概过程

  1. 插入⼀个值按⼆叉搜索树规则进⾏插入
  2. 新增结点以后,只会影响祖先结点的⾼度,也就是可能会影响部分祖先结点的平衡因⼦,所以更新从新增结点->根结点路径上的平衡因⼦,实际中最坏情况下要更新到根,有些情况更新到中间就可以停⽌了,具体情况我们下⾯再详细分析。
  3. 更新平衡因子过程中没有出现问题,则插入结束
  4. 更新平衡因子过程中出现不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上⼀层,所以插⼊结束。

2.2.2 平衡因子更新

更新原则:

  • 平衡因⼦ = 右⼦树⾼度-左⼦树⾼度
  • 只有⼦树⾼度变化才会影响当前结点平衡因⼦
  • 插⼊结点,会增加⾼度,所以新增结点在parent的右⼦树,parent的平衡因⼦++,新增结点在
    parent的左⼦树,parent平衡因⼦--
  • parent所在⼦树的⾼度是否变化决定了是否会继续往上更新

更新停⽌条件:

  • 更新后parent的平衡因⼦等于0,更新中parent的平衡因⼦变化为-1->0 或者 1->0,说明更新前parent⼦树⼀边⾼⼀边低,新增的结点插⼊在低的那边,插⼊后parent所在的⼦树⾼度不变,不会影响parent的⽗亲结点的平衡因⼦,更新结束。
  • 更新后parent的平衡因⼦等于1 或 -1,更新前更新中parent的平衡因⼦变化为0->1 或者 0->-1,说明更新前parent⼦树两边⼀样⾼,新增的插⼊结点后,parent所在的⼦树⼀边⾼⼀边低,parent所在的⼦树符合平衡要求,但是⾼度增加了1,会影响parent的⽗亲结点的平衡因⼦,所以要继续向上更新。
  • 更新后parent的平衡因⼦等于2 或 -2,更新前更新中parent的平衡因⼦变化为1->2 或者 -1->-2,说明更新前parent⼦树⼀边⾼⼀边低,新增的插⼊结点在高的那边,parent所在的⼦树⾼的那边更高了,破坏了平衡,parent所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:1、把parent⼦树旋转平衡。2、降低parent⼦树的高度,恢复到插入结点以前的⾼度。所以旋转后也不需要继续往上更新,插入结束。
  • 不断更新,更新到根,跟的平衡因⼦是1或-1也停⽌了。

如下图所示:更新到10结点,平衡因⼦为2,10所在的⼦树已经不平衡,需要旋转处理

更新到中间结点,3为根的⼦树⾼度不变,不会影响上⼀层,更新结束

最坏更新到根停止

2.2.3 插入结点及更新平衡因子的代码实现

代码实现:

cpp 复制代码
	bool insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		else
		{
			// 找到要插入的结点位置;
			Node* cur = _root;
			Node* parent = nullptr;
			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 (kv.first > parent->_kv.first)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			cur->_parent = parent;

			// 控制平衡因子
			// 更新平衡因子
			while (parent)
			{
				if (cur == parent._left)
				{
					parent->_bf--;
				}
				else 
				{
					parent->_bf++;
				}

				if (parent._bf == 0)
				{
					// parent所在的子树高度不变,不会再影响上一层,更新结束
					break;
				}
				else if (parent->_bf == -1 || parent->_bf == 1)
				{
					// parent所在的子树高度变了,会再影响上一层,继续往上更新
					cur = parent;
					parent = parent->_parent;
				}
				else if (parent->_bf == 2 || parent->_bf == -2)
				{
					// parent所在的子树已经不平衡了,需要旋转处理
					break;
				}
				else
				{
					assert(false);
				}
			}
			return true;	
		}
	}

2.3 旋转

2.3.1 旋转的原则

  1. 保持搜索树的规则
  2. 让旋转的树从不平衡变平衡,其次降低旋转树的高度

旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。
说明:下⾯的图中,有些结点我们给的是具体值,如10和5等结点,这⾥是为了⽅便讲解,实际中是什么值都可以,只要大小关系符合搜索树的性质即可。

2.3.2 右单旋

  • 下图展示的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要
    求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰,他代表了所有右单旋的场景,实际右单旋形态有很多种。
  • 在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从-1变成-2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树左边太⾼了,需要往右边旋转,控制两棵树的平衡。
  • 旋转核心步骤,因为5 < b⼦树的值 < 10,将b变成10的左⼦树,10变成5的右⼦树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

代码实现:

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

			// 父节点的左孩子变为subLR
			parent->_left = subLR;
			if (subLR)
			{
				subLR->_parent = parent;  // 防止出现特殊情况:subLR为空
			}

			//记录原来的父节点的父节点
			Node* parentParent = parent->_parent;

			// subL代替原来父节点的位置,原来的父节点变为左孩子
			subL->_right = parent;
			parent->_parent = subL;

			if (parent == _root)
			{
				subL->_parent = nullptr;
				_root = subL;
			}
			else
			{
				if (parentParent->_left == parent)
				{
					parentParent->_left = subL;
				}
				else(parentParent->_right = parent)
				{
					parentParent->_right = subL;
				}
				sub->_parent = parentParent;
			}
			
			// 更新平衡因子
			parent->_bf = subL->_bf = 0;
		}

2.3.3 左单旋

  • 下图展示的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要
    求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体跟上⾯右旋类似。
  • 在a⼦树中插入⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从1变成2,10为根的树左右高度差超过1,违反平衡规则。10为根的树右边太高了,需要往左边旋转,控制两棵树的平衡。
  • 旋转核心步骤,因为10 < b⼦树的值 < 15,将b变成10的右⼦树,10变成15的左⼦树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

代码实现:

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

			// 父节点的右孩子变为subRL结点
			parent->_right = subRL;
			if (subRL)
				subRL->_parent = parent;   // 防止出现特殊情况:subRL为空
			
			// 记录父节点的父节点
			Node* parentParent = parent->_parent; 

			// 原来的父节点变为subR的左孩子
			subR->_left = parent;
			parent->_parent = subR;

			if (parent == _root)
			{	
				_root = subR;
				subR->parent = nullptr;
			}
			else
			{
				if (parentParent->_left == parent)
				{
					parentParent->_left = subR;
				}
				else
				{
					parentParent->_right = subR;
				}
				subR->_parent = parentParent;
			}

			// 平衡因子的更新
			parent->_bf = subR->_bf = 0;

		}

2.3.4 左右双旋

通过下两张图可以看到,左边⾼时,如果插⼊位置不是在a⼦树,⽽是插⼊在b⼦树,b⼦树⾼度从h变成h+1,引发旋转,右单旋⽆法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边⾼,但是插⼊在b⼦树中,10为跟的⼦树不再是单纯的左边⾼,对于10是左边⾼,但是对于5是右边⾼,需要⽤两次旋转才能解决,以5为旋转点进⾏⼀个左单旋,以10为旋转点进⾏⼀个右单旋,这棵树这棵树就平衡了。

双旋,不是纯粹的一边高,上面是单旋处理方式,可以看到还是不平衡

上图分别为左右双旋中h==0和h==1具体场景分析,下⾯我们将a/b/c⼦树抽象为⾼度h的AVL

⼦树进⾏分析,另外我们需要把b子树的细节进⼀步展开为8和左⼦树⾼度为h-1的e和f⼦树,因为

我们要对b的⽗亲5为旋转点进⾏左单旋,左单旋需要动b树中的左⼦树。b⼦树中新增结点的位置

不同,平衡因⼦更新的细节也不同,通过观察8的平衡因⼦不同,这⾥我们要分三个场景讨论。

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

代码实现:

cpp 复制代码
	// 左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		int bf = subLR->_bf;

		RotateL(subL);
		RotateR(parent);

		// 平衡因子的调节
		if (bf == -1)
		{
			subL->_bf = subLR->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			parent->_bf = subLR->_bf = 0;
			subL->_bf = -1;
		}
		else if(bf == 0)
		{
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

2.3.5 右左双旋

跟左右双旋类似,下⾯我们将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 == -1)
		{
			subR->_bf = 1;
			subRL->_bf = parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = subRL->_bf = 0;
			parent->_bf = -1;
		}
		else if(bf == 0)
		{
			subR->_bf = subRL->_bf = parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

2.4 AVL树的查找

那⼆叉搜索树逻辑实现即可,搜索效率为 O(logN)

cpp 复制代码
	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

2.5 AVL树的平衡检测

我们实现的AVL树是否合格,我们通过检查左右⼦树高度差的的程序进⾏反向验证,同时检查⼀下结点的平衡因⼦更新是否出现了问题。

cpp 复制代码
	// 求高度
	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int lh = _Height(root->_left);
		int rh = _Height(root->_right) + 1;
		return lh > rh ? lh + 1: rh + 1;
	}


	// 实现要写成递归,IsBalance
	// 递归计算平衡因子绝对值是否<=2

	bool _IsBalanceTree(Node* root)
	{
		// 空树也是平衡因子
		if (root == nullptr)
		{
			return true;
		}

		// 计算Root结点的平衡因子:即Root左右子树的高度差 
		// 求出左右子树的高度
		int lh = _Height(root->_left);
		int rh = _Height(root->_right);

		// 如果计算出的平衡因子与Root的平衡因子不相等,或者 
		// Root平衡因子的绝对值超过2,则一定不是AVL树 
		if (lh - rh != root->_bf || abs(root->_bf) >= 2)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		// 递归检测左子树和右子树
		return _IsBalanceTree(root->_left) &&  _IsBalanceTree(root->_right);

	}

测试代码,Test.cpp

cpp 复制代码
void TestAVLTree1()
{
	AVLTree<int, int> t;

	// 常规的测试例 
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	
	// 特殊的带有双旋场景的测试用例 
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

	for (auto e : a)
	{
		t.Insert({ e, e });
		cout << e << "->" << t.IsBalanceTree() << endl;
	}

	cout << t.IsBalanceTree() << endl;

}


int main()
{
	TestAVLTree1();
	return 0;
}

检测性能

cpp 复制代码
// 插入一堆随机值,测试平衡,顺便测试一下高度和性能
void TestAVLTree2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}
	size_t begin2 = clock();
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	size_t end2 = clock();
	cout << "Insert:" << end2 - begin2 << endl;
	cout << t.IsBalanceTree() << endl;

	cout << "Height:" << t.Height() << endl;
	cout << "Size:" << t.Size() << endl;
	size_t begin1 = clock();
	// 确定在的值 
	/*for (auto e : v)
	{
	t.Find(e);
	}*/
	// 随机值 
	for (size_t i = 0; i < N; i++)
	{
		t.Find((rand() + i));
	}
	size_t end1 = clock();
	cout << "Find:" << end1 - begin1 << endl;
}


int main()
{
	//TestAVLTree1();
	TestAVLTree2();
	return 0;
}
相关推荐
wjs20247 小时前
DOM CDATA
开发语言
Tingjct7 小时前
【初阶数据结构-二叉树】
c语言·开发语言·数据结构·算法
猷咪7 小时前
C++基础
开发语言·c++
IT·小灰灰7 小时前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧7 小时前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q7 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳07 小时前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾7 小时前
php 对接deepseek
android·开发语言·php
CSDN_RTKLIB7 小时前
WideCharToMultiByte与T2A
c++
2601_949868367 小时前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter