c++数据结构之AVL树

一.概念

1.基本概念

2.高度差与效率

既然左右子树高度差绝对值控制在1以内 ,那么这棵树就接近一个满二叉树的状态,所以它的高度就是可控的,为logN 。那么增删查改效率也能控制在logN,也就是说,对比普通二叉搜索树那种极端高度差(一棵子树有接近n的高度,而另一棵几乎没有节点,效率为O(N))的情况,效率有极大的提升。

树的高度推导过程请参考笔者这篇文章

3.平衡因子

这个东西其实可有可无,只是有了它更容易观察和控制树的平衡,所以在行文时引入它。

二.AVL树的实现

1.每个节点类模板的成员变量

AVL树节点的值仍旧是一对键值对,并储存在类模板pair中。为了方便遍历和观察控制高度差,成员变量除了存值的pair,还有左节点,右节点,父节点,以及平衡因子。

2.AVL树类模板的成员变量

3.插入

①.先按二叉搜索树的规则执行插入。

Ⅰ.用cur进行遍历

cur的key与待插入节点的key进行比较,待插入的小,就代表它将插入当下cur的右子树,那么就让cur往当下自己的右子树走,在往下走之前,别忘了将当下的cur赋值给parent。就这样让cur一直迫近待插入节点的父节点。当cur为待插入节点父节点时,遍历结束。相当于cur既当此生的自己,也当来生的父亲,还充当待插入节点的信标。

Ⅱ.执行插入

先判断待插入节点与其父节点大小,然后执行插入。

Ⅲ.插入完后,要链接上_parent。
复制代码
	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 (cur->_kv.first < kv.first)
			{//cur和待插入节点的值进行比较,待插入值大于cur,cur往右子树走,小于则往左子树走。
				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;
		}
		if (parent->_kv.first < kv.first)
		{
			parent->_left = cur;
		}
		//链接父节点
		cur->_parent = parent;
	}

②.更新平衡因子

插入后,就需要去更新一下从插入节点到根节点这一路的平衡因子 ,最坏的情况是更新到根节点,一般情况是更新到这一路中途就结束了。结束的标志是平衡因子均未出现问题(为0,-1,1)。

Ⅰ.更新原则

i.平衡因子 = 右子树高度 - 左子树高度

ii.只有在插入后高度发生改变才会影响当前节点平衡因子

iii.在插入时,假如高度增加,如果将节点插在parent右边,parent的_bf++,插在左边,_bf--

iv.parent的子树高度是否发生变化,决定了是否要向上进行更新

Ⅱ.更新停止条件

上面三种情况的图示:

由于旋转很重要且较难,就将它单拎出来。

3.5旋转

①.旋转的原则

②.左单旋

在a子树中插入新节点,此时parent的_bf变成2,平衡打破。b子树中首个节点的大小介于a,c之间,如老蒋一般中正(他中正个毛),旋即将b向左扭,成为10这个节点的右子树,然后将15这个节点提上去,10这个节点的一支,就反成了15节点的左子树。

③.右单旋

右单旋也是相同原理,只是变个方向的事,将完成插入后子树的兄弟子树,b子树(这个子树的首节点大小十分中正,介于a,c子树对应的节点之间,b子树首节点比c子树的要小,所以b子树抛给parent当左子树是符合搜索树规则的),抛给当下的parent节点(10),给parent节点当左子树,然后完成插入的子树的父亲节点(cur/subLR)又提上去作根节点,最后之前这个parent节点(10)这一支只能沦落作cur的右子树。

但不免会有些疑惑,这里的a,b,c子树不过是个框架,它的内里究竟是怎样的呢?

可以发现,这几种情况在旋转之后,parent和cur的_bf都被抹平,变成了0,所以下面代码实现时,要在旋转完以后,将这二位的_bf更新为1。

h可以等于任意数,但h越大,树越复杂,就没必要一一列举了。

④.左右双旋(大boss)

Ⅰ.引子

单旋场景是纯粹的一边高(同一支上每个节点的子树全是同一边高),举个例子,右单旋中,如果旋转前subL位于Parent左边,那么新插入节点也必然在subL左边。

而以下场景,新插入节点是在subL右边的,如果再用右单旋的思路去旋转,旋转后从左边高变右边高了,那么旋转后的树仍旧无法平衡。

可以观察到,插入后,若平衡因子异号,不是纯粹的一边高(10这个节点的子树是左边高,而5这个节点的子树是右边高),那么就用不了单旋的思路。

Ⅱ.步入正题

那么该如何操作呢,我们进入双旋。

先单旋一遍,将造成一遍高一边低局面的子树的根节点进行旋转,使得单边高矮统一。

然后就是纯粹的一边高的树,将这棵树再次单旋即可得到平衡的树。

4.旋转代码实现

4-1.右单旋

①.命名规范

parent:待旋转节点(_bf绝对值变为2的节点),老的根节点

subL(cur):parent左子节点,旋转后新的根节点

subLR:subL右子节点,新插入节点的右兄弟节点

②.易忽视的小细节

Ⅰ.由于是三叉链的结构,旋转之后,还要更改指向经过变动的节点(包括subLR以及parent)的_parent的指向。

Ⅱ.subLR为空的情况。不判断会直接访问空指针。

Ⅲ.subL是新的根节点,不代表它是整棵树的根节点,它可能只是某棵子树的根节点,所以需要讨论subL是子树根节点还是整棵树根节点。

如果是整棵树根节点,subL的_parent需要指向空。

如果是子树根节点,需要改变Parent父节点(Pparent)的指向,从指向parent改成指向subL

那么如何找到Parent的父节点呢?

就需要在右旋之前或者说Parent还是根节点时,用一个新指针Pparent记录这个节点。

Ⅳ.旋转完之后,subL和parent的_bf发生变化,还需要更新

③.全部代码(小细节,坑很多)
复制代码
	void RotateR(Node* Parent)
	{//右单旋
		Node* subL = Parent->_left;
		Node* subLR = subL->_right;
		Parent->_left = subLR;
		if(subLR)
		subLR->_parent = Parent;//由于是三叉链的结构,旋转之后,还要更改_parent的指向
		Node* Pparent = Parent->_parent;//记录Parent的父亲节点
		Parent->_parent = subL;
		subL->_right = Parent;
		if (Parent == _root)//Parent节点就是整棵树根节点的情况
		{//旋转后记得更新_root
			_root == subL;
			subL->_parent = nullptr;//根节点的父节点为空。
		}
		else//不是整棵树的根
		{
			if (Pparent->_left == parent)
			{
				Pparent->_left = subL;
				subL->_parent = Pparent;
			}
			else
			{
				Pparent->_right = subL;
				subL->_parent = Pparent;
			}
		}
	}

4-2.左单旋

基本跟上面相同,倒个个儿的事。

复制代码
void RotateL(Node* Parent)
{//左单旋
	Node* subR = Parent->_right;
	Node* subRL = subR->_left;
	Parent->_right = subRL;
	if (subRL)
	{
		subRL->_parent = Parent;
	}
	Node* Pparent = Parent->_parent;
	subR->_left = Parent;
	Parent->_parent = subR;
	if (Parent == _root)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (Pparent->_left == Parent)
		{
			Pparent->_left = subR;
			subR->_parent = Pparent;
		}
		else if(Pparent->_right == Parent)
		{
			Pparent->_right = subR;
			subR->_parent = Pparent;
		}

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

4-3左右双旋

整棵树中,在插入后,平衡因子发生改变的也仅仅是Parent,subL,subLR三个。而影响其他两个节点的平衡因子的关键人物是------subLR(下面值为8的节点),可以依照新节点插入subLR子树的位置,来分成以下三种情况(确切的说是两种):

场景1

场景2

场景3

可复用上面两个单旋的代码,Parent->left(subL)先左单旋,Parent再右单旋。但困难的地方在于平衡因子的更新,照两个单旋各自的更新来操作,最后Parent,subL,subLR的平衡因子统统会被抹平成0,这显然是不对的。所以不能单纯的用单旋里的更新机制来调节双旋的平衡因子。

所以得在两个单旋之前,插入之后,用新常量bf记录subLR的平衡因子。然后在两次单旋以后,用bf按以上三种情况讨论,重写正确的平衡因子:

复制代码
if (bf == -1)
{//场景1
	subLR->_bf = 0;
	subL->_bf = 0;
	Parent->_bf = 1;
}
if else(bf == 1)
{//场景2
	subLR->_bf = 0;
	subL->_bf = -1;
	Parent->_bf = 0;
}
if else(bf == 0)
{//场景3
	subLR->_bf = 0;
	subL->_bf = 0;
	Parent->_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->_right);//单旋后三个节点_bf全部抹0,但双旋后的平衡因子绝不可能为此值。所以要记下插入后旋转前的平衡因子。
	//更新正确的平衡因子
	if (bf == -1)
	{//场景1
		subLR->_bf = 0;
		subL->_bf = 0;
		Parent->_bf = 1;
	}
	else if(bf == 1)
	{//场景2
		subLR->_bf = 0;
		subL->_bf = -1;
		Parent->_bf = 0;
	}
	else if(bf == 0)
	{//场景3
		subLR->_bf = 0;
		subL->_bf = 0;
		Parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

4-4右左双旋

基本思想和上面相似,只是这里要反一下,subR先右旋,然后Parent再左旋,然后旋转前,插入时bf,及其对应的每种旋转后三个节点_bf都与上面不同。

复制代码
void RotateRL(Node* Parent)
{//右左单旋
	Node* subR = Parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;
	RotateR(Parent->_right);
	RotateL(Parent);
	if (bf == -1)
	{
		subRL->_bf = 0;
		subR->_bf = 1;
		Parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		Parent->_bf = -1;
	}
	else if (bf == 0)
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		Parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

5.再探插入之满足各个旋转的条件

我们将具体旋转过程(单作一个接口)和插入中控制旋转分开,上面部分已阐述旋转过程以及其分类的具体代码实现,接下来回到Insert接口中,讨论插入后需旋转的情况。

5-1.接入右单旋

首先得明晰具体情境,完成插入操作后,Insert接口中的Parent对应的是右旋接口中的Parent,cur对应的是右旋接口中的subL。

那么判断为右单旋的条件就是(说明插在cur左边,左边高度增加,那么cur高度差就变成了-2):

代码:

复制代码
if (Parent->_bf == -2 && cur->_bf == -1)
{//右单旋
				RotateR(Parent);
}

5-2.接入左单旋

5-3.接入左右双旋

cur和Parent两个节点的子树,由于高的一侧相反,所以平衡因子也是相反的,因此条件是:

5-4.接入右左双旋

反一下就行:

6.求高度

递归展开图:

走一个后续遍历:

复制代码
	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;
	}

7.检测平衡

不能用类里的平衡因子检测,会监守自盗,要单独实现一个接口。单独求高度和高度差。

注意:abs()是取绝对值的意思

走一个前序遍历:

复制代码
bool _isBalanceTree(Node* root)
{
	if (root == nullptr)
	{//空树也是AVL
		return true;
	}
	//计算平衡因子
	int leftHight = _Height(root->_left);
	int rightHight = _Height(root->_right);
	int diff = rightHight - leftHight;
	//判断异常与否
	if (abs(diff) >= 2)
	{
		cout << root->_kv.first << "高度差异常" << endl;
		return false;
	}
	if (diff != root->_bf)
	{
		cout << root->_kv.first << "平衡因子异常" << endl;
		return false;
	}
	return _isBalanceTree(root->_left) && _isBalanceTree(root->_right);
}

8.查找

由于接近满二叉树,所以开销为h = logN次,效率很高。

相关推荐
博界IT精灵2 小时前
图的存储结构(哈喜老师版本)
数据结构·考研
游乐码2 小时前
c#插入排序
数据结构·算法·排序算法
大大杰哥2 小时前
leetcode hot100(2)双指针,滑动窗口
数据结构·算法·leetcode
2401_840105202 小时前
题解: [GESP202409 八级] 美丽路径
数据结构·c++·算法·动态规划
今儿敲了吗2 小时前
链表篇(五)——链表中间结点
数据结构·笔记·算法·链表
gumichef3 小时前
栈和队列(1)
开发语言·数据结构
凯瑟琳.奥古斯特3 小时前
力扣1367:二叉树中查找链表路径
数据结构·算法·leetcode·链表
qq_296553273 小时前
【LeetCode】最大子数组乘积:三种解法从暴力到最优
数据结构·算法·leetcode·职场和发展·动态规划·柔性数组
不知名的老吴3 小时前
关于C++中的placement new
数据结构·c++·算法