【C++实现AVL树】


一、AVL树概念

AVL树出现在二叉搜索树之后,弥补了二叉搜索树的一些缺点。

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

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

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

但是,AVL树不一定都有平衡因子,因为平衡因子只是用来检测平衡的方式,如果有其他检测的方法,也可以将平衡因子弃之不用。

例:
在这里插入图片描述

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


二、AVL树节点的定义

一个AVL树的节点和二叉搜索树类似,不同的是AVL树采用三叉链的结构更方便控制(即子节点也有一个指向父节点的指针)。但这个和平衡因子一样,不是必须要求的,也可采用其他方法,只要能完成相同的操作即可(本篇文章采用三叉链进行讲解)。
另外,对AVL树中存储的数据,这里采用pair<K,V>的方式,可以适用于更多的场景。

代码如下:

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)
		, _bf(0)//初始时,该节点没有左右孩子,平衡因子为0
		, _kv(kv)
	{}
};

AVL树类模板初始如下:

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	AVLTree()
		:_root(nullptr)
	{}
private:
	Node* _root;
};

三、AVL树的插入

AVL树的插入分为三步:

  1. 像二叉搜索树一样按照左孩子小于父节点,右孩子大于父节点的规则插入节点
  2. 检查每一个节点的平衡因子绝对值是否小于2
  3. 若平衡因子绝对值大于1,则进行旋转

插入节点:

若树为空树,直接将新增节点作为根节点。否则再依次向下比较节点key值的大小,以确定正确的插入位置。若已有相同key值的节点存在,则插入失败。不要忘记维护好三个节点指针的指向

如下:
在这里插入图片描述
代码实现如下:

bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{//根节点为空,直接将新增节点作为根节点
			_root = new Node(kv);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{//比较新增节点和当前节点的key值,确定将其放在左/右
			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;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
}

但是,就上图而言,很明显已经不是一棵AVL树了,因为它的节点中出现了平衡因子不小于2的情况。所以为了保证每插入一个节点之后,AVL树结构不被破坏,就需要检查节点的平衡因子。
本篇文章中的平衡因子为右子树高度-左子树高度

又因为一个节点的平衡因子与左右子树有关,所以新增一个节点之后,只会影响新增节点的祖先节点的平衡因子,也就是说,只需要检测新增节点到根节点所在路径中的节点平衡因子。

更新平衡因子的详细步骤:

  • 首先更新新增节点的父节点平衡因子

如果新增节点为父节点的左孩子,则父节点平衡因子-1;若为右孩子,则父节点的平衡因子+1。

  • 然后向上更新父节点的祖先节点的平衡因子

这里有三种情况:
①插入新增节点后,父节点平衡因子为0。则说明插入之前父节点已经有一个孩子,插入后只是新增一个孩子,以父节点为根的子树高度不变,也就不会影响祖先的平衡因子,则更新结束。

②插入新增节点后,父节点平衡因子为1或-1。则说明更新前父节点平衡因子为0,更新后,父节点的左右子树其中之一的高度+1。这时必定会影响到父节点的祖先节点的平衡因子,所以需要继续向上更新。

③插入新增节点后,父节点平衡因子为2或-2,这时父节点为根的子树已经不平衡了,需要进行旋转(下文讲解)。

上面三种情况如图:
在这里插入图片描述
注意,这里最坏情况下,会一直更新到根节点。

代码实现如下:

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
	{
		// 说明插入更新平衡因子之前,树中平衡因子就有问题了
		assert(false);
	}
}

四、AVL树的旋转

当一棵树中某一棵子树的根节点的平衡因子绝对值大于1时,为了保证AVL树结构,就必须进行旋转操作。而旋转又分为四种----左单旋、右单旋、左右双旋和右左双旋。下面一 一介绍:
右单旋:

当某一个节点的平衡因子为-2且子节点平衡因子为-1时,表示左子树的高度比右子树大2,为了平衡,就必须进行右单旋,如下:
在这里插入图片描述
这样既保证了平衡因子满足要求,又保证了搜索树的特性,而且这棵树的高度完成了-1。
注意,需要调整的不一定是整棵树,也可能是某一棵子树,这种情况下,调整子树不会破坏整棵树的平衡。

代码如下:

if (parent->_bf == -2 && cur->_bf == -1)
{
	RotateR(parent);
}
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	if (subLR)//不能将空节点的父节点赋值
		subLR->_parent = parent;
	//记录父节点的父节点,方便为后续subL的父节点赋值
	Node* parentParent = parent->_parent;

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

	if (parent == _root)
	{//父节点为根,则subL变为新的根
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parentParent->_left == parent)
			parentParent->_left = subL;
		else
			parentParent->_right = subL;

		subL->_parent = parentParent;
	}
	//别忘了调整subL和parent的平衡因子
	subL->_bf = parent->_bf = 0;
}

左单旋:

类比右单旋,左单旋就是一个节点的平衡因子为2且子节点平衡因子为1的情况,如图:
在这里插入图片描述

这里就不粘贴代码了,在文章最后会给出完整代码。

双旋(左右/右左):

当一棵树的子树的左子树的高度大于右子树,且这棵子树的子树的右子树高度大于左子树时(相反情况也适用),就需要用到双旋,如图:
在这里插入图片描述
这种情况就需要用到左右双旋,直接复用左单旋和右单旋即可。

void RotateLR(Node* parent)
{
	RotateL(parent->_left);
	RotateR(parent);
}

