(保姆级教学篇)AVL树的插入实现及其四种旋转

注意:本博客的平衡因子计算方式统一为右子树的高度减左子树的高度

1.简介AVL树

        当我们学习完了搜索二叉树,而紧跟着当然要开始学习AVL树啦。AVL树呢,就是搜索二叉树的一种强化版,它叫做平衡搜索二叉树,为什么会出现这种树?

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

        AVL树具有以下性质:

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

2.部分实现

        知道了以上的性质,为了发现它更独特的元素——平衡因子,那我们在实现方面和搜索二叉树有什么不同呢?(ps:对搜索二叉树实现有问题的同学可以看看我上一篇博客哦)

        由于我们的平衡因子只能出现0 1 -1,而我们插入的数据是不固定的,难免我们的平衡因子会出现2或者-2,那么我们该如何把他们再变回1或-1呢?这就来到了我们的标题——旋转。

        旋转一共分为四种,分别是左单旋、右单旋、左右旋、右左旋。在不同的场景下要选择不同的旋转方式。下面我就从AVL树的插入来一步一步引进旋转吧!

        看到了这里,我就默认你有搜索二叉树的基础啦,我们在实现插入的时候,不难发现,其实逻辑和搜索二叉树的逻辑是差不多的,只是会多了对平衡因子的处理,接下来就看看代码和注释吧:

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

template<class K>
struct AVLTreeNode
{
	K _key;
	AVLTreeNode<K>* _left;  // 右节点
	AVLTreeNode<K>* _right;  // 左节点
	AVLTreeNode<K>* _parent;  // 父亲节点
	int _bf;  // 平衡因子 balance factor

	AVLTreeNode(const K& key)
		:_key(key);
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

template<class K>
class AVLTree
{
	using Node = AVLTreeNode<K>;

	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				return false;
		}
        // 到此之前的都和搜索二叉树的逻辑一样,目的是找到插入的位置
		cur = new Node(key);  // 此时cur就是新建节点的位置,此时是空的,可以直接new,把cur当作新节点
		if (cur->_key > parent->_key)  // 如果是要插入到右边
		{
			parent->_right = cur;
			cur->_parent = parent;
			cur->_parent->_bf++;  // 唯一的不同,父亲的平衡因子要发生改变,由于是插入到右边,所以当然是加一,下面同样
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
			cur->_parent->_bf--;
		}
	}

        处理到这,我们算是把之前搜索二叉树的逻辑搞完了,新增的内容就是父亲的平衡因子要发生改变。但是你以为这就完了吗?还早着呢!难道除了父亲的会改变,爷爷不会改变平衡因子吗?祖爷不会吗?就算改变了,你能保证你爷爷祖爷的平衡因子满足条件不为2或者-2吗?所以这才是我们的开始,现在我们的任务就是继续往上更新平衡因子,代码继续:(ps:接在上述代码的后面,还是实现插入)

        接下来是什么逻辑呢?我们要写一个循环,不断的向上更新,所以当cur的父亲为空的时候再停止,那么我们该如何确定什么时候停止更新,什么时候不要更新,又什么时候要旋转呢?接下来我们就分析这个问题:

        我们知道,插入一个节点对于AVL树来说,影响最大的就是它的平衡因子,如果插入在左边,平衡因子会--,如果插入在后边,平衡因子会增大。所以我们从重要的平衡因子下手:

1.如果插入了节点cur后,parent的平衡因子是0。

        这里说明原来parent的平衡因子是1或者-1,由于插入左边或者右边导致平衡因子发生变化,所以我们可以发现之前parent是一边高一边矮的,而cur插入到了较矮的一边,实现了两边的平衡,所以我们祖先的节点平衡因子是不会发生变化的,因为插入了较矮一边,更加平衡了整棵树。

