C++AVL树

本期我们来实现详细讲解AVL树

目录

AVL树的概念

代码实现

全部代码


AVL树的概念

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

AVL树的增删查改最多走高度次,也就是O(logN)

代码实现

cpp 复制代码
template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K,V>* _left;
	AVLTreeNode<K,V>* _right;
	AVLTreeNode<K,V>* _parent;
	int _bf;//平衡因子
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_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* 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 (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;
		//控制平衡
		
		return true;
	}
private:
	Node* _root = nullptr;
};

我们先写出基础框架,然后我们来完成insert,此时我们完成的是平衡搜索树的,而AVL树还要控制平衡,另外需要注意的是,AVL树是三叉树,还有一个指针指向父亲节点

我们插入新节点后,平衡因子会发生变化,有多种情况,我们进行分析

1.新增节点在左,parent的平衡因子减一

2.新增节点在右,parent的平衡因子加一

3.更新后parent平衡因子 == 0,说明parent所在子树高度不变,不会再影响祖先,不用再继续沿着root的路径往上更新

4.更新后parent平衡因子 == 1 或者 -1,说明parent所在子树高度发生变化,会影响祖先,需要继续沿着root的路径往上更新

5.更新后parent平衡因子 == 2 或者 -2,说明parent所在子树高度发生变化,且不平衡,需要对parent所在子树进行旋转,让他重新平衡

6.更新到根节点

在上面的条件中,如果是3和5,那么就结束插入,如果是4,那么继续循环,更新祖先的平衡因子

6也是插入结束,这是最坏的情况,比如满二叉树插入新节点,他的平衡因子就会一直更新到根节点

cpp 复制代码
//控制平衡
		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)
			{
				//旋转

			}
			else
			{
				return false;
			}
		}

控制平衡代码也非常长,这里是我们对于上面的逻辑进行的实现,最后为什么还要有一个else呢?这是当我们的代码出现错误时,如果错误在这里,说明平衡因子在插入前就有问题,方便我们判断,当然,上面的逻辑还差一个旋转,下面我们来看看到底怎么进行旋转

我们在旋转时要注意两个问题,一个是旋转后要保证它还是搜索树,二是要变为平衡树且降低这个子树的高度

假设我们有这样两棵树,其中红色的节点是我们新插入的节点,此时的平衡因子就出现了问题,需要我们进行旋转

我们对第一棵树这样旋转,这是什么操作呢?3作为1的左子树,下面的节点一定是比1大的,所以我们把3的左子树作为1的右子树(这里3的左子树是空,所以1的右子树也是空),然后把1作为3的左子树,这样就可以把1按下来,使整棵树的高度降低

同样的,我们看第二棵树,我们一样的把3的左子树作为1的右子树,也就是2变为1的右子树 ,然后把1变为3的左子树,这样整棵树的高度就降下来了,平衡因子问题也就解决了

当然这还没完,当我们旋转完后,还要把父亲节点给更正

上面的只是一种情况,我们的parent,也就是平衡因子为2的节点都是根节点,如果不是根节点,我们是需要提前保存一下,然后更正父亲节点的位置

cpp 复制代码
void RotateL(Node* parent)
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		parent->_right = curleft;
		if (curleft)
		{
			curleft->_parent = parent;
		}
		cur->_left = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = cur;
		if (parent == _root)//parent是根节点
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		parent->_bf = cur->_bf = 0;
	}

整个过程是非常复杂的,大家需要画图来进行辅助理解 ,我们给这个函数传的参数是平衡因子为2的节点,也就是parent

我们的旋转其实还没有完成,因为旋转的情况不可能只有这么简单的两种,我们把上面的这副图叫做抽象图,它没有画出具体的图,但是这里是一个代表,画了abc3棵子树,插入之前,abc是3棵AVL子树,如果在c里插入一个新节点,导致c的高度变为h+1,就会导致60的平衡因子变为1,进而导致30的平衡因子变为2,这棵树就不平衡了,就需要旋转

下面我们来分情况进行讨论

第一种情况是h=0,大概是这个样子

第二种情况是h=1,新增节点是60的右孩子的孩子,无论是左右孩子都会导致30的平衡样子变为2,而需要旋转,这里是有两种情况的

第三种情况是h=2,此时情况就非常复杂了 ,我们可以确定ab是xyz的任意一种,而c是z,只有这样在c新增节点才能让这棵树进行旋转,再加上ab的选择,这棵树插入前的可能形状组合有 3*3*1 = 9种,而在c插入新节点有4种情况,所以这里一共是有 4*9 = 36种情况的,大家想想,这还只是h为2的情况,如果h是3,4,5呢?所以是这里的情况是无穷无尽的,于是就需要有抽象图来帮助我们进行理解

