C++中的AVL树

一、AVL树存在的意义

二叉搜索树有时会退化为单支树,此时进行查找效率相当于顺序表,十分低下,为此提出平衡二叉搜索树的概念:当向二叉搜索树中插入新节点时,使左右子树的高度差不超过1

1.补:为什么不让高度差相等?

只有当节点数是(2^n)-1的时候,也就是满二叉树的时候才能左右完全相等,因此不超过1是相对最好的情况

二、一个标准的AVL树

①左右子树都是AVL树

②左右子树的高度差(又称平衡因子),绝对值不超过1(即-1/0/1),一般选择用右子树高度减左子树高度来计算平衡因子

对于一个AVL树的节点:

struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;//因为AVL树要进行旋转来调整树的高度,期间需要访问节点的父节点

	int _bf;//平衡因子
};

一棵AVL树的图示、

三、AVL树的插入

按照搜索二叉树的逻辑插入,插入之后连接上父节点,接下来需要更新平衡因子,分两种情况:

①插入在左子树,parent平衡因子--

②插入在右子树,parent平衡因子++

更新之后要考虑是否需要继续更新,这是由parent更新之后的平衡因子决定的,可以分为三种情况

①parent的平衡因子==0

这说明更新之前parent平衡因子是1/-1,插入节点插入在了矮的一边,parent所在子树高度不变,不需要向上更新了

②parent的平衡因子==1/-1

这说明更新之前parent平衡因子是0,插入节点插入在了任意一边,parent所在子树高度改变,需要向上继续更新

③parent的平衡因子==2/-2

这说明更新之前parent平衡因子是1/-1,插入节点插入在了高的一边,parent所在子树高度改变并且树的平衡被破坏,需要进行旋转处理

④parent的平衡因子不是这五个数

可以通过assert来停止执行程序,这说明之前的更新逻辑出现问题了,需要检查修改

⭐⭐⭐⭐⭐

当进行完旋转逻辑后,我们就不需要再向上更新了,此时务必break一下,否则会出现未知的错误

四、AVL树插入时的旋转

AVL树在插入时的旋转可以分为四种,

4.1左单旋

4.1.1旋转逻辑展示

旋转的原则是①保持搜索树的规则不变

②控制平衡,降低高度

就此,我们可以画出旋转的抽象图

旋转前:                                                                旋转后:

 

不难发现,30作为根节点,起初其平衡因子是2;60作为根节点右孩子,起初平衡因子是1

观察图可以发现,我们着重要处理3点:

①subRL变为parent的右边

②parent变为subR的左边

③subR变成这棵子树的根

4.1.2实例的情况:

在由抽象图对应实例的时候,

当h==0,只有两个节点和一个新插入节点,可能的情况为一种

当h==1,因为a,b,c都只有一种可能,所以对应可能的情况为一种

当h==2,因为a,b,c中,a有三种可能的情况,b有三种可能的情况,c有一种可能的情况(c只能是高度为2的满二叉树,单只树的话平衡因子一定会在更新到parent之前就出现不平衡的状况),在插入结点的时候有四种可能的情况

算下来3*3*1*4=36种可能的情况

当h>=3之后,情况的数量会出现爆炸式增长

4.1.3实现

完成上述分析后,我们就可以按照这样的思路进行旋转逻辑的实现了

①完成观察图所得出的三个节点位置改变

②完成对各节点parent关系的处理

③检验这是一整棵树还是一棵子树,进行调整后根节点subR的更新

④更新平衡因子

4.1.4代码参考

void RotateL(Node* parent)
{
	//先直接改,然后考虑每个更改后的父节点问题,最后修改平衡因子
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	subR->_left = parent;

	//然后修改父节点问题
	Node* parentParent = parent->_parent;

	//对应h=0的情况
	if (subRL != nullptr)
	{
		subRL->_parent = parent;
	}

	subR->_parent = parentParent;
	parent->_parent = subR;
	if (parentParent != nullptr)
	{

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

	}
	else
		_root = subR;

	//最后修改平衡因子
	subR->_bf = 0;
	parent->_bf = 0;

}

4.1补:

抽象例子种以30为根这棵树有可能是一整棵树,也有可能是一棵树的子树,其中60这一节点本质上是平衡因子为2的节点的右孩子节点

4.2右单旋

4.2.1旋转逻辑演示

旋转前:                                                        旋转后:

不难发现,60作为根节点,起初其平衡因子是-2;30作为根节点左孩子,起初平衡因子是-1

观察图可以发现,我们着重要处理3点:

①subLR变为parent的左边

②parent变为subL的右边

③subL变成这棵子树的根

实现过程与左单旋同理

4.2.2代码参考

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

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

	//处理父节点问题
	if (subLR)
	{
		subLR->_parent = parent;
	}
	Node* parentParent = parent->_parent;
	subL->_parent = parentParent;
	parent->_parent = subL;

	if (parentParent)
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else
		{
			parentParent->_right = subL;
		}
	}
	else
		_root = subL;

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

4.3右左双旋

4.3.1需求的变化

左单旋处理的是:                                        而现在要处理:

在这里会发现,30作为根节点,起初其平衡因子是2;60作为根节点右孩子,起初平衡因子不再是1,而变成了-1

为了将其变得平衡,我们的解决办法是先以60为parent右旋,再以30为parent左旋

4.3.2抽象情况变化

因为要以60为parent进行一次右旋,因此对于原来b部分需要进行拆分处理,

需要添加新的例子节点50,为原来b部分子树的根节点

在插入的时候(若h>0)左右孩子会影响50的平衡因子,而50的平衡因子对于最后旋转完成后平衡因子的更新有影响

①我们先假设在分割后的b部分下插入节点

此时待处理的是

经过一次右单旋

进行了这次右旋之后,树的结构变成了左单旋处理的结构,接下来只需按照左单旋的思路进行旋转即可

完成旋转后,可以发现subRL,subR,parent的平衡因子分别变成了0,1,0

②如果在分割后的c部分下插入节点

待处理的情况:                                        处理最终情况:

                  完成旋转后,可以发现subRL,subR,parent的平衡因子分别变成了0,0,-1

③如果h==0,平衡因子与前两种情况都不同            

此时待处理的情况是:                                  处理的结果是:

完成旋转后,可以发现subRL,subR,parent的平衡因子分别变成了0,0,0

4.3.3关于h范围对情况的影响

①当h==0时,是一种单独的情况,会把subRL,subR,parent的平衡因子都置为0

②当h==1时,插入位置在50的左右都可以,有两种情况,插入位置在左和右的平衡因子会出现不同情况

③当h>=2时,插入位置会变得非常多,总情况数爆炸式增长

4.3.4代码参考

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

	int tmp = subRL->_bf;

	RotateR(subR);
	RotateL(parent);

	//接下来要更新平衡因子
	if (tmp == 0)
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else if (tmp == -1)
	{
		subR->_bf = 1;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else if (tmp == 1)
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = -1;
	}
	else
		assert(false);//当走到这一步的时候一定是出问题了,因为平衡因子不该出现-1,0,1之外的值
}

4.4左右双旋

4.4.1需求改变

右单旋处理的是:                                         而现在的情况是:

在这里会发现,60作为根节点,起初其平衡因子是-2;30作为根节点左孩子,起初平衡因子不再是-1,而变成了1

为了将其变得平衡,我们的解决办法是先以30为parent左旋,再以60为parent右旋

4.4.2抽象情况变化

以30为parent左旋,需要用到30的右孩子节点,我们设为50

①假设在分割后的c部分下插入节点

待处理的情况:

一次以30为parent的左单旋

再经过一次以60为parent的右单旋

完成旋转后,可以发现subLR,subL,parent的平衡因子分别变成了0,-1,0

②假设在分割后的b部分下插入节点

待处理:                                                         处理后:

完成旋转后,可以发现subLR,subL,parent的平衡因子分别变成了0,0,1

③如果h==0,平衡因子与前两种情况都不同

此时待处理的情况是:                                 处理后:

完成旋转后,可以发现subLR,subL,parent的平衡因子分别变成了0,0,0

4.4.3代码参考

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

	int tmp = subLR->_bf;

	RotateL(subL);
	RotateR(parent);

	if (tmp == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;

	}
	else if (tmp == 1)
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else if (tmp == -1)
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
		assert(false);

}

五、判断模拟实现的树是不是平衡二叉搜索树

可以通过分别计算左右子树的高度,再把两个高度相减得到一个diff

比较abs(diff),即diff的绝对值是否>=2,如果确实大于等于,那说明树不平衡

此外再更新过程中平衡因子出现问题的概率也很大,所以有必要比较一下diff与当前根节点的平衡因子是否相同

5.补:出错时的更多调试方法

①首先对于一个选择语句多个条件判断可以选择分开他们为多个if

然后在每个if中添加打印,以此来打印以下报错信息

②在Test函数测试过程中,会用到循环测试,此时可以在循环体中打印具体情况来排查错误

③当循环次数很多的时候,我们不方便使用F10逐语句调试,此时可以在循环体中加一个条件语句,打上断点来进行调试

六、AVL树的删除(只呈现思路)

删除的总体大逻辑与插入很像,遵循这样几个原则:

①以搜索树的规则进行删除

②更新平衡因子

1>平衡因子变为0,说明原来是1/-1,子树的高度发生变化,需要继续更新

2>平衡因子变为1/-1,说明原来是0,子树高度不发生改变,无需继续更新

3>平衡因子变为2/-2,需要走旋转逻辑,此处走完旋转逻辑之后还需要继续进行更新,因为删除影响的范围往往十分广

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值