初识C++之AVL树

目录

 

一、AVL树的概念

二、模拟实现一个AVL树

1.结构体定义

2.更新平衡因子

3.旋转子树

3.1 新节点插入较高右子树的右侧——右右->左单旋

3.2 插入较高左子树的左侧——左左->右单旋

3.3 插入较高右子树的左侧节点——右左->右左双旋

3.4 在左子树的右侧插入——左右->左右双旋

3.5 总结

4.完善调整平衡因子和实现数据插入


一、AVL树的概念

AVL树,说白了其实也是一颗二叉搜索树。 二叉搜索树虽然可以缩短查找到的效率,但如果数据有序或接近有序,二叉搜索树就会退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下:

 因此,两位俄罗斯数学家就在1962年发明一种解决问题的方法:当向二叉搜索树中插入新节点后,如果能保证每个节点的左右子树高度之差的绝对值不超过1(需要对树中的节点进行调整),即可降低树的高度,从而减少平均搜索长度。这种树就被称为AVL树。

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

(1)它的左右子树都是AVL树

(2)左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。一般来讲,平衡因子 = 右子树的高度 - 左子树高度。

当然AVL树并不是一定要加入平衡因子。加入平衡因子只是实现AVL树的一种方法。

 上图就是一棵AVL树,它的所有节点的平衡因子的绝对值都不超过1。

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

二、模拟实现一个AVL树

1.结构体定义

namespace MyAVLTree
{
	template<class K, class V>
	struct AVLTreeNode
	{
		pair<K, V> _kv;//kv结构,存储key和value

		//AVL树和普通的二叉树不同,最好使用三叉链结构
		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:

	private:
		Node* _root;//创建节点
	};
}

 AVL树的结构体定义就和普通的二叉树不同了。

首先因为它所使用的是KV模型,所以这里就创建了一个pair<K, V>来存储key和value。当然,如果你不想使用kv模型,只想用k模型,修改一下结构体即可。

第二个不同就是AVL树用的是三叉链,多了一个指向父节点的指针。该指针在修改平衡因子和让二叉树平衡有着重要作用。

第三个就是这里的AVL数中多了一个平衡因子,用于记录每棵子树的高度差。上文也说了,平衡因子只是实现AVL树的一种方法,你也可以使用其他方法来实现AVL树。

2.更新平衡因子

平衡因子上的高度差表示该子树是否为AVL树,如果它的绝对值大于1,就说明该树不是AVL树,修改调整。因此,每插入一个节点,都可能需要对平衡因子进行更新。

那么平衡因子怎么更新呢?先假设我们有以下一个AVL树:

 (1)在它的8号节点的左子树插入一个节点:

 此时8号节点上的平衡因子就需要更新为0。但是当8号因子更新后,就无需再向上更新了。

(2)假设现在有如下一棵AVL树:

 在它的9号节点上插入一个节点:

此时业需要更新节点。但是这次就不再是只更新插入节点的父节点,而是需要一直向上更新,直到更新到根节点。这也就说明,平衡因子的更新并不是全部节点都需要更新,但是在最坏情况下,需要一直更新到根节点

(3)假设现在有如下AVL树:

在9号节点插入一个节点:

此时再更新平衡因子,就会导致8号节点上的平衡因子的绝对值大于1,导致该子树不是AVL树,就需要对其进行调整。

通过上面的三种插入节点的例子,就可以总结出如下更新平衡因子的规律。

(1)如果在父节点的右节点插入,就要让父节点的平衡因子++;如果在父节点的左节点插入,就要让父节点的平衡因子--

(2)平衡因子可能需要一直向上更新。如果一个节点的平衡因子更新为0,就不再需要更新它的父节点。因为当平衡因子更新为0时,说明该节点原来的平衡因子为-1或1,此时它的左子节点或右子节点为空。插入一个节点使其更新为0后,就是填上了空节点;反之如果更新为1或-1,就需要向上更新,因此此时该子树的高度发生变化。

(3)如果一个节点上的平衡因子更新为2或-2,就说明该子树不是AVL树,需要进行调整。

通过上面的三个结论,就可以写出更新平衡因子的代码:

//更新平衡因子
while (parent)//当parent为空,即为根节点的父节点时结束
{
	if (cur == parent->_left)//插入在左子节点
		--parent->_bf;
	else if (cur == parent->_right)//插入在右子节点
		++parent->_bf;

	if (parent->_bf == 0)//父节点的平衡因子为0,不再需要向上更新
	{
		break;
	}		
	else if (parent->_bf == 1 || parent->_bf == -1)//父节点的平衡因子为1或-1,
	{											   //继续向上跟新
		cur = parent;
		parent = parent->_parent;
	}
	else if (parent->_bf == 2 || parent->_bf == -2)//父节点的平衡因子为2或-2,
	{											   //插入有问题,调整
		//调整方法
	}
	else//来到这里,说明程序出错有问题
		assert(false);
}

 当然,虽然更新平衡因子的代码写好了,但是当平衡因子为2或-2时,需要对子树进行调整的代码还没有写好,这里的内容放在下面讲。

这里没有写函数定义是因为这一部分是放在insert()函数中的,方便后续使用。当然,如果你想将其提取出来单独写成一个函数体也是可以的。

3.旋转子树

当更新完平衡因子后,如果遇到平衡因子大于1的绝对值的情况,就需要调整子树。而调整子树的方法,就是进行旋转。

让子树进行旋转,就要达到以下几个目的:

(1)让这棵子树的左右子树高度差不超过1

(2)旋转后的子树依然为搜索树

(3)更新调整子节点的平衡因子

(4)让这棵子树的高度和插入前保持一致

要达成旋转,又分为几种情况。

3.1 新节点插入较高右子树的右侧——右右->左单旋

从字面意思就能理解,当一个新的节点插入到较高右子树的右侧时,就需要将这棵子树进行左单旋。单看文字可能不太好理解,我们就用不同高度的AVL树来进行模拟。

在上图中,就是一个AVL树,其中的a、b、c分别代表了一个高度为h的子树。这棵子树的右子树就先天比左子树高1。在这里,就让h等于不同的值来模拟向较高右子树的右子节点插入的情况。

(1)当h = 0时:

当h等于0时,向右子树的右侧插入,可以画出如下AVL树:

可以看到,此时根节点上的平衡因子就会变成2,需要进行调整。调整的方法很简单,根据二叉搜索树左子树必然小于右子树的特性,就可以让节点20作为根节点,让节点15和30分别作为节点20的左右子节点。然后分别更新15、20和20号节点的左子节点的父指针。最后让15号节点的原父节点指向20号节点:

此时就可以在让该子树保持二叉搜索树特性的同时,左右子树的平衡因子君不大于1的绝对值,依然是一棵AVL树。

(2)当h = 1时:

 当h = 1,就可以画出如上的AVL树。此时向它的右子树的右侧插入:

此时,就会导致以20为根节点的子树的平衡因子等于2,需要进行旋转。将20节点作为基点,让20号节点及其左子树作为30号节点的左子树,然后让30号节点的左子树作为它的原父节点20号节点的右子树。再分别更新20、30和25号节点的父指针。最后让20号节点的原父节点指向30号节点。此时 就使得整棵树为AVL树。

 (3)当h = 2时:

 当h等于2时,就可以画出如上AVL树。继续在该AVL树的右子树的右侧插入:

此时根节点的平衡因子为2,需要调整。以20为基点,让30好节点的父节点20号节点及其左子树充当30号节点的左子树,然后让30号节点的左子树充当20号节点的右子树。再分别更新20、30和25号节点的父指针。最后让20号节点的原父节点指向30号节点:

通过上面的三个实例,就可以总结出在一棵右子树较高的AVL树的右子节点处插入时的调整规律。即首先以平衡因子为2的节点为基点,将基点及其左子树当做基点的右子节点的左子树,而基点的右子节点的左子树作为基点的右子树。然后分别更新基点、基点的右子节点、基点的右子节点的左子节点的父指针。最后让基点原来的父节点指向基点原来的右子节点。