但是不管它是什么情况,我们只需把60的左边作为30的右边,再把30作为60的左边即可,以不变应万变

上面我们实现了左单旋,下面我们就需要实现右单旋了

结合这副图,我们可以理解,右单旋是将30的右边给60的左边,再把60作为30的右边,这里和左单旋是类似的,下面我们继续分情况讨论

我们看h=0和h=1的情况,h=0只有一种情况,而h=1是有两种情况的,在30左孩子的左右新增节点都会发生旋转

h=2 也与左单旋基本一样,只不过此时a是z,bc是xyz中的一种,这里同样一共有36种情况

下面我们来写代码

cpp 复制代码
void RotateR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		parent->_left = curright;
		if (curright)
		{
			curright->_parent = parent;
		}
		Node* ppnode = parent->_parent;
		cur->_right = parent;
		parent->_parent = cur;
		if (ppnode == nullptr)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		parent->_bf = cur->_bf = 0;
	}

有了左旋的经验,我们可以轻松写出右旋

实现了单旋,下面我们来看看双旋

这种情况我们该如何进行旋转呢?这里就需要进行双旋,我们可以看出,如果插入新节点后,路径长的像直线就是单旋,是折线就是双旋

首先我们来看h=0的情况,这里60就是新增的节点

​​​​​​​

再看h=1,在60的左边或者右边新增都会导致双旋

我们再看h=2的情况,首先a和d是xyz中的任意一种, 而60的左右子树高度为h-1,也就是1,所以60的左右孩子就是一个节点,此时新增节点有4种情况,在60的两个孩子的左右子树4个位置

这里有共用 3*3*4 = 36种情况,h=3,4,5的情况就更加复杂,这里也是无穷无尽的

接着我们来看双旋的操作,我们先以90为旋转点,进行右单旋,再以30为旋转点,进行左单旋

我们先以90为旋转点进行右单旋,也就是90为parent,我们把60的右边作为90的左边,再把90作为60的右边,然后我们就可以发现,这棵树从折线变成了直线

再以30作为旋转点,也就是parent,我们就可以得到一棵正常的树

我们再看这棵树,同样的,我们先以90为旋转点,进行右单旋

再以30为旋转点,进行左单旋,就完成了

我们分析可以得到,双旋结果的本质是:60的左边给30的右边,60的右边给90的左边,60成了这棵树的根

根据插入节点位置的不同,30和90的平衡因子也是不同的

而当60自己就是新增节点时,又是一种情况

我们来看,当h>0时,我们在c插入新节点诱发旋转,c从h-1变为h,最终30的平衡因子是-1 ,c最后变成了90的左边

如果是在b插入新节点诱发旋转,b最终变为30的右边,90的平衡因子变为1

上面是两种情况,还有一种就是60本身是新增,60的平衡因子就是0,所以这里我们一共分出3种情况

也就是说,这里最终的平衡因子是需要看60的,30是parent,90是cur,60是curleft,也就是说,我们要看curleft的平衡因子

cpp 复制代码
void RotateRL(Node* parent)//右左双旋
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		int bf = curleft->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		if (bf == 0)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = -1;
		}
		else if(bf == -1)
		{
			cur->_bf = 1;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

我们根据curleft的平衡因子来确定最终的平衡因子

既然有右左双旋,那么就肯定还有左右双旋

这里的代码其实是基本一样的,我这里就不详细说了,下面我们直接来实现代码

cpp 复制代码
void RotateLR(Node* parent)//左右双旋
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		int bf = curright->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		if (bf == 0)
		{
			cur->_bf = 0;
			curright->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			cur->_bf = -1;
			curright->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			cur->_bf = 0;
			curright->_bf = 0;
			parent->_bf = 1;
		}
		else
		{
			assert(false);
		}
	}

我们根据curright的平衡因子来确定最终的平衡因子

这些旋转完成后,我们需要把insert补全

cpp 复制代码
bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		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 (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)
			{
				//旋转
				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)
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
		return true;
	}

可以看到,我们是分了非常多的情况进行讨论的

下面我们再写一个函数来判断我们的AVL树是否正常

cpp 复制代码
int Height(Node* root)//求高度
	{
		if (root == nullptr)
		{
			return 0;
		}
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}
	bool IsBalance()
	{
		return IsBalance(_root);
	}
	bool IsBalance(Node* root)//判断是否为AVL树
	{
		if (root == nullptr)
		{
			return true;
		}
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);
		if (rightHeight - leftHeight != root->_bf)//平衡因子不正常时
		{
			cout << "平衡因子异常:" <<root->_kv.first<< "->" << root->_bf << endl;
			return false;
		}
		return abs(rightHeight - leftHeight) < 2
			&& IsBalance(root->_left)  //所有的子树都要符合
			&& IsBalance(root->_right);
	}