至于右左双旋类比即可。
但这样还不够,因为在某些场景下,有些节点的平衡因子会出错,这种情况下文再详谈


五、AVL树的验证

上文中,我们已经实现了AVL树的插入,那么该怎么验证这个插入的过程没有错误呢?

答:需要对整棵树计算出左子树和右子树的高度,判断它们的差的绝对值是否小于2。
注意,这里计算的过程应该包含每一棵子树。不用平衡因子检测是因为平衡因子本身可能出错

代码如下:

bool IsBalance()
{
	return _IsBalance(_root);
}

int Height(Node* root)
{
	if (root == NULL)
		return 0;
	int leftHeight = Height(root->_left);
	int rightHeight = Height(root->_right);
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

bool _IsBalance(Node* root)
{
	if (root == NULL)
		return true;

	// 对当前树进行检查
	int leftHeight = Height(root->_left);
	int rightHeight = Height(root->_right);
	if (rightHeight - leftHeight != root->_bf)
	{//检查平衡因子是否有误
		cout << root->_kv.first << "现在是:" << root->_bf << endl;
		cout << root->_kv.first << "应该是:" << rightHeight - leftHeight << endl;
		return false;
	}
	return abs(rightHeight - leftHeight) < 2
		&& _IsBalance(root->_left)
		&& _IsBalance(root->_right);
}

事实是上面的实现AVL树的代码确实会导致一些情况下,某些平衡因子出错的情况。
而且错误是出现在双旋的过程中的,该过程中需要控制平衡因子。在双旋中,把一些节点的平衡因子置0,但经过两次旋转后,它们的平衡因子可能并不为0。
例:
在这里插入图片描述
可以看到,这里双旋之后,3的平衡因子为-1,但原代码处理后,3的平衡因子为0,因为3在第一次左旋的时候,已经将其平衡因子置零了。

上面这种情况是左右双旋,而当右左双旋时,结果就会是右边的节点的平衡因子变为1。
当h=0,6为新增节点时,双旋后平衡因子都为0(具体过程可以通过画图观察一下)。

那么怎么区分这三种情况,并做出改正呢?

可以看到,这三种情况跟旋转前的6的平衡因子有关(6的平衡因子为1、-1和0的情况)。所以,在旋转之前记录6的平衡因子,再对旋转后的6的左/右节点的平衡因子进行改正即可。

这里只给出左右双旋的图示,右左双旋可以类比,总结如下:

  • 旋转前 sunLR:-1;旋转后 parent:1,subL:0,subLR:0

在这里插入图片描述

  • 旋转前 subLR:1;旋转后 parent:0,subL:-1,subLR:0

在这里插入图片描述

  • 旋转前 subLR:0;旋转后 parent:0,subL:0,subLR:0

在这里插入图片描述

下面给出完整的代码:

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)
		, _bf(0)//初始时,该节点没有左右孩子,平衡因子为0
		, _kv(kv)
	{}
};

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	AVLTree()
		:_root(nullptr)
	{}

	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{//根节点为空,直接将新增节点作为根节点
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{//比较新增节点和当前节点的key值,确定将其放在左/右
			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;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		// 控制平衡
		// 1、更新平衡因子 -- 新增节点到根节点的祖先路径
		// 2、出现异常平衡因子,那么需要旋转平衡处理
		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)
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1) // 左单旋
				{
					RotateL(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 RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
		{
			subRL->_parent = parent;
		}

		Node* parentParent = parent->_parent;
		subR->_left = parent;
		parent->_parent = subR;

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

		subR->_bf = parent->_bf = 0;
	}

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

		parent->_left = subLR;
		if (subLR)//不能将空节点的父节点赋值
			subLR->_parent = parent;
		//记录父节点的父节点,方便为后续subL的父节点赋值
		Node* parentParent = parent->_parent;

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

		if (parent == _root)
		{//父节点为根,则subL变为新的根
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (parentParent->_left == parent)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;

			subL->_parent = parentParent;
		}
		//别忘了调整subL和parent的平衡因子
		subL->_bf = 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;
		}
		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);
		}
	}

	bool IsBalance()
	{
		return _IsBalance(_root);
	}

	int Height(Node* root)
	{
		if (root == NULL)
			return 0;
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	bool _IsBalance(Node* root)
	{
		if (root == NULL)
			return true;

		// 对当前树进行检查
		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		if (rightHeight - leftHeight != root->_bf)
		{
			cout << root->_kv.first << "现在是:" << root->_bf << endl;
			cout << root->_kv.first << "应该是:" << rightHeight - leftHeight << endl;
			return false;
		}

		return abs(rightHeight - leftHeight) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
private:
	Node* _root;
};

六、AVL树的性能

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


七、总结

本篇文章主要是介绍了AVL树的插入,对其中的四种旋转方法做了详解。
重中之重要注意区分什么时候该用哪一种合适的旋转方法(根据节点的平衡因子判断),以及双旋之后该对哪些节点的平衡因子做出调整(不需要死记硬背,记住其中一种双旋结果,另外的就可以类比了)。

此外,本篇文章中并没有少搜索、修改以及删除的方法。因为搜索和修改比较简单,类比搜索树即可。而删除操作还是需要进行平衡因子的检查以及相应的旋转操作,可以类比插入。

另外,本篇文章中AVL树中的数据是pair<int,int>型的,可以适用于更多场景。读者可以根据自身需要,将数据类型改为int等。


本篇完,青山不改,绿水长流!

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Undefined__yu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值