AVL树介绍与实现

Posted 可乐不解渴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AVL树介绍与实现相关的知识,希望对你有一定的参考价值。

走好选择的路,别选择好走的路,你才能拥有真正的自己。


在之前的二叉搜索树当中,虽然可以提高我们查找数据的时间效率,但如果插入二叉搜索树的数据是有序或接近有序的,此时二叉搜索树会退化为单支树,此时就相当于是一个单链表,在单链表当中查找数据,效率是很低下的,时间复杂度会退化到O(n)。因此下面要讲的AVL树就是二叉搜索树,但与传统的二叉搜索树不同,它们不会退化到O(n),且增删查改的时间复杂度为O(logN)。

AVL树

AVL树概念

两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:
AVL树就是为了避免树退化为单支树而创建的树,规定在向二叉搜索树中插入新节点时,如果能保证每个节点的左右子树高度之差的绝对值不超1(需要对树中的节点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1、它的左右子树都是AVL树。
2、左右子树高度之差(简称平衡因子)的绝对值不超过1。

AVL树结点的定义

我们这里直接实现KV模型的AVL树,这里我就直接使用pair,为了方便后续的操作,这里将AVL树中的结点定义为三叉链的结构,并在每个结点当中引入平衡因子(右子树高度-左子树高度)。除此之外,还需要一个构造函数,当new AVL树对象时,此时new会先申请空间,然后再去调用构造函数,将每个结点的指针的指向全部置空,否则后期容器出错,且新构造结点的左右子树均为空树,于是将新构造结点的平衡因子初始设置为0即可。

template<class K,class V>
struct AVLTreeNode	//三叉链

	AVLTreeNode* m_left;  
	AVLTreeNode* m_right;
	AVLTreeNode* m_parent;

	pair<K, V>m_kv; //K V 模型
	int m_bf;	//平衡因子


	AVLTreeNode(const pair<K, V>&kv)
		: m_left(nullptr), m_right(nullptr), m_parent(nullptr)
		, m_kv(kv), m_bf(0)
	
;

注意: 这里结点的结构不一定要定义成三叉链且带平衡因子,这里只是为了实现简单方便加上的,且我们这里的平衡因子计算是右子树高度-左子树高度,不意味着反过来就不行。

AVL树插入

AVL树的插入与二叉搜索树插入规则相同。具体可以看之前的二叉搜索树的博客。
1、待插入结点的key值比当前结点小就插入到该结点的左子树。
2、待插入结点的key值比当前结点大就插入到该结点的右子树。
3、待插入结点的key值与当前结点的key值相等就插入失败。

而AVL树与普通的二叉搜索树不同的是,每次插入一个结点进去,都需要从插入位置开始一直沿着父亲往上更新平衡因子。如下图所示,待插入的结点为5,此时插入进去之后要更新5开始一直更新平衡因子,最差要更新到根结点。

我们插入结点后更新的平衡因子的规则如下:

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

每更新完一个祖先结点就判断一下祖先结点的平衡因子是否出现问题。

parent更新后的平衡因子分析
0当更新完之后parent的平衡因子变为0,此时只有-1/1经过++或 - -操作后会变成0,说明新结点插入到了parent左右子树当中高度较矮的一棵子树,插入后使得parent左右子树的高度相等了,此操作并没有改变以parent为根结点的子树的高度,从而不会影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。 此时我们直接break跳出更新语句即可。
-1/1当更新完之后parent的平衡因子变为-1/1,此时只有0经过++或 - -操作后会变成-1/1,说明原来以parent为根结点的左右子树的高度是相等的,而你插入一个新的结点进来,影响了parent的平衡因子,进而影响到了parent的父结点的平衡因子,此时需要继续往上更新。
-2/2当更新完之后parent的平衡因子变为-2/2,此时只有-1/1经过++或 - -操作后会变成-2/2,说明新结点插入到了parent左右子树当中高度较高的一棵子树,使得parent左右子树高度差等于-2/2了,不满足AVL的性质了,为了让其继续满足,此时我们就需要旋转操作来让其平衡。

若cur为新增结点,此时cur的平衡因子为0,此时cur的父亲结点的平衡因子此时有3种情况:
parent的平衡因子更新平衡因子后,平衡因子一定是在-1/0/1这三种情况之中。因为新增结点cur最终会插入到parent的一个空树当中,在新增结点插入前,其父结点的状态有以下三种可能:

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

此时父结点的平衡因子一定不可能为-2或者2,如果为-2或2,那么就说明cur此时不为新增结点,而是向上更新时,cur变为了cur的某个祖先。

当cur不为新增结点时:
如下图所示:

我们先插入3结点到6的左边,此时cur是指向的3,而parent指向的是6,此时我们要更新平衡因子,3的为 0,6的为-1,然后我们发现cur的父亲6的平衡因子是-1或者1时,我们要继续往上更新.

cur = parent;
parent = parent->m_parent;

此时cur就指向的6,而parent指向的15,这个时候cur就变为了新增结点的祖先了,此时我们更新parent的平衡因子,由于新插入的结点在parent的左边,此时我们就需要–parent的平衡因子,就变为了-2,此时我们就需要旋转,如上图所示。
但上图的右下角的平衡因子的计算与我博客中写的计算方式是相反的,即左子树高度-右子树高度,其他操作完全相同。

代码如下:

pair<node*,bool> insert(const pair<K, V>& kv)
	
		if (m_root == nullptr)		//1、如果平衡二叉树为空时,直接再我们的根结点指针创建一个结点
		
			this->m_root = new node(kv);
			return make_pair(m_root,true);
		
		else
		
			//2、如果不为空,我们就按搜索树的规则进行寻找一个位置进行插入
			node* parent = nullptr;					
			node* cur = m_root;
			while (cur != nullptr)
			
				if (cur->m_kv.first < kv.first)
				
					parent = cur;
					cur = cur->m_right;
				
				else if (cur->m_kv.first > kv.first)
				
					parent = cur;
					cur = cur->m_left;
				
				else
				
					return make_pair(cur,false);
				
			
			//这里的parent不会造成空指针崩溃的情况
			//当头指针就与我们插入的元素相同时,在上面则会直接return false,不会来到这一步
			//到了这里判断我们要插入的值是比我们的parent值谁的大
			//如果比我的key值大,则创建一个新的结点插入到parent右边。否则插入到parent左边。
			node*newnode = new node(kv);
			cur = newnode;
			if (parent->m_kv.first < kv.first)
			
				parent->m_right = cur;
				cur->m_parent = parent;
			
			else
			
				parent->m_left = cur;
				cur->m_parent = parent;
			


			//3、更新我们的平衡因子
			while (parent != nullptr)	//我们要一直更新,有可能更新到根结点,如果更新到根结点时,根结点的parent为nullptr,此时就停止循环
			
				if (parent->m_left == cur)	//如果在我们的左边添加的结点我们就--,右边++
				
					--parent->m_bf;
				
				else
				
					++parent->m_bf;
				

				if (parent->m_bf == 0)	//如果此时的平衡因子为0了,说明此时已经是平衡的了,不需要任何的处理
				
					break;
				
				else if (parent->m_bf== 1 || parent->m_bf== -1)	//如果此时为 -1 或者 1时
																//就说明此时某一边填加了一个新的结点,有可能导致上层结点不平衡
																//我们要继续往上更新平衡因子
				
					cur = parent;
					parent = parent->m_parent;
				
				else if (parent->m_bf == 2 || parent->m_bf == -2)	//如果等于 2 或 -2时,那么此时就会发现此时我们的平衡搜索二叉树不平衡了
																	//我们要进行旋转, 让它重新变的平衡
				
					if (parent->m_bf == 2)		
					
						if (cur->m_bf == 1)  //左旋 路径是直线
						
							RotateL(parent);
						
						else if (cur->m_bf == -1) //右左双旋 路径是折线
						
							RotateRL(parent);
						

					
					else if(parent->m_bf== -2)
					
						if (cur->m_bf == -1)   //右旋 路径是直线
						
							RotateR(parent);
						
						else if(cur->m_bf == 1) 左右双旋 路径是折线
						
							RotateLR(parent);
						
					

					//旋转完成后,parent所在的树的高度恢复到了插入节点前高度
					//如果是子树,对上层没有影响,更新结束。
					break;
				

			
			return make_pair(newnode, true);
		
	

AVL树的旋转

根据上面的分析我们可以得到以下几种旋转的情况:

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

其中前两种是直线,所以进行单旋即可,二后面两种情况是折线,需要双旋才能平衡。

左单旋

具象图动图演示:

具象图旋转示意图如下:

左旋的步骤:
如上图所示:

  1. 先让subRL做parent的右子树。
  2. 再将parent做subR的左子树。
  3. 判断parent是否是根的位置,如果为根,那么就让subR做新的根,否则让parent之前的父亲链接上subR。
  4. 最后更新平衡因子。

代码如下:

//传进来的是不平衡的结点的指针
void RotateL(node* parent)	//左旋

	node* subR = parent->m_right;	//出问题的右结点
	node* subRL = subR->m_left;		//出问题的右结点的左结点

	parent->m_right = subRL;		//首先先将我们的parent的右指向subRL
	if (subRL != nullptr)			//再将subRL的父亲指向parent结点,但这样要判断一下是否是空指针,如果subRL是空指针的话,
									//那么解引用它会出现问题
	
		subRL->m_parent = parent;
	

	node* curParent = parent->m_parent;	//拿一个结点存储parent的父亲
	parent->m_parent = subR;			//再使得parent的父亲指针指向它原先的右结点(subR)

	subR->m_left = parent;				//在让subR的左指向parent
	if (parent == m_root)				//在这里得判断一下它是否为根,如果parent为根的话,那么我们的根结点指针也得改变
										//并且将subR的父亲指针置为空,此时subR此时为根结点
	
		m_root = subR;
		subR->m_parent = nullptr;
	
	else
										//如果不为头节点,那么我们只需要将subR的父亲指针指向parent之前的父亲结点
		if (curParent->m_left == parent)
		
			curParent->m_left = subR;
		
		else
		
			curParent->m_right = subR;
		
		subR->m_parent = curParent;
	
	subR->m_bf = parent->m_bf = 0;	//更新平衡因子

右单旋

具象图动图演示:

具象图旋转示意图如下:

右旋的步骤:
如上图所示:

  1. 先让subLR做parent的左子树。
  2. 再将parent做subL的右子树。
  3. 判断parent是否是根的位置,如果为根,那么就让subL做新的根,否则让parent之前的父亲链接上subL。
  4. 最后更新平衡因子。

代码如下:

void RotateR(node* parent)	//右旋

	node* subL = parent->m_left;
	node* subLR = subL->m_right;

	parent->m_left = subLR;
	if (subLR != nullptr)
	
		subLR->m_parent = parent;
	

	node* curParent = parent->m_parent;
	parent->m_parent = subL;

	subL->m_right = parent;
	if (parent==m_root)
	
		m_root = subL;
		subL->m_parent = nullptr;
	
	else
	
		if (curParent->m_left == parent)
		
			curParent->m_left = subL;
		
		else
		
			curParent->m_right = subL;
		
		subL->m_parent = curParent;
	
	//为什么subLR的平衡因子不需要改变呢?
	//因为它的左右子树的平衡并没有被改变,所以不需要改变其平衡因子。
	//只需要更改这里出现问题的parent结点与subL结点的平衡因子
	subL->m_bf = parent->m_bf = 0;	//更新平衡因子


左右双旋

具象图动图演示:

具象图旋转示意图如下:
1、首先插入一个新结点

2、再以30为旋转点先进行左旋

3、最后在以40为旋转点进行右旋

4、最后更新平衡因子
这里的subLR点为40这个点
这里平衡因子左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:

  1. 当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。
  2. 当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。
  3. 当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。

代码如下:

void RotateLR(node* parent)

	node* subL = parent->m_left;
	node* subLR = subL->m_right;
	int bf = subLR->m_bf;

	RotateL(subL);
	RotateR(parent);

	if (bf == 1)
	
		parent->m_bf = 0;
		subL->m_bf = -1;
		subLR->m_bf = 0;
	
	else if (bf == -1)
	
		parent->m_bf = 1;
		subL->m_bf = 0;
		subLR->m_bf = 0;
	
	else if (bf == 0)
	
		parent->m_bf = 0;
		subL->m_bf = 0;
		subLR->m_bf = 0;
	

右左双旋

具象图动图演示:


具象图旋转示意图如下:
1、先插入一个新结点

2、在以50为旋转点来进行旋转

此时平衡因子
3、最后以40为旋转点进行旋转

4、同理这里也是更新平衡因子,同样有三种情况:
此时这里的subRL是40这个结点。

  1. 当subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。
  2. 当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。
  3. 当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。
    代码如下:
void RotateRL(node* parent)

	node* subR = parent->m_right;
	node* subRL = subR->m_left;
	int bf = subRL->m_bf;

	RotateR(subR);
	RotateL(parent);
	if (bf == 1)
	
		parent->m_bf = -1;
		subRL->m_bf = 0;
		subR->m_bf = 0;
	
	else if (bf == -1)
	
		parent->m_bf = 0;
		subRL->m_bf = 0;
		subR->m_bf = 1;
	
	else if (bf == 0)
	
		parent->m_bf = 0;
		subRL->m_bf = 0;
		subR->m_bf = 0;
	

AVL树删除

AVL树得删除,必须先利用key值找到我们要删除得结点。
此时删除结点有以下几种情况:
1、删除的是叶子结点,此时直接删除即可,再更新平衡因子。
2、删除的不是叶子结点,是度为1的结点,即左子树或者右子树一边为空的结点时,只需要将这个结点父亲的的左右指针的一个去指向该结点不为空的一边即可,最后更新平衡因子。
3、删除的结点左右都不为空时,此时我们就需要找一个结点来替换该结点来删除,这里我们采用当前删除结点的右子树的最左节点来替换删除。那么最左结点也只可能为叶子结点或者度为1的结点,故转变为了前两种情况。

综上所述:即最终删除的所有情况都转化为了删除叶子结点或者是度为1的结点。

知道了怎么删除结点,那么平衡因子要怎么调节呢?

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

此时parent的平衡因子有以下3种情况:

parent更新后的平衡因子分析
0只有当父亲平衡因子变化之前是-1/1时,经过–/++操作后会变成0,说明原来parent的左右子树的高度差为1,现在删除的是parent结点左右子树高的一边,此时会影响到parent的父结点及其祖先结点的平衡因子,因此需要继续往上更新平衡因子。
-1/1只有当父亲变化前是0,经过–/++操作后会变成-1/1,说明原来parent的左子树和右子树高度相同,现在我们删除一个结点,并不会影响以parent为根结点的子树的高度,从而变化影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。
-2/2只有当父亲平衡因子变化之前是-1/1时,经过–/++操作变成-2/2,说明原来parent的左右子树的高度差为1,现在删除的是parent结点左右子树矮的一边,此时会影响到parent的父结点及其祖先结点的平衡因子,因此需要继续往上更新平衡因子。此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。

此时在旋转处理情况有以下6种:

  1. 当parent的平衡因子为 -2,parent的左孩子的平衡因子为-1时,进行右单旋。
  2. 当parent的平衡因子为 -2,parent的左孩子的平衡因子为0时,也进行右单旋。
    具体情况如下图所示:
    删除结点为26,更新前此时parent结点为18,平衡因子为-2,而parent的左孩子结点的平衡因子为0。删除之后需要以parent为旋转点来进行一个右单旋,此时parent的平衡因子更新为-1,而parent的左孩子结点的平衡因子为1。此时旋转后无需继续往上更新平衡因子,因为这种情况旋转后树的高度并没有发生变化。
  3. 当parent的平衡因子为 -2,parent的左孩子的平衡因子为1时,进行左右双旋。
  4. 当parent的平衡因子为 2,parent的右孩子的平衡因子为1时,进行左单旋。
  5. 当parent的平衡因子为 2,parent的右孩子的平衡因子为0时,也进行左单旋。

具体情况如下图所示:
我们要删除30这个结点,此时parent为50这个结点,parent的右孩子我们命名为parentRight,此时parentRight为60号结点,删除30这个结点之后,parent平衡因子为2了,此时需要看parentRight,此时parentRight为0,我们需要进行一个左单旋。parentRight的平衡因子更新完后变为-1,parent的平衡因子变为1。此时旋转后无需继续往上更新平衡因子,因为这种情况旋转后树的高度并没有发生变化。

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

其他的4种情况的旋转与之前插入的情况相同。

代码如下:

bool Erase(const K& key)

	if (m_root == nullptr)
	
		return false;
	
	else
	
		node* parent = nullptr;
		node* cur = m_root;
		node* delParent = nullptr;
		node* delNode = nullptr;
		while (cur != nullptr) //找删除的结点的位置
		
			if (cur->m_kv.first < key)
			
				parent = cur;
				cur = cur->m_right;
			
			else if(cur->m_kv.first > key)
			
				parent = cur;
				cur = cur->m_left;
			
			else
			
				//找到了结点的情况
				if (cur->m_left == nullptr)
				
					if (cur == m_root)
					
						m_root = m_root->m_right;
						if (m_root != nullptr) //这里是判断当左右都为空的时候,nullptr奔溃
						
							m_root->m_parent = nullptr;
						
						delete cur;
						return true;
					
					else
					
						delParent = parent;
						delNode = cur;
					
				
				else if (cur->m_right == nullptr)
				
					if (cur == m_root)
					
						m_root = cur->m_left;
						if (m_root != nullptr)
						
							m_root->m_parent = nullptr;
						
						delete cur;
						return true;
					
					else
					
						delParent = parent;
						delNode = cur;
					
				
				else
				
					node* leftMinParent = cur;
					node* leftMin = cur->m_right;
					while (leftMin->m_left != nullptr) //找右半边的最左结点,为右边最小值
					
						leftMinParent = leftMin;
						leftMin = leftMin->m_left;
					
					cur->m_kv.first = leftMin->m_kv.first;
					cur->m_kv.second = leftMin->m_kv.second;
					delParent = leftMinParent;
					delNode = leftMin;
				
				break;
			
		

		if (cur == nullptr) //没找到的情况
		
			return false;
		

		//更新平衡因子
		cur = delNode;
		parent = delParent;
		while (parent != nullptr) //最坏一路更新到根结点
		
			if (cur == parent->m_left)  //判断删除的是在父亲的那一边,然后更新平衡因子
			
				++parent->m_bf;
			
			else if(cur == parent->m_right)
			AVL树介绍与实现

再回首数据结构—AVL树

C++AVL树的实现--详细解析旋转细节

C++AVL树的实现--详细解析旋转细节

深度解析AVL树

平衡二叉查找树AVL