并且通过观察,可以发现,在旋转完后,传入的基点和基点的下一个节点上的平衡因子都会被置为0。且只需调整这两个节点。因为只有这两个节点的位置的左右子树被调整了,其他节点的左右子树都未调整。

void RotateL(Node* parent)//左单旋
{
	Node* subR = parent->_right;//构建节点指向基点的右子节点
	Node* subRL = subR->_left;//构建节点指向subR节点的左子节点

	parent->_right = subRL;//让基点的右节点指向cur的左子节点
	subR->_left = parent;//让cur的左子节点指向基点
			
	if(subRL)//当subRL不为空时才需要更新
		subRL->_parent = parent;//该节点此时为基点的右子节点,更新_parent指向基点

	Node* ppNode = parent->_parent;//记录基点的父节点
	subR->_parent = parent->_parent;//此时cur替代基点的位置,让cur的parent指向上一个父节点
	parent->_parent = subR;//此时基点为cur的左子节点,更新parent让其指向cur

	if (parent->_parent == nullptr)//parent的父节点为空,说明它为根,更新根节点
		_root = subR;
	else//此时需要将原基点的父节点与现在的基点subR相链接,不链接的话该节点依然指向parent
	{
		if (ppNode->_left == parent)//基点为其父节点的左节点
			ppNode->_left = subR;
		else//基点为其父节点的右节点
			ppNode->_right = subR;
	}

	parent->_bf == subR->_bf == 0;//更新parent和cur节点上的平衡因子,设置为0		
}

左单旋的代码需要操作的节点其实就4个。即基点parent,基点的右节点subR,subR的左节点subRL和基点的父节点parent->_parent。

左单旋的代码有几个点需要注意。

(1)插入的节点是AVL树的右子树的右侧节点,所以在旋转时要考虑到subR节点的左子节点为空的情况。当为空时,就无需更新该节点的父节点。

(2)基点有可能是根节点。当基点为根节点时,需要将根节点_root指向subR,更新根节点。

(3)当基点不是根节点时,在更新完基点,subR和subRL的父节点后,要记得更新基点的原父节点所指向的节点。如果仅仅是让parent->_parent = subR,就会导致断链。

 如在上图中,在更新完节点30、40和50后,要让节点20的_left或_right指向节点40。

从整体上看,就可以看成是以基点为中心进行了一次左旋转,因此这种旋转方式被叫做“左单旋”。

3.2 插入较高左子树的左侧——左左->右单旋

右单旋对应的情况和左单旋相反。右单旋是用于在一棵AVL树的左子树的左侧节点上插入时使用的。同样的,以如下一棵AVL树为例子,先画出h,即高度等于不同值的情况:

 (1)当h等于0时:

向该AVL数的左子树的左侧插入,可以画出如下AVL树图:

此时50号节点的平衡因子为2,需要调整。即以50号节点为基点进行旋转,让50号节点的左子节点指向40号节点的右子节点,然后让40号节点的左子节点指向50号节点。再分别更新40号节点、50号节点和40号节点的左子节点(如果存在)的父节点。最后让50号指针的父节点指向40号节点。

 (2)当h = 1时:

 向该AVL树的左子树的左侧插入:

此时50号节点的平衡因子为-2,需要进行调整。调整方法和h=0时一样。以50号节点为基点进行旋转。先让50号节点的左子节点指向40号节点的右子树,再让40号节点的右子节点指向50号节点。然后分别跟新50、40和45号节点的父指针。最后让50号节点的原父节点指向40号节点。

通过上面的两个例子,就可以总结出当在AVL树的左子树的最左节点插入时的旋转规律。假设平衡因子为-2的节点为parent,parent的左子节点为subL,subL的右子节点为subLR。首先让parent的左子节点指向subLR,然后让subL的右子节点指向parent。然后分别更新parent、subL和subLR的父指针,最后让parent原来的父节点指向subL即可。

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

	parent->_left = subLR;//让基点指向subLR
	subL->_right = parent;//让subL指向基点

	if (subLR)//subLR节点不为空时,更新它的父指针
		subLR->_parent = parent;

	Node* ppNode = parent->_parent;
	parent->_parent = subL;//更新基点的父指针,指向subL
	subL->_parent = ppNode;//更新subL的父指针,指向基点的原父节点

	if (ppNode == nullptr)//基点的原父节点为空,说明基点为根节点
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent)//基点为原父节点的左子节点
			ppNode->_left = subL;
		else//基点为原父节点的右子节点
			ppNode->_right = subL;
	}

	parent->_bf = subL->_bf = 0;//更新平衡因子
}

和左单旋一样,右单旋后的AVL树的只有parent和subL的平衡因子被改变。且通过观察可以发现,这两个节点的平衡因子都被改为了0

3.3 插入较高右子树的左侧节点——右左->右左双旋

上面的两种情况都是只需要单次旋转。但是在某些情况下,可能需要进行二次旋转。

以下图的AVL树为例,h代表的是子树的高度。

(1)当h = 0时:

 在该AVL树的右子树的左侧插入:

可以看见,左单旋的情况不同,左单旋是在右子树的最右子节点的右边插入,而这里却是在左边插入。

在这种情况下,单旋就不再起作用,而是需要进行双旋转。

要将这个树调整为AVL树,第一步要以30节点为基点进行右单旋。即让25号节点的右子节点指向30号节点,让30号节点的左子节点指向25号节点的右子节点。再更新对应节点的父指针和让25号节点与20号节点相链接。

 通过这种方法,就让这棵树的结构变成了可以使用左单旋的结构。

因此第二步就是进行左单旋。即让25号节点的左子节点指向20号节点,然后让20号节点的右子节点指向25号节点的左子节点。再更新对应节点的父指针和让25号节点与20号节点的原父节点相链接。

 通过右左两次单旋,就将这棵树调整为了AVL树。

(2)当h = 1时:

在这个AVL树的右子树的左侧插入:

可以看到,该树的20号节点的平衡因子为2,需要调整。同时它插入的节点在右子树的最右节点的左侧。这就导致单次旋转失效。需要进行双旋。

 和h=0时一样,首先以30号节点为基点进行右旋。让30号节点的左子节点指向25号节点的右子节点,再让25号节点的右子节点指向30号节点,然后更新对应节点的父指针,并让30号节点的原父节点20号节点指向25号节点。

第二步进行左单旋。以20号节点为基点进行左单旋。首先让25号节点的左子节点指向20号节点,然后让20号节点的右子节点指向23号节点,再更新20、25、23号节点的父指针,然后让20号节点的原父节点指向25号节点。

通过上面的两个例子,就可以总结出当在AVL树的右子树的左侧插入节点时的旋转规律。首先以平衡因子为2的节点的右节点为基点,进行右单旋;然后再以平衡因子为2的节点为基点,进行左单旋。

旋转完后,需要对平衡因子进行更新。一共分为三种情况。设平衡因子为2的节点为parent,parent的右子节点为subR,subR的左子节点为subRL。

情况一:在右子树的左侧节点的左子节点插入(subRL的平衡因子 == -1):

 以上图为例,在进行第一次右单旋时,25号节点的右子节点指向30号节点,30号节点的左子节点指向25号节点的右子节点,该节点为空。这就导致25号节点的平衡因子变为1,30号节点的平衡因子也变为1,而20号节点的平衡因子变为2。当进行第二次左单旋后,25号节点的左子节点指向20号节点,20号节点的右子节点指向25号节点的左子节点,进而导致30号节点的平衡因子改变为1,25号和20号节点的平衡因子改变为0。

因此,当subRL的平衡因子为-1时,subR的平衡因子修改为1,parent和subRL的平衡因子修改为0

情况二:在右子树的左侧节点的右子节点插入(subRL的平衡因子 == 1):

上图的情况就是在右子树的左侧的右子节点插入。在进行第一次右单旋后,25号节点的右子节点指向30号节点,30号节点的左子节点指向25号节点的左子节点27号节点,此时就会导致25号节点的平衡因子修改为1,30号节点的平衡因子修改为0,20号节点的平衡因子修改为2。然后进行第二次左单旋。让25号节点的左子节点指向20号节点,20号节点的右子节点指向25号节点的左子节点,该节点此时为空。这就导致20号节点的平衡因子修改为-1,30号节点的平衡因子修改为0,25号节点的平衡因子修改为0

因此,当subRL的平衡因子为1时,parent的平衡因子修改为-1,subR和subRL的平衡因子修改为0

情况三:插入的节点就是它本身(subRL的平衡因子 == 0):

 如上图,此时插入后引起调整的节点就是插入的subRL自己。此时的情况很简单,parent、subR和subRL的平衡因子全部置为0

void RotateRL(Node* parent)//右左双旋
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	RotateR(parent->_right);
	RoatteL(parent);

	//更新parent、subR、subRL的平衡因子
	if (bf == -1)//在subRL的左子树插入
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->__bf = 0;
	}
	else if (bf == 1)//在subRL的右子树插入
	{
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == 0)//subRL就是引起调整的节点
	{
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else
		assert(false);//走到这里,说明出现错误
}

右单旋和左单旋的代码实现在上面中已经写过了,所以此处直接复用即可。

3.4 在左子树的右侧插入——左右->左右双旋

以以下一棵AVL树为例:

 h为该子树的高度。

(1)当h = 0时:

在该AVL树的左子树的右侧插入节点:

 此时50号节点的平衡因子为-2,需要调整。要注意,这一情况和右单旋的情况是相反的。

 右单旋的需要旋转的节点可以看成直线,而需要进行双旋的节点可以看成曲线。

这种情况下,需要进行两次旋转。首先是以40号节点进行左单旋。首先让45号节点的左子节点指向40号节点,然后让40节点的右子节点指向45号节点的左子节点,再更新对应节点的父指针,最后让40号节点的原父节点指向45号节点。

接着进行第二次单旋。以50号节点进行右单旋。首先让45号节点的右子节点指向50号节点,然后让50号节点的左子节点指向45号节点的右子节点,再更新对应节点的父指针,最后让50号节点的原父节点指向45号节点。

 (2)当h = 1时:

 在该AVL树的左子树的右侧插入节点:

 此时它的50号节点的平衡因子为-2,需要调整。

第一步,以40号节点为基点进行左单旋。首先让45号节点的左子节点指向40号节点,然后让40号节点的右子节点指向45号节点的左子节点,再更新40、43、45好节点的父指针,最后让40号节点的原父节点50号节点指向45号节点。

第二步,以50号节点为基点进行右单旋。首先让45号节点右子节点指向50号节点,然后让50号节点的左子节点指向45号节点的右子节点,再更新对应节点的父指针,最后让50号节点的原父节点指向45号节点。

通过上面两个例子,就可以总结出如果在AVL树的左子树的右侧插入节点时的调整规律。首先是以平衡因子为-2的parent节点的左子节点为基点进行左单旋;然后再以parent节点为基点进行右单旋。

在旋转完成后,要记得更新平衡因子。通过观察,其实可以发现在左右双旋的情况下, 平衡因子的更新分为三种情况。

需要调整的节点的为parent,parent节点的左子节点为subL,新插入的节点为subLR

情况一:在左子树的右侧节点的左子节点插入(subLR的平衡因子 == -1):

 如上图,在左子树的右侧节点的左子节点插入导致调整,如上图的43号节点就是这种情况。此时45号节点的左侧高,在进行左单旋时,就会将43号节点给49号节点,使40号节点的平衡因子+1变为0,45节点的平衡因子由-1变2。当进行第二次右单旋时,40节点不变,由于让45号节点指向50号节点,此时平衡,45号平衡因子为0。50号节点因为指向的45号节点的右子节点为空,平衡因子被修改为1。

在subLR的平衡因子等于-1时,会有三个节点的平衡因子被修改。此时parent的平衡因子修改为1,subL和subLR的平衡因子都修改为0。

情况二:在左子树的右侧节点的右子节点插入(subLR的平衡因子== 1):

如上图,当在AVL树的左子树的右侧的右子节点插入,即在45号插入时,45号的平衡因子由0变1,40号平衡因子由0变1。50号平衡因子由-1变为-2。在经过第一次左单旋后,因为40号拿的45号节点的左子节点为空,且让45号节点的右子节点指向40号节点,此时40号节点的平衡因子由1变为-1,45号节点的平衡因子由1变为-1,50号节点平衡因子不变。当经过第二次右单旋后,50号节点接受45号节点的右子节点,45号节点指向50号节点。导致45号节点的平衡因子由-1变为0,50号节点的平衡因子由-2变为0,40号节点的平衡因子不变。

在subLR的平衡因子等于1时,会有三个节点的平衡因子被修改。subL的平衡因子修为该-1,parent和subLR的平衡因子修改为0

情况三:当插入的节点就是它本身时(subLR的平衡因子等于0):

如上图的情况,subLR就是本身就是被插入的节点,此时subLR的平衡因子为0。

这种情况就无需过多讲解了,很容易理解。此时parent、subL和subRL的平衡因子全部置为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)//subLR的左子树插入
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1)//subLR的右子树插入
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else if (bf == 0)//subLR自己就是新插入的节点
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
		assert(false);//到这里,说明程序有错误
}

右单旋和左单旋的代码实现在上面中已经写过了,所以此处直接复用即可。

3.5 总结

通过上面的分析,就可以得出一棵AVL树要进行调整,一共分为4中情况。

(1)当在AVL树的右子树的右侧插入时,采用左单旋。将parent和subL的平衡因子更新为0

(3)当在AVL树的左子树的左侧插入时,采用右单旋。将parent和subL的平衡因子更新为0

(3)当在AVL树的右子树的左侧插入时,采用右左双旋,先右单旋,再左单旋。parent、subR和subRL的平衡因子都需更新,分三种情况

(4)当在AVL树的左子树的右侧插入时,采用左右双旋,先做单旋,再右单旋。parent、subL和subLR的平衡因子都需更新,分三种情况

4.完善调整平衡因子和实现数据插入

有了调整AVL树的方法后,就可以完善调整平衡因子的方法了。同时,也可以将向AVL树插入数据的方法完成了。

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 (kv.first < cur->_kv.first)//键值小于节点上的键值,向左走
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first)//键值大于节点上的键值,向右走
		{
			parent = cur;
			cur = cur->_right;
		}
		else//传入的键值与节点上的键值相等
			return false;
	}

	cur = new Node(kv);
	if (kv.first < parent->_kv.first)//插入的键值比parent的小,插入在左边
	{
		parent->_left = cur;
		cur->_parent = parent;//三叉链链接,让子节点的parent指针指向父节点
	}
	else//插入的键值比parent的大,插入在右边
	{
		parent->_right = cur;
		cur->_parent = parent;
	}

	//更新平衡因子
	while (parent)//当parent为空,即为根节点的父节点时结束
	{
		if (cur == parent->_left)//插入在左子节点
			--parent->_bf;
		else if (cur == parent->_right)//插入在右子节点
			++parent->_bf;

		if (parent->_bf == 0)//父节点的平衡因子为0,不再需要向上更新
		{
			break;
		}		
		else if (parent->_bf == 1 || parent->_bf == -1)//父节点的平衡因子为1或-1,
		{											   //继续向上跟新
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)//父节点的平衡因子为2或-2,
		{											   //插入有问题,调整
			//调整方法
			if (parent->_bf == 2 && cur->_bf == 1)//该条件说明是在右子树的右侧插入。采用左单旋
			{
				RotateL(parent);
				break;
			}
			else if (parent->_bf == -2 && cur->_bf == -1)//该条件说明是在左子树左侧插入。右单旋
			{
				RotateR(parent);
				break;
			}
			else if (parent->_bf == 2 && cur->_bf == -1)//该条件说明该树的右子树高,且插入的节点在右子树的左侧
			{
				RotateRL(parent);//右左双旋
				break;
			}
			else if (parent->_bf == -2 && cur->_bf == 1)//该条件说明该树的左子树高,且插入的节点在左子树的右侧
			{
				RotateLR(parent);//左右双旋
				break;
			}
			else
				assert(false);//走到这里,说明平衡因子调整方法出现问题
		}
		else//来到这里,说明程序出错有问题
			assert(false);
	}

	return true;
}

void RotateL(Node* parent)//左单旋
{
	Node* subR = parent->_right;//构建节点指向基点的右子节点
	Node* subRL = subR->_left;//构建节点指向subR节点的左子节点

	parent->_right = subRL;//让基点的右节点指向cur的左子节点
	subR->_left = parent;//让cur的左子节点指向基点
			
	if(subRL)//当subRL不为空时才需要更新
		subRL->_parent = parent;//该节点此时为基点的右子节点,更新_parent指向基点

	Node* ppNode = parent->_parent;//记录基点的父节点
	subR->_parent = parent->_parent;//此时cur替代基点的位置,让cur的parent指向上一个父节点
	parent->_parent = subR;//此时基点为cur的左子节点,更新parent让其指向cur

	if (ppNode == nullptr)//parent的父节点为空,说明它为根,更新根节点
	{
		_root = subR;
		_root->_parent = nullptr;
	}	
	else//此时需要将原基点的父节点与现在的基点subR相链接,不链接的话该节点依然指向parent
	{
		if (ppNode->_left == parent)//基点为其父节点的左节点
			ppNode->_left = subR;
		else//基点为其父节点的右节点
			ppNode->_right = subR;
	}

	parent->_bf = subR->_bf = 0;//更新parent和cur节点上的平衡因子,设置为0		
}

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

	parent->_left = subLR;//让基点指向subLR
	subL->_right = parent;//让subL指向基点

	if (subLR)//subLR节点不为空时,更新它的父指针
		subLR->_parent = parent;

	Node* ppNode = parent->_parent;
	parent->_parent = subL;//更新基点的父指针,指向subL
	subL->_parent = ppNode;//更新subL的父指针,指向基点的原父节点

	if (ppNode == nullptr)//基点的原父节点为空,说明基点为根节点
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent)//基点为原父节点的左子节点
			ppNode->_left = subL;
		else//基点为原父节点的右子节点
			ppNode->_right = subL;
	}

	parent->_bf = subL->_bf = 0;//更新平衡因子
}

void RotateRL(Node* parent)//右左双旋
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	RotateR(parent->_right);
	RotateL(parent);

	//更新parent、subR、subRL的平衡因子
	if (bf == -1)//在subRL的左子树插入
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else if (bf == 1)//在subRL的右子树插入
	{
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == 0)//subRL就是引起调整的节点
	{
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else
		assert(false);//走到这里,说明出现错误
}

void RotateLR(Node* parent)//左右单旋
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;

	RotateL(parent->_left);
	RotateR(parent);

	//更新parent、subL、subLR的平衡因子
	if (bf == -1)//subLR的左子树插入
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1)//subLR的右子树插入
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else if (bf == 0)//subLR自己就是新插入的节点
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
		assert(false);//到这里,说明程序有错误
}

为了检查这棵树是否是AVL树,我们可以用以下代码来进行测试:

void _inorder(Node* root)//中序遍历
{
	if (root == nullptr)
		return;

	_inorder(root->_left);
	cout << root->_kv.first << ":" << root->_kv.second << endl;
	_inorder(root->_right);
}

int Height(Node* root)//计算子树高度
{
	if (root == nullptr)
		return 0;

	int lh = Height(root->_left);
	int rh = Height(root->_right);

	return lh > rh ? lh + 1 : rh + 1;
}

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 << "平衡因子异常" << endl;
	}

	return abs(rightHeight - leftHeight) < 2 &&
		_isBalance(root->_left) &&
		_isBalance(root->_right);
}

中序遍历用于查看排序是否正常,而isBalance()则用于查看这棵树的所有子树的高度差是否不超过1的绝对值

在最后,其实AVL树大家现在只需要知道它的插入逻辑就可以了,不需要去了解如何进行删除等操作。一是因为AVL树本身就比较难,在大家未来的笔试面试中都不会考删除相关操作,只会考AVL树的插入逻辑。二是AVL树的删除比插入还要复杂一些,学习成本有点高,在现阶段无需过多深入。当然,如果自己有兴趣,也可以尝试了解一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值