这里有两个IsBalance,空参数的那个是为了在外边使用,因为_root是私有的,下面的那个可以写成子函数,不写也没事,因为构成了函数重载

接着我们测试一下,没有问题

再用1w个随机值测试一下(有些同学写的代码跑不过某些随机值),这里也没有问题

我们上面实现了插入,删除我这里就不写了,给大家提供一下思路,感兴趣的可以自己去实现一下

删除也是分为三部,按搜索树查找节点删除,更新平衡因子,出现异常时旋转

我们看这张图片,我们删除了2,此时1的平衡因子变成了-1,但是此时是不需要更新的,因为高度没有发生变化

而如果我们把9删掉,8的平衡因子变为了0 ,此时就需要更新了,因为高度是发生了变化

大家想想,插入时我们是填充低的树,高度才会变化,而删除是删掉高的树,高度会变化,原来的平衡因子是-1或者1,就是一边高一边低,然后变成了0,说明高的那一端被删掉了,所以就需要更新

所以我们一路往上更新,这里到根节点结束了

我们继续删除,这次我们把6和8删掉,此时根节点的平衡因子变成了-2

然后我们进行旋转,这里是右单旋,旋转其实没什么区别,就是那几种,但是平衡因子的更新会变得非常麻烦,旋转我们也是根据删除的路径,而不是插入时的路径,是非常麻烦的

具体的大家可以在网上查一查,或者根据数据结构相关的书籍来看一看

全部代码

cpp 复制代码
using namespace std;
#include<iostream>
#include<assert.h>

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K,V>* _left;
	AVLTreeNode<K,V>* _right;
	AVLTreeNode<K,V>* _parent;
	int _bf;//平衡因子
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_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* 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 (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)
			{
				//旋转
				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)
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
		return true;
	}
	void RotateL(Node* parent)//左单旋
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		parent->_right = curleft;
		if (curleft)
		{
			curleft->_parent = parent;
		}
		cur->_left = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = cur;
		if (parent == _root)//parent是根节点
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		parent->_bf = cur->_bf = 0;
	}

	void RotateR(Node* parent)//右单旋
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		parent->_left = curright;
		if (curright)
		{
			curright->_parent = parent;
		}
		Node* ppnode = parent->_parent;
		cur->_right = parent;
		parent->_parent = cur;
		if (ppnode == nullptr)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
		parent->_bf = cur->_bf = 0;
	}

	void RotateRL(Node* parent)//右左双旋
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		int bf = curleft->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		if (bf == 0)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = -1;
		}
		else if(bf == -1)
		{
			cur->_bf = 1;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void RotateLR(Node* parent)//左右双旋
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		int bf = curright->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		if (bf == 0)
		{
			cur->_bf = 0;
			curright->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			cur->_bf = -1;
			curright->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			cur->_bf = 0;
			curright->_bf = 0;
			parent->_bf = 1;
		}
		else
		{
			assert(false);
		}
	}
	int Height(Node* root)//求高度
	{
		if (root == nullptr)
		{
			return 0;
		}
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}
	bool IsBalance()
	{
		return IsBalance(_root);
	}
	bool IsBalance(Node* root)//判断是否为AVL树
	{
		if (root == nullptr)
		{
			return true;
		}
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);
		if (rightHeight - leftHeight != root->_bf)//平衡因子不正常时
		{
			cout << "平衡因子异常:" <<root->_kv.first<< "->" << root->_bf << endl;
			return false;
		}
		return abs(rightHeight - leftHeight) < 2
			&& IsBalance(root->_left)  //所有的子树都要符合
			&& IsBalance(root->_right);
	}
private:
	Node* _root = nullptr;
};

以上即为本期全部内容,希望大家可以有所收获

如有错误,还请指正

相关推荐
charlie1145141919 分钟前
C++ STL CookBook
开发语言·c++·stl·c++20
袁袁袁袁满9 分钟前
100天精通Python(爬虫篇)——第113天:‌爬虫基础模块之urllib详细教程大全
开发语言·爬虫·python·网络爬虫·爬虫实战·urllib·urllib模块教程
ELI_He99915 分钟前
PHP中替换某个包或某个类
开发语言·php
Lenyiin16 分钟前
01.02、判定是否互为字符重排
算法·leetcode
小林熬夜学编程20 分钟前
【Linux网络编程】第十四弹---构建功能丰富的HTTP服务器:从状态码处理到服务函数扩展
linux·运维·服务器·c语言·网络·c++·http
m0_7482361123 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
倔强的石头10631 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
鸽鸽程序猿31 分钟前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd32 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo61735 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript