【C++】 AVL树学习:严格平衡二叉搜索树

📚 博主的专栏

  🐧 Linux   |   🖥️ C++   |   📊 数据结构  | 💡C++ 算法 | 🅒 C 语言  | 🌐 计算机网络

下一篇:红黑树

目录

AVL树的概念

AVL树的实现

插入新节点,调整节点平衡因子

AVL树的旋转

1.(左边高) 新节点插入较高左子树的左侧---左左:右单旋

2. 新节点插入较高右子树的右侧---右右:左单旋

3.左右双旋

请注意:这里一共有3种情况

4.右左双旋:

判断AVL树是否平衡?算树的左右高度差是否在范围内:

大型项目调试小技巧:

AVL树的删除--了解

AVL树的性能


AVL树的概念

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

 例如:在有2/4个节点的时候,做不到左右高度差相等

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

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

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

平衡因子 = 右子树高度 - 左子树高度

1.按照搜索树规则插入

2.更新插入节点的祖先节点的平衡因子

  • a、插入父亲的左边,父亲平衡因子--
  • b、插入父亲的右边,父亲平衡因子++

是否需要更新祖先的平衡因子?

  • c、需要更新:父亲的平衡因子 == 1 or -1,父亲所在的子树高度改变,需要向上更新,因为对于祖先来说,父亲所在子树的高度变化,插入结束。
  • d、不需要更新:父亲的平衡因子 == 0,父亲所在的子树高度不变,不需要向上更新,插入结束        
  • e、父亲平衡因子 == 2 or -2,父亲所在的子树已经不平衡了,需要旋转处理

如图:

AVL树的实现

这里是将之前所讲过的搜索二叉树KV版本直接拿过来修改,这是修改过后:未旋转版

插入新节点,调整节点平衡因子

1. 按照二叉搜索树的方式插入新节点

2. 调整节点的平衡因子

#include<assert.h>
#include<iostream>
#include<string>
using namespace std;
namespace AVL
{

	template<class K, class V>
	struct AVLTreeNode
	{
		//在K_V添加了parent
		AVLTreeNode<K, V>* _left;
		AVLTreeNode<K, V>* _right;
		AVLTreeNode<K, V>* _parent;

		int _bf = 0; //balance factor

		pair<K, V> _kv;

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

	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; //往右节点走之前,先存下parent
					cur = cur->_right;
				}
				else if (cur->_kv.first > kv.first)
				{
					parent = cur;

					cur = cur->_left;
				}
				else//默认定义(90%的情况下),搜索树不允许冗余,因此若值相等就不插入 
				{
					return false;
				}
			}
			cur = new Node(kv);
			if (parent->_kv.first < 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) {
					break; // 子树高度未变,停止更新
				}
				else if (parent->_bf == 1 || parent->_bf == -1) {
					cur = parent;// 向上移动指针,保证在下一次循环的时候,更新到父节点的平衡因子
					parent = parent->_parent;
				}
				else if (parent->_bf == 2 || parent->_bf == -2) {
					// 需要旋转(未实现)




					break;
				}
				else {
					assert(false); // 平衡因子异常
				}
			}
			return true;
		}

		Node* Find(const pair<K, V>& kv)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_kv.first > kv.first)
				{
					cur = cur->_left;
				}
				else if (cur->_kv.first < kv.first)
				{
					cur = cur->_right;
				}
				else
				{
					return cur;
				}
			}
			return nullptr;
		}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
	private:
		void _InOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_InOrder(root->_left);
			cout << root->_kv.first << ":" << root->_kv.second << " ";
			_InOrder(root->_right);
		}

		Node* _root = nullptr;
	};
	inline void TestAVLTree1()
	{
		int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
		AVLTree<int, int> t1;
		for (auto e : a)
		{
			t1.Insert({e, e});
		}
		t1.InOrder();
	                
	}
}

AVL树的旋转

假设 h == 0,插入一个新节点

h == 1

h == 2 的 AVL树有三种    a一定是z(注意是需要旋转)

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:


1.(左边高) 新节点插入较高左子树的左侧---左左:右单旋

让b为60的左边,60为30的右边

首先左子树小于根节点小于右子树,右子树大于根节点大于左子树。

b > 30,b < 60,c > 60,60 > 30,因此让b去做60的左子树,60去做30的右子树不违和

代码编写:

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

			parent->_left = subLR;
			//还需要更改subLR父亲,但是有可能subLR为空
			if(subLR) subLR->_parent = parent;

			subL -> _right = parent;
			Node* ppNode = parent->_parent;
			parent->_parent = subL;
			//有可能parent就是这棵树的根节点;或者是这棵树的子树,就需要找到ppNode
			if (parent == _root)
			{
				_root = subL;
				_root->_parent = nullptr;
			}
			else
			{
				//先判断parent是父节点的左子树还是右
				if (ppNode->_left == parent)
				{
					ppNode->_left = subL;
				}
				else
				{
					ppNode->_right = subL;
				}
				subL->_parent = ppNode;
			}
			//再更改parent的平衡因子
			parent->_bf = subL->_bf = 0;
		}

2. 新节点插入较高右子树的右侧---右右:左单旋

30<b<60<c 仍然遵循搜索树

同理 

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

			parent->_right = subRL;
			//还需要更改subRL父亲,但是有可能subLR为空
			if (subRL) subRL->_parent = parent;

			subR -> _left = parent;
			Node* ppNode = parent->_parent;
			parent->_parent = subR;
			//有可能parent就是这棵树的根节点;或者是这棵树的子树,就需要找到ppNode
			if (parent == _root)
			{
				_root = subR;
				_root->_parent = nullptr;
			}
			else
			{
				//先判断parent是父节点的左子树还是右
				if (ppNode->_left == parent)
				{
					ppNode->_left = subR;
				}
				else
				{
					ppNode->_right = subR;
				}
				subR->_parent = ppNode;
			}
			//再更改parent的平衡因子
			parent->_bf = subR ->_bf = 0;
		}

如何调用右旋,左旋,修改insert,根据平衡因子

                else if (parent->_bf == 2 || parent->_bf == -2) {
				// 需要旋转(未实现)
				//根据平衡因子判断左右单旋
				if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);

				}
				break;
}

注意:旋转完了就不用向上再更新了,因为此时这个子树已经平衡了,对于整个二叉树来说,高度没有变化


3.左右双旋

新节点插入较高左子树的右侧---左右:先左单旋再右单旋

h = 0        这里的60是插入的节点

h = 1        可以在60的左右分别插入节点

如图先以30为旋转点,进行左单旋,60成为30的父节点,对于90来说,整棵树变成单纯的左边高

再以90为旋转点,进行右单旋,让60成为90的父节点。(30 < b < 60 < c < 90)

请注意:这里一共有3种情况

1.60自己新增,它的初始是0,旋转后平衡因子都为0,也就是这种情况:

2.60的平衡因子是-1,就代表b插入

3.60的平衡因子是1,就代表c插入

场景一:抽象图在b插入     

场景二:抽象图在c插入

 如图我们以h == 1的示例具体图来模拟这个旋转

void RotateLR(Node* parent)  //左右双旋 --parent的平衡因子是-2,代表左边高,parent->_left是1,
									//因此先左旋parent->_left,再右旋parent
{
	//先对cur进行左单旋,也就是parent的left
	RotateL(parent->_left);
	RotateR(parent);
}

但是在单旋函数内部会将父节点以及父节点的left或者right的平衡都改为0

因此需要提前记录节点,并在双旋函数中再对次进行修改,根据以上的三种情况根据subLR的平衡因子,看是在subLR的左插入还是右

Node* subL = parent->_left;
Node* subLR = subL->_right;

int bf = subLR->_bf;  //用于区分三种情况,自己是新增,在subLR左边新增or右边新增

再由subLR的平衡因子来分情况讨论

 			if (bf == -1)//在subLR的左边插入,最终情况就是,subLR的左给SubL,parent缺左
			{
				subLR->_bf = 0;
				subL->_bf = 0;
				parent->_bf = 1;
			}
			else if(bf == 1)//在subLR的右边插入,最终情况,subLR的右给parent的左,subL缺右。
			{
				subLR->_bf = 0;
				subL->_bf = -1;
				parent->_bf = 0;
			}
			else if (bf == 0)//subLR自己是新增,他的左右为空因此,旋转过后,高度不变
			{
				subLR->_bf = 0;
				subL->_bf = 0;
				parent->_bf = 0;
			}
			else
			{
				assert(false);
			}

4.右左双旋:

可以发现只靠左单旋右单旋,无法让整个子树变成AVL树,并且循环仿佛,也不符合单旋的要求

如何判断?可观察单旋的平衡因此的符号都相同,而双旋的的平衡因子是相反数,高的方向出现了折现就是双旋,高的方向是一个直线就是单旋,也可以观察图的区别 

经过这个单旋,变成单纯的右边高,因此再使用一个左单旋,就可以平衡

与左右双旋同理:

void RotateRL(Node* parent)  //右左双旋 --parent的平衡因子是2,代表右子树更高,(parent->_right是-1
							//,代表左边高,因此先右旋(parent->_right,再左旋parent
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	RotateR(parent->_right);
	RotateL(parent);
	//..
	if (bf == 1)  //在subRL的右边插入,最终情况,subRL的右给subR,subRL的左给parent的右。parent右还缺一个
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		parent->_bf = -1;
	}
	else if(bf == -1)
	{
		subRL->_bf = 0;
		subR->_bf = 1;
		parent->_bf = 0;
	}
	else if (bf == 0)
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}

}

判断AVL树是否平衡?算树的左右高度差是否在范围内:

并且还要判断平衡因子是否正确

		int _Height(Node* root)
		{
			if (root == nullptr)
				return 0;

			return max(_Height(root->_left), _Height(root->_right)) + 1;
		}
		bool _IsBalance(Node* root)
		{
			//判断当前树
			if (root == nullptr)	
				return true;
			int leftHeight = _Height(root->_left);
			int rightHeight = _Height(root->_right);
			//不平衡
			if (abs(leftHeight - rightHeight) >= 2)
			{
				return false;
			}
			//顺带检查平衡因子是否正确
			if (rightHeight - leftHeight != root->_bf)
			{
				cout << root->_kv.first << endl;
				return false;
			}

			return _IsBalance(root->_left) 
				&& _IsBalance(root->_right);
		}
大型项目调试小技巧:

1.先看是插入谁导致出现的问题

2.打条件断点,画出插入前的树

3.单步跟踪,对比图———分析细节原因

如果所遇到的代码量大,此时可通过条件断点的方式,通过打印的办法,判断出谁出错,想在他这里停止下来。

AVL树的删除--了解

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置

AVL树的性能

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

结语:

       随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

   

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。               

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值