2.如果插入节点cur后,parent的平衡因子是1或者-1。

        这里说明原来parent的平衡因子是0,所以我们可以发现之前parent是两边高度一样的,插入了一个后导致了树的一边高,这里还是满足AVL树的性质的,我们只能保证parent的平衡因子不需要调整,但是我们又不能保证祖先的平衡因子是否要发生变化,因为新插入了节点会导致整个右树或者左树高度增大,很有可能祖先原来是-1或者1,从而导致变成了-2或者2,所以我们继续向上找祖先,看它的平衡因子是否需要调整

3.如果插入节点cur后,parent的平衡因子是2或者-2。

        这里说明原来parent的平衡因子是1或者-1,我们的cur插入到了两边较高的那一边,从而导致树的不平衡,这里已经违反了规则,我们需要进行调整,进行调整的方式是什么呢?也就是我们标题所说的——旋转

        根据以上的情况,我们先写一部分代码:

while (cur->_parent)
{
	if (cur == cur->_parent->_left)
		cur->_parent->_bf--;
	else
		cur->_parent->_bf++;

	if (cur->_parent->_bf == 0)  // 说明之前是1或-1,不需要往上更新
		break;	
	else if (cur->_parent->_bf == 1 || cur->_parent->_bf == -1)  // 说明之前是0,需要往上更新
		cur = cur->_parent;
	else if (cur->_parent->_bf == 2 || cur->_parent->_bf == -2) // 现在是2或-2,之前是1或者-1,已经违反了规则,需要往上更新,需要开始旋转
	{
        //...
	}
	else  // 说明出现问题了
		assert(false);
}

return true;

3.旋转

        看到了上面的 //... 了嘛,我们只要完成了那部分的代码,插入的实现代码我们就了结啦。

        进行到了这里,我们已经完成了50%了,接下来就是AVL树最重要也是最复杂的旋转部分。由于我们的不平衡,所以我们需要使它所有的子树都平衡,我们知道不平衡的原因就是某一边比另一边高了两个高度,所以为了让他平衡,就是要把太高的那部分降下来,所以就是旋转的本质。接下来我们就分类讨论有哪几种情况需要旋转。

3.1右单旋

        如果我们新插入的节点在较高左子树的左侧,我们就要进行右单旋,使左侧平衡:

在这种情况下,对于parent的左边比右边高了两个高度,所以解决方案就是将60下沉,让subL做根,这样就能使右边的高度也变成h+1,而左边的高度也会是h+1,这样不就实现了平衡嘛。如下:

经过了这样的旋转之后,就能完美的解决问题啦!

那么我们的代码该如何编写呢?核心逻辑无非是那两个,一是各个节点之间的关系要改变,像我定义了每个节点的parent,所以要改变父亲是谁,要改变儿子是谁,要改变父亲的父亲的儿子要换成谁;二是平衡因子要改变。

其次还有一个特殊的问题就是,如果我们的60是根呢?也就是我们parent节点没有父亲了呢?这就说明我们的root节点要改变换人,还要把新根的父亲置为空,这里面的逻辑十分复杂,要考虑的情况特别多,同学们要谨慎哦!

话不多说,我们来把想的来实现为代码,拥有了上述的思路和图的辅助,写代码还是不难的:

void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* pparent = parent->_parent;  // 父亲的父亲
	subL->_right = parent;
	if (subLR)  // 有可能为空
		subLR->_parent = parent;
	parent->_left = subLR;
	parent->_parent = subL;
	if (pparent == nullptr)  // 说明是根
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else  // 是子树
	{
		if (parent == pparent->_left)  // parent是左子树
			pparent->_left = subL;
		if (parent == pparent->_right)  // parent是右子树
			pparent->_right = subL;
		subL->_parent = pparent;
	}
	parent->_bf = subL->_bf = 0;  // 我们的高节点下沉以后,平衡因子是一定为0的
}
3.2左单旋

        如果我们新插入的节点是较高右子树的右侧,我们就要实现左单旋,来使其平衡。

        有了右单旋的基础,我们再来看左单旋,其实就是一种镜像版,来看图就知道了:

而这里我们要实现的就是高的那边左下沉,实现后如下:

这里就不需要多说了吧,和右单旋是一样的道理,就直接代码咯:

void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* pparent = parent->_parent;
	parent->_right = subRL;  // 链接parent的右节点,若subRL是空也没事
	if (subRL)
		subRL->_parent = parent;  // 链接subRL的父亲,如果是空肯定没有父亲
	subR->_left = parent;  // 链接subR的左孩子
	parent->_parent = subR;  // 链接parent的父亲
	// 开始处理根节点与新根的关系
	if (pparent == nullptr)  // 如果原本parent的父亲是空,说明parent是根节点
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (pparent->_left == parent)
			pparent->_left = subR;
		else if (pparent->_right == parent)
			pparent->_right = subR;
		subR->_parent = pparent;
	}
	parent->_bf = subR->_bf = 0;
}
3.3左右旋

        当新节点插入到较高左子树的右侧,那么我们该如何是好呢?我们先看看图再分析分析:

        我们能看到,原本60的平衡因子是0的,因为两边都是h-1,而30的平衡因子原来也是0的,因为两边都是h,而90原来是-1的,因为右边是h,左边是h+1,而我们新曾了这个节点之后,60变为了-1,30变为了1,而90变为了-2,这就导致了我们的违反规则。

        还是一样,我们的想法还是:太高的往下沉,记住这个思想就好了,90不是右边只有h,左边有h+2嘛,我们就让他们都变成h+1,这样不就实现平衡了嘛,那我们看看,哪个节点适合做新的根呢?我们发现,只有60是比30大,但是又比90小的,那么我们是不是可以把60推上去做根,而且我们也可以发现,如果把60推上去了,90就会在60的右边,那么右边的高度将会是h+1,而30会在60的左边,那么左边的高度也会是h+1,好像会实现平衡诶,所以为了把60推上去,我们需要先把60进行左单旋,结果如下:

很好,60离根只有一步之遥了,接下来我们需要把90下来,让60上去,所以我们需要对90进行一次右单旋,结果如下:

是不是很神奇,当进行两次旋转之后,竟然达到了平衡,相信你们肯定认为代码会很复杂,其实不是的,只是进行了两次旋转,但是这个过程是非常难想的,我们只是站在了巨人的肩膀上,那么我们旋转完后最复杂的点在于哪里呢?当然是——平衡因子!

我们的平衡因子经过这么多的变化,到底是这么变化的呢?有什么规律嘛?答案是有的,比如左右旋,根据上图发现,我们把60做为了根节点,而它原来的左子树给了30的右子树,原来的右子树给了90的左子树,而我们原来的b和c的高度都是h-1,新增后b的高度变为了h,而我们插入到了具有高度为h的左子树的30节点的右子树上,刚刚好实现了30节点的平衡,所以30节点的平衡因子一定是0,而我们的90节点,我们把c插入到了具有高度为h的右子树的90节点的左子树上,所以90节点的平衡因子一定是1,因为右边比左边高一,而60不用多说,平衡因子一定是0,因为右边和左边的最大高度都是h

但是别忘了,我们上图只讨论了插入在左边的情况,还可能插入在右边,所以我们又要讨论一番,而插入在右边的话就会影响60原来的平衡因子,会变为1,而最终会使30的平衡因子变为-1,90的平衡因子还是0,同学们可以自己看看上面的图然后脑补一下,很容易想出来啦

那么经过以上的分析,我们不难发现,左右旋的平衡因子其实是固定的,那么我们可以开始实现代码啦!

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;
	RotateL(parent->_left);
	RotateR(parent);
	if (bf == 0)  // 新增节点
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subL->_bf = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 1;
	}
	else
	{
		assert(false);
	}
}
3.4右左旋

        新节点插入在较高右子树的左侧,这样我们就需要右左旋了,有了左右旋的基础,再看右左旋就容易多了,也是一样的思想,把高的下沉,我们先看看图:

我们会先对90进行右旋:

再对30进行左旋,使其下沉:

这和左右旋的思路几乎一模一样,还有值得注意的是,也是有可能插入到左边的,所以继续我们的代码吧:

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;
	RotateR(parent->_right);
	RotateL(parent);
	if (bf == 0)
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subR->_bf = 0;
		subRL->_bf = 0;
		parent->_bf = -1;
	}
	else if (bf == -1)
	{
		subR->_bf = 1;
		subRL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

4.链接

        我们实现完旋转的代码之后,就可以和我们的总体部分进行链接了,经过上面的分析,我们可以发现什么时候旋转是有逻辑的、有规律的,那就是:同号单旋、异号双旋。看谁的号呢,就是看parent和cur 的号,cur就是从插入节点上来的那个cur,也就是有一个出问题的父亲的cur,所以一总结就能得出以下代码:

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);
break;

最后别忘了break哦,防止再进去变平衡因子。就会全部错误。

5.总结

        到这呢,我们的AVL树的插入代码算是完成了,你没看错,这仅仅是插入,现在认识到了它的恐怖之处了吧,我也认识到了,不多说,完整代码放在下面:

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

template<class K>
struct AVLTreeNode
{
	K _key;
	AVLTreeNode<K>* _left;  // 右节点
	AVLTreeNode<K>* _right;  // 左节点
	AVLTreeNode<K>* _parent;  // 父亲节点
	int _bf;  // 平衡因子 balance factor

	AVLTreeNode(const K& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

template<class K>
class AVLTree
{
public:

	using Node = AVLTreeNode<K>;

	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				return false;
		}

		cur = new Node(key);
		if (key > parent->_key)
			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)  // 说明之前是1或-1,不需要往上更新
				break;	
			else if (parent->_bf == 1 || parent->_bf == -1)  // 说明之前是0,需要往上更新
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) // 现在是2或-2,之前是1或者-1,已经违反了规则,需要往上更新,需要开始旋转
			{
				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);
				break;
			}
			else  // 说明出现问题了
				assert(false);
		}

		return true;
	}


private:
	// 左旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* pparent = parent->_parent;
		parent->_right = subRL;  // 链接parent的右节点,若subRL是空也没事
		if (subRL)
			subRL->_parent = parent;  // 链接subRL的父亲,如果是空肯定没有父亲
		subR->_left = parent;  // 链接subR的左孩子
		parent->_parent = subR;  // 链接parent的父亲
		// 开始处理根节点与新根的关系
		if (pparent == nullptr)  // 如果原本parent的父亲是空,说明parent是根节点
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (pparent->_left == parent)
				pparent->_left = subR;
			else if (pparent->_right == parent)
				pparent->_right = subR;
			subR->_parent = pparent;
		}
		parent->_bf = subR->_bf = 0;
	}
	// 右旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* pparent = parent->_parent;  // 父亲的父亲
		parent->_left = subLR;
		if (subLR)  // 有可能为空
			subLR->_parent = parent;
		subL->_right = parent;
		parent->_parent = subL;
		if (pparent == nullptr)  // 说明是根
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else  // 是子树
		{
			if (parent == pparent->_left)  // parent是左子树
				pparent->_left = subL;
			if (parent == pparent->_right)  // parent是右子树
				pparent->_right = subL;
			subL->_parent = pparent;
		}
		parent->_bf = subL->_bf = 0;  // 我们的高节点下沉以后,平衡因子是一定为0的
	}
	// 左右旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		if (bf == 0)  // 新增节点
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 1;
		}
		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 == 0)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)
		{
			subR->_bf = 1;
			subRL->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	
	Node* _root = nullptr;
};

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

七分七分_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值