C++进阶-AVL树(平衡二叉查找树)(难度较高)

目录

1.预备知识

2.AVL的概念

3.AVL树基本结构的实现

4.AVL树的插入(最主要)

4.1大概过程

4.2平衡因子的更新

4.2.1更新原则

4.2.2更新停止条件

4.3初步代码

5.AVL树的旋转(最难)

5.1旋转的原则

5.2右单旋

5.3左单旋

5.5左右双旋

5.6右左双旋

5.7AVLTree实现Insert的全部代码

6.AVL树其他功能的实现

6.1查找的实现

6.2中序遍历的实现

6.3计算AVL树的高度

6.4判断是否为AVL树(或判断是否平衡)

6.5计算AVL树的大小

7.最终代码

8.AVL树的删除

9.总结


1.预备知识

要学习AVL树首先要了解二叉搜索树,可以见我之前的博客:

C++进阶-二叉搜索树(二叉排序树)_c++二叉树详解-CSDN博客文章浏览阅读1k次,点赞40次,收藏22次。二叉搜索树难度相对于原来的各种知识要考虑的情况更多,后面的AVL树和红黑树难度更高,手动实现的更难,不过以后面试和笔试中要考到这些实现的注意事项,所以一定不要觉得这篇二叉搜索树难就不学了,后面的手动模拟实现map和set难度更上一层楼,所以任重而道远。好了,这讲内容就到这里,下讲将讲解:C++进阶-set,喜欢的可以一键三连哦,下讲再见!!!二叉树的简单讲解(实现详见下两讲)https://blog.csdn.net/2401_86446710/article/details/145887950?_c++二叉树详解https://blog.csdn.net/2401_86446710/article/details/149307955?spm=1011.2415.3001.10575&sharefrom=mp_manage_link

2.AVL的概念

(1)AVL树是最先发明的⾃平衡⼆叉查找树,AVL可以是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的左右⼦树都是AVL树,且左右⼦树的⾼度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树,通过控制⾼度差去控制平衡。

(2)为什么称之为AVL树: AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,他们在1962年的论⽂《An algorithm for the organization of information》中发表了它。

(3)AVL树实现引⼊了平衡因⼦(balance factor)的概念,每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1,虽然AVL树并不是必须要平衡因⼦,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡,就像⼀个⻛向标⼀样。

(4)思考⼀下为什么AVL树是⾼度平衡搜索⼆叉树,要求⾼度差不超过1,⽽不是⾼度差是0呢?0不是更好的平衡吗?这是因为有些AVL树本身是一定达不到高度差为0的,比如有两个结点的二叉搜索树,那么就只能一个为根结点,另外一个为根结点的左孩子或右孩子,而这个时候根结点的右子树或左子树高度为0,而左子树或右子树的高度为1,是肯定不能达到高度差为0的。所以说AVL树并不一定达到完美的平衡,但是一定能达到最好的平衡!

(5)AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 logN ,那么增删查改的效率也可以控制在 O(logN) ,相⽐⼆叉搜索树有了本质的提升。

如以下的图就是AVL树,其中的结点上的-1/0/1都是平衡因子

3.AVL树基本结构的实现

我们实现AVL树的结点的时候要实现成key-value类型,因为这样是最符合我们AVL树的结构的,也就是说,我们的模板参数要有两个,一个为K(代表key),一个为V(代表value),同时要在二叉搜索树实现的基础上还要有一个pair用来访问key和value,此外,我们还要增加一个parent指针,因为之后要控制平衡因子,还要增加一个用来存储平衡因子的int类型的变量。

通过这些解释,我们能设计出以下结构:

cpp 复制代码
#include<assert.h>
#include<iostream>
using namespace std;
//AVLTree结点定义
template<class K,class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	//平衡因子
	int _bf;
	//构造函数
	AVLTreeNode(pair<K,V> kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{ }
};
//AVLTree的定义
template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//进行任意操作
private:
	Node* _root = nullptr;
};

以后我们将在AVLTree的public部分进行操作。

4.AVL树的插入(最主要)

4.1大概过程

(1)插⼊⼀个值按⼆叉搜索树规则进⾏插⼊;

(2)新增结点以后,只会影响祖先结点的⾼度,也就是可能会影响部分祖先结点的平衡因⼦,所以更新从新增结点->根结点路径上的平衡因⼦,实际中最坏情况下要更新到根,有些情况更新到中间就可以停⽌了,具体情况我们下⾯再详细分析;

(3)更新平衡因⼦过程中没有出现问题(_bf=0/1/-1),则插⼊结束;

(4)更新平衡因⼦过程中出现不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树的⾼度,不会再影响上⼀层,所以插⼊结束。

4.2平衡因子的更新

4.2.1更新原则

(1)平衡因子=右子树的高度-左子树的高度;

(2)只有子树的高度变化才会影响当前结点的平衡因子;

(3)插入结点,可能会增加高度,如果新增结点在parent的右⼦树,parent的平衡因⼦++;如果新增结点在parent的左⼦树,parent平衡因⼦--;

(4)parent所在⼦树的⾼度是否变化决定了是否会继续往上更新;

4.2.2更新停止条件

更新停止主要是看parent的平衡因子最终的结果的,有以下情况:

(1)更新后parent的平衡因⼦等于0,更新中parent的平衡因⼦变化为-1->0 或者 1->0,说明更新前parent⼦树⼀边⾼⼀边低,新增的结点插⼊在低的那边,插⼊后parent所在的⼦树⾼度不变,不会影响parent的⽗亲结点的平衡因⼦,更新结束。

(2)更新后parent的平衡因⼦等于1 或 -1,更新前更新中parent的平衡因⼦变化为0->1 或者 0->-1,说明更新前parent⼦树两边⼀样⾼,新增的插⼊结点后,parent所在的⼦树⼀边⾼⼀边低,parent所在的⼦树符合平衡要求,但是⾼度增加了1,会影响parent的⽗亲结点的平衡因⼦,所以要继续向上更新。

(3)更新后parent的平衡因⼦等于2 或 -2,更新前更新中parent的平衡因⼦变化为1->2 或者 -1->-2,说明更新前parent⼦树⼀边⾼⼀边低,新增的插⼊结点在⾼的那边,parent所在的⼦树⾼的那边更⾼了,破坏了平衡,parent所在的⼦树不符合平衡要求,需要旋转处理,旋转的⽬标有两个:1、把parent⼦树旋转平衡。2、降低parent⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不需要继续往上更新,插⼊结束。

(4)不断更新,更新到根,根的平衡因⼦是1或-1也不再继续往上更新了。

4.3初步代码

通过以上两点,我们可以写出初步代码:

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	//如果AVL树是空树,那么就直接创建完根结点就结束
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	Node* parent = nullptr;
	Node* cur = _root;
	//按照二叉搜索树的相同逻辑进行插入操作(不更新平衡因子)
	while (cur)
	{
		//我们要比较的是二者的first大小,所以要:cur->_kv.first与kv.first(新插入的结点)
		//新插入的结点更大,往右子树走
		if (cur->_kv.first < kv.first)
		{
			//要更新parent,之后要更新平衡因子
			parent = cur;
			cur = cur->_right;
		}
		//新插入的结点更小,往左子树走
		else if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		//不能插入相同的值的话,我们就只能return false;
		else
		{
			return false;
		}
	}
	//这个时候插入结束代表cur=nullptr
	//这个时候cur的位置就是新增结点的位置
	cur = new Node(kv);
	//这个时候我们还需要更新cur与parent的关系
	if (parent->_kv.first > kv.first)
	{
		//为左孩子
		parent->_left = cur;
	}
	else
	{
		//为右孩子
		parent->_right = cur;
	}
    //记得把cur->_parent=parent;
    cur->_parent = parent;
	//更新平衡因子
	//循环结束条件就是parent==nullptr(也可以写成cur==_root)时
	while (parent)
	{
		//先调整parent到更新后的结果
		if (cur == parent->_left)
		{
			--parent->_bf;
		}
		else
		{
			++parent->_bf;
		}
		//再判断需不需要继续更新以及调整
		//平衡因子为0,结束循环
		if (parent->_bf == 0)
		{
			break;
		}
		//平衡因子为1/-1,继续往上更新
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			cur = parent;
			parent = cur->_parent;
		}
		//平衡因子为2/-2,需要进行旋转操作
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			//旋转操作
			//之后代码写的地方


			//旋转后结果直接结束循环
			break;
		}
		//代表之前平衡因子更新有问题,之前AVL树就已经不平衡了
		else
		{
			assert(false);
		}
	}
	//到这里也是插入成功的
	return true;
}

5.AVL树的旋转(最难)

5.1旋转的原则

(1)保持搜索树的规则;

(2)让旋转的树从不满⾜变平衡,其次降低旋转树的⾼度。

其中旋转分为四种:左单旋/右单旋/左右双旋/右左双旋。

我将根据各种旋转对应的情况来分析。

5.2右单旋

插入新结点之前:

其中a、b、c的高度都为h(h>=0)。

插入新结点之后:

新增结点之后,a的高度变成了h+1,导致了parent的平衡因子变成了-2,这种情况我们就要改变成这样:

我们只关心变化指针的指向的部分,比如:10的左孩子原来是5结果现在指向了5的右孩子,此外,5和10的两个关系改变了,5从10的左孩子变成了10的父亲,10从5的父亲变成了5的右孩子,且这个时候5和10的平衡因子都变成了0。

这个时候我们还要考虑一下,原来10的parent存不存在,如果存在,那么就需要改变10的parent的孩子指向,且还要判断10原来是parent的左孩子还是右孩子,这个时候就要多判断一步了,如果说没有,就没必要考虑。

我们另5这个结点为subL,则b这个结点为subLR,则这个时候可以:

在Inert函数加上:

cpp 复制代码
//右单旋
if (parent->_bf == -2 && cur->_bf == -1)
{
				//我们之后的右左和左右单旋可能要用到这个右单旋,所以先写成函数形式
				RotateR(parent);
}

在AVLTree类中加上:

cpp 复制代码
//右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	//10的左孩子变成5的右孩子
	parent->_left = subLR;
	//5的右孩子变成10
	subL->_right = parent;
	//改变每个孩子的父母指向
	//h可能为0(即b和c可能不存在)
	//所以要先判断一下
	//b存在
	if (subLR)
	{
		//b的父亲变成10
		subLR->_parent = parent;
	}
	//其次还可能parent->_parent==nullptr
	//即parent==_root时,就要直接改变5的父亲是nullptr即可
	//否则,我们就要改变10的原父亲的孩子指向
	Node* parentParent = parent->_parent;
	parent->_parent = subL;
	if (parent == _root)
	{
		//把5的父亲置为nullptr
		subL->_parent = nullptr;
		//并把_root置为subL;
		_root = subL;
	}
	else
	{
		//原来的10结点是其父亲的左孩子
		if (parentParent->_left == parent)
		{
			//原来的10结点的左孩子变成5
			parentParent->_left = subL;
		}
		else
		{
			//原来的10结点的右孩子变成5
			parentParent->_right = subL;
		}
		//5的父亲变成原来的10结点的父亲
		subL->_parent = parentParent;
	}
	//改变二者的平衡因子(subL、parent)
	subL->_bf = parent->_bf = 0;
}

这个右单旋的改变如果不结合图形来理解,那么最终会导致这些代码看不懂的,建议各位看代码之前先要理解一下如何旋转的!

5.3左单旋

新增结点之前:

其中a、b、c的高度为h(h>=0)。

新增结点之后会变成:

也就是说:当parent->_bf==2 并且当cur->_bf==1时,这种情况就要用到左单旋!

这个时候我们左单旋最终的结果是:

同样,我们需要观察其指向的变化:

首先10的右孩子从15变成了b,15的左孩子从b变成了10,如果h在不为0的情况下(即b不为nullptr),还需要改变b的父亲指向成10,然后还要注意:10的父亲从原来的父亲变成了15,15的父亲从原来的10变成了10原来的父亲,如果10原来的父亲存在的情况下(10原来的父亲不是nullptr),也需要改变10原来的父亲的孩子指向,这个完全可以类似于右单旋,最后再改变15和10的平衡因子为0。

在Insert中加:

cpp 复制代码
//左单旋
else if (parent->_bf == 2 && cur->_bf == 1)
{
				RotateL(parent);
}

我们可以令10为parent,15为subR,b为subRL,则在AVLTree类中加:

cpp 复制代码
//左单旋
void RotateL(Node* parent)
{
	//15结点
	Node* subR = parent->_right;
	//b
	Node* subRL = subR->_left;
	//先改变孩子的指向
	//15的右孩子变成b
	parent->_right = subRL;
	//再改变b的父亲
	//如果高度h不为0才进行操作
	if (subRL)
	{
		subRL->_parent = parent;
	}
	//10的左孩子变成15
	subR->_left = parent;
	//我们先存储起来parent->_parent,不然很容易绕晕
	Node* parentParent = parent->_parent;
	//改变10的父亲为15
	parent->_parent = subR;
	//如果parentPrent不存在即parent==_root时
	if (parent == _root)
	{
		//15的父亲变成nullptr
		subR->_parent = nullptr;
		//并把_root置为subR
		_root = subR;
	}
	else
	{
		//10为原来为父亲的左孩子
		if (parentParent->_left == parent)
		{
			//则10原来的父亲的左孩子变成了15
			parentParent->_left = subR;
		}
		else
		{
			parentParent->_right = subR;
		}
		//还要记得改变15的父亲为parentParent
		subR->_parent = parentParent;
	}
	//改变平衡因子
	subR->_bf = parent->_bf = 0;
}

这个可以根据右单旋进行改变指向,但是一定不要遗漏什么!

5.5左右双旋

没有增加结点之前的图形为:

a、b、c的高度为h(h>=0)。

插入结点后,变成:

这个时候我们要处理的情况更多了,当h为0时,假设是这样的:

那么就要变成这样:

当h不为0,原图可以(假设)变成:

则这样又延伸出两种情况:

(1)e新增时:

这个时候要先变成:

这就相当于我们把5看成parent的左单旋。

再改变成这样:

这也就相当于把5和a和e看成右单旋中的a,再把10看成parent,完成右单旋操作。

(2)f新增时

会变成如下形式:

我们就可以先变成这样:

再变成这样:

这个对齐方式都是差不多的,也就是说不管怎么样都是需要先进行左单旋,再进行右单旋,所以称之为左右双旋,第一个是传递parent->_left为参数进行的左单旋,第二个是传递parent为参数的右单旋,所以在Insert函数可以这样添加:

cpp 复制代码
//左右双旋
else if (parent->_bf == -2 && cur->_bf == 1)
{
				RotateLR(parent);
}

如果单纯的调用左单旋函数和右单旋函数这种方式那么就会造成平衡因子混乱的问题,所以我们需要把8的平衡因子分情况讨论(直接令8的平衡因子为bf):

(1)bf=0时,代表就是h=0时,那么这个时候我们观察一下,10和5的平衡因子都变成了0;

(2)bf=1时,代表就是f高度变化时,这个时候我们可以通过观察发现,10的平衡因子为0,5的平衡因子为-1;

(3)bf=-1时,代表就是e高度变化时,这个时候我们可以通过观察发现,10的平衡因子为1,5的平衡因子为0。

我们还是用原来的命名方式,把10作为parent,5作为subL,8作为subLR,这个时候我们可以写出以下代码:

cpp 复制代码
//左右双旋
void RotateLR(Node* parent)
{
	//将5作为subL
	Node* subL = parent->_left;
	//将8作为subLR
	Node* subLR = subL->_right;
	//这个时候就要记住subLR->_bf的值了,因为之后左单旋和右单旋会改变平衡因子
	int bf = subLR->_bf;
	//左单旋
	RotateL(parent->_left);
	//右单旋
	RotateR(parent);
	//改变平衡因子
	if (bf == 0)
	{
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else
	{
		//平衡因子有问题
		assert(false);
	}
	//记得把8的平衡因子也改一下
	subLR->_bf = 0;
}

5.6右左双旋

在新增结点之前:

其中a、b、c的高度为h(h>=0)。

新增结点之后:

通过观察parent->_bf==2且cur->_bf==-1时满足右左双旋。

右左双旋分为以下情况

(1)h为0时,这个时候可以类似于没新增结点是这样:

新增结点后变成这样:

这个时候我们很容易发现此时的13结点的_bf=0,而这样第一步为:

这也就相当于把15看做parent的右单旋(15没有右孩子),

最后转化成:

这也就相当于把15作为parent的左单旋。

这样画虽然说简单,但是也很难看出来是右左双旋。

h>=1时,我们在插入之前看成如下形式:

这个时候若b(e+f+13)高度变成h+1,那么就有两种情况了。

(2)e的高度变成h时:

那么第一步旋转就会有:

这一步也就相当于我们把15看成parent的右单旋,也就是这样调用:RotateR(parent->_right);

最终变成:

这就相当于调用RotateL(parent);的左单旋。

这样的结果也是把parent->_bf=0;subR->_bf=1;subRL->_bf=0;

(3)把f变成高度为h:

这个时候的subRL->_bf=1;

这个时候我们就需要先旋转成这样:

这个是RotateR(parent->_right);的右单旋。

最后旋转成这样:

这个时候相当于RotateL(parent);的左单旋,此时parent->_bf=-1;subR->_bf=0;subRL->_bf=0;

根据这些分析,最终在Insert函数里增加:

cpp 复制代码
//右左双旋
else if (parent->_bf == 2 && cur->_bf == -1)
{
				RotateRL(parent);
}
//平衡因子有问题,不平衡
else
{
				assert(false);
}

在AVLTree类里面加:

cpp 复制代码
void RotateRL(Node* parent)
{
	//把15作为subR
	Node* subR = parent->_right;
	//把13作为subRL
	Node* subRL = subR->_left;
	//存储此时13的平衡因子
	int bf = subRL->_bf;
	//右单旋
	RotateR(parent->_right);
	//左单旋
	RotateL(parent);
	//h=0时
	//所有的平衡因子都为0
	if (bf == 0)
	{
		parent->_bf = 0;
		subR->_bf = 0;
	}
	//f增加
	//情况3
	else if (bf == 1)
	{
		parent->_bf = -1;
		subR->_bf = 0;
	}
	//e增加
	//情况2
	else if (bf == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
	}
	//AVL树不平衡
	//平衡因子非法
	else
	{
		assert(false);
	}
	//不要忘记
	subRL->_bf = 0;
}

5.7AVLTree实现Insert的全部代码

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	//如果AVL树是空树,那么就直接创建完根结点就结束
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	Node* parent = nullptr;
	Node* cur = _root;
	//按照二叉搜索树的相同逻辑进行插入操作(不更新平衡因子)
	while (cur)
	{
		//我们要比较的是二者的first大小,所以要:cur->_kv.first与kv.first(新插入的结点)
		//新插入的结点更大,往右子树走
		if (cur->_kv.first < kv.first)
		{
			//要更新parent,之后要更新平衡因子
			parent = cur;
			cur = cur->_right;
		}
		//新插入的结点更小,往左子树走
		else if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		//不能插入相同的值的话,我们就只能return false;
		else
		{
			return false;
		}
	}
	//这个时候插入结束代表cur=nullptr
	//这个时候cur的位置就是新增结点的位置
	cur = new Node(kv);
	//这个时候我们还需要更新cur与parent的关系
	if (parent->_kv.first > cur->_kv.first)
	{
		//为左孩子
		parent->_left = cur;
	}
	else
	{
		//为右孩子
		parent->_right = cur;
	}
	//更新平衡因子
	//循环结束条件就是parent==nullptr(也可以写成cur==_root)时
	while (parent)
	{
		//先调整parent到更新后的结果
		if (cur == parent->_left)
		{
			--parent->_bf;
		}
		else
		{
			++parent->_bf;
		}
		//再判断需不需要继续更新以及调整
		//平衡因子为0,结束循环
		if (parent->_bf == 0)
		{
			break;
		}
		//平衡因子为1/-1,继续往上更新
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			cur = parent;
			parent = cur->_parent;
		}
		//平衡因子为2/-2,需要进行旋转操作
		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;
		}
		//代表之前平衡因子更新有问题,之前AVL树就已经不平衡了
		else
		{
			assert(false);
		}
	}
	//到这里也是插入成功的
	return true;
}

6.AVL树其他功能的实现

6.1查找的实现

AVL树查找类似于二叉搜索树的查找,所以代码如下:

cpp 复制代码
//AVL树的查找
bool Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first < key)
		{
			cur = cur->_right;
		}
		else if (cur->_kv.first > key)
		{
			cur = cur->_left;
		}
		else
		{
			return true;
		}
	}

	return false;
}

6.2中序遍历的实现

中序遍历本质上是不会需要传参的,但是标准的中序遍历的实现是需要传参的,这里我们可以借助一个辅助函数来完成,我们调用辅助函数来实现(主要是外部无法访问到_root):

cpp 复制代码
//AVL树的中序遍历
//辅助函数
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}
//实现函数
void _InOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}

	_InOrder(root->_left);
	cout << root->_kv.first << " ";
	_InOrder(root->_right);
}

6.3计算AVL树的高度

在二叉树时,我们知道,想要知道左子树的高度,就是先递归左子树后递归右子树,最终返回二者最大值+1,因为我们还要算进根结点的高度,所以最终代码为:

cpp 复制代码
//计算高度
//辅助函数
int Height()
{
	return _Height(_root);
}
//实现函数
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;
}

6.4判断是否为AVL树(或判断是否平衡)

这个函数的实现也是需要传递参数_root的,所以也要借助一个辅助函数。

实现这个功能时,我们需要用abs算法判断平衡因子大小是否在正常范围(abs(bf)<=1为正常),但是有这么多路径,不可能每一条路径都要判断平衡因子是否正常吧!

我们可以先计算出左子树的高度,再计算出右子树的高度,如果二者相差的绝对值大于等于2就一定是不平衡的。光这样是不够的,因为我们判断高度差没有用处,万一平衡因子有问题,那么之后很难解决,所以我们还要额外增加一个判断:右子树-左子树的高度差是否与平衡因子相同,如果不相同就是不平衡的,反之为平衡的。

所以最终代码如下:

cpp 复制代码
//判断AVL树是否平衡
//辅助函数
bool IsBalanceTree()
{
	return _IsBalanceTree(_root);
}
//实现函数
bool _IsBalanceTree(Node* root)
{
	// 空树也是AVL树
	if (nullptr == root)
		return true;
	// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;
	// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
	// pRoot平衡因子的绝对值超过1,则一定不是AVL树
	if (abs(diff) >= 2)
	{
		cout << root->_kv.first << "高度差异常" << endl;
		return false;
	}
	if (root->_bf != diff)
	{
		cout << root->_kv.first << "平衡因子异常" << endl;
		return false;
	}
	// pRoot的左和右如果都是AVL树,则该树一定是AVL树
	return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

6.5计算AVL树的大小

我们计算AVL树的大小,可以用一个?:表达式来求得结果,先前面用判断传入的参数是否是nullptr的,如果是,就为0,否则递归左右子树求得大小最后再加1,(加上根结点),所以最终代码如下:

cpp 复制代码
//计算AVL树的大小
//辅助函数
int Size()
{
	return _Size(_root);
}
//实现函数
int _Size(Node* root)
{
	return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
}

7.最终代码

代码非常长,自己写的时候一定要注意,如果是自己敲出的代码记得问deepseek是否有问题。

cpp 复制代码
#include<assert.h>
#include<iostream>
using namespace std;
//AVLTree结点定义
template<class K,class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	//平衡因子
	int _bf;
	//构造函数
	AVLTreeNode(pair<K,V> kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{ }
};
//AVLTree的定义
template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//进行任意操作
	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//10的左孩子变成5的右孩子
		parent->_left = subLR;
		//5的右孩子变成10
		subL->_right = parent;
		//改变每个孩子的父母指向
		//h可能为0(即b和c可能不存在)
		//所以要先判断一下
		//b存在
		if (subLR)
		{
			//b的父亲变成10
			subLR->_parent = parent;
		}
		//其次还可能parent->_parent==nullptr
		//即parent==_root时,就要直接改变5的父亲是nullptr即可
		//否则,我们就要改变10的原父亲的孩子指向
		Node* parentParent = parent->_parent;
		parent->_parent = subL;
		if (parent == _root)
		{
			//把5的父亲置为nullptr
			subL->_parent = nullptr;
			//并把_root置为subL;
			_root = subL;
		}
		else
		{
			//原来的10结点是其父亲的左孩子
			if (parentParent->_left == parent)
			{
				//原来的10结点的左孩子变成5
				parentParent->_left = subL;
			}
			else
			{
				//原来的10结点的右孩子变成5
				parentParent->_right = subL;
			}
			//5的父亲变成原来的10结点的父亲
			subL->_parent = parentParent;
		}
		//改变二者的平衡因子(subL、parent)
		subL->_bf = parent->_bf = 0;
	}
	//左单旋
	void RotateL(Node* parent)
	{
		//15结点
		Node* subR = parent->_right;
		//b
		Node* subRL = subR->_left;
		//先改变孩子的指向
		//15的右孩子变成b
		parent->_right = subRL;
		//再改变b的父亲
		//如果高度h不为0才进行操作
		if (subRL)
		{
			subRL->_parent = parent;
		}
		//10的左孩子变成15
		subR->_left = parent;
		//我们先存储起来parent->_parent,不然很容易绕晕
		Node* parentParent = parent->_parent;
		//改变10的父亲为15
		parent->_parent = subR;
		//如果parentPrent不存在即parent==_root时
		if (parent == _root)
		{
			//15的父亲变成nullptr
			subR->_parent = nullptr;
			//并把_root置为subR
			_root = subR;
		}
		else
		{
			//10为原来为父亲的左孩子
			if (parentParent->_left == parent)
			{
				//则10原来的父亲的左孩子变成了15
				parentParent->_left = subR;
			}
			else
			{
				parentParent->_right = subR;
			}
			//还要记得改变15的父亲为parentParent
			subR->_parent = parentParent;
		}
		//改变平衡因子
		subR->_bf = parent->_bf = 0;
	}
	//左右双旋
	void RotateLR(Node* parent)
	{
		//将5作为subL
		Node* subL = parent->_left;
		//将8作为subLR
		Node* subLR = subL->_right;
		//这个时候就要记住subLR->_bf的值了,因为之后左单旋和右单旋会改变平衡因子
		int bf = subLR->_bf;
		//左单旋
		RotateL(parent->_left);
		//右单旋
		RotateR(parent);
		//改变平衡因子
		if (bf == 0)
		{
			subL->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else
		{
			//平衡因子有问题
			assert(false);
		}
		//记得把8的平衡因子也改一下
		subLR->_bf = 0;
	}
	void RotateRL(Node* parent)
	{
		//把15作为subR
		Node* subR = parent->_right;
		//把13作为subRL
		Node* subRL = subR->_left;
		//存储此时13的平衡因子
		int bf = subRL->_bf;
		//右单旋
		RotateR(parent->_right);
		//左单旋
		RotateL(parent);
		//h=0时
		//所有的平衡因子都为0
		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
		}
		//f增加
		//情况3
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
		}
		//e增加
		//情况2
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
		}
		//AVL树不平衡
		//平衡因子非法
		else
		{
			assert(false);
		}
		//不要忘记
		subRL->_bf = 0;
	}
	//AVL树的插入
	bool Insert(const pair<K, V>& kv)
	{
		//如果AVL树是空树,那么就直接创建完根结点就结束
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		//按照二叉搜索树的相同逻辑进行插入操作(不更新平衡因子)
		while (cur)
		{
			//我们要比较的是二者的first大小,所以要:cur->_kv.first与kv.first(新插入的结点)
			//新插入的结点更大,往右子树走
			if (cur->_kv.first < kv.first)
			{
				//要更新parent,之后要更新平衡因子
				parent = cur;
				cur = cur->_right;
			}
			//新插入的结点更小,往左子树走
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			//不能插入相同的值的话,我们就只能return false;
			else
			{
				return false;
			}
		}
		//这个时候插入结束代表cur=nullptr
		//这个时候cur的位置就是新增结点的位置
		cur = new Node(kv);
		//这个时候我们还需要更新cur与parent的关系
		if (parent->_kv.first > kv.first)
		{
			//为左孩子
			parent->_left = cur;
		}
		else
		{
			//为右孩子
			parent->_right = cur;
		}
		//记得把cur->_parent=parent;
		cur->_parent = parent;
		//更新平衡因子
		//循环结束条件就是parent==nullptr(也可以写成cur==_root)时
		while (parent)
		{
			//先调整parent到更新后的结果
			if (cur == parent->_left)
			{
				--parent->_bf;
			}
			else
			{
				++parent->_bf;
			}
			//再判断需不需要继续更新以及调整
			//平衡因子为0,结束循环
			if (parent->_bf == 0)
			{
				break;
			}
			//平衡因子为1/-1,继续往上更新
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = cur->_parent;
			}
			//平衡因子为2/-2,需要进行旋转操作
			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;
			}
			//代表之前平衡因子更新有问题,之前AVL树就已经不平衡了
			else
			{
				assert(false);
			}
		}
		//到这里也是插入成功的
		return true;
	}
	//AVL树的查找
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}

		return false;
	}
	//AVL树的中序遍历
	//辅助函数
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	//实现函数
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
	//计算高度
	//辅助函数
	int Height()
	{
		return _Height(_root);
	}
	//实现函数
	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;
	}
	//判断AVL树是否平衡
    //辅助函数
	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}
	//实现函数
	bool _IsBalanceTree(Node* root)
	{
		// 空树也是AVL树
		if (nullptr == root)
			return true;
		// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		int diff = rightHeight - leftHeight;
		// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
		// pRoot平衡因子的绝对值超过1,则一定不是AVL树
		if (abs(diff) >= 2)
		{
			cout << root->_kv.first << "高度差异常" << endl;
			return false;
		}
		if (root->_bf != diff)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}
		// pRoot的左和右如果都是AVL树,则该树一定是AVL树
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}
	//计算AVL树的大小
	//辅助函数
	int Size()
	{
		return _Size(_root);
	}
	//实现函数
	int _Size(Node* root)
	{
		return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
	}
private:
	Node* _root = nullptr;
};

我这里提供了一个测试代码,以方便你判断自己代码是否有误,代码如下:

cpp 复制代码
// 测试代码
void TestAVLTree1()
{
	AVLTree<int, int> t;
	// 常规的测试用例
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		t.Insert({ e, e });
	}
	t.InOrder();
	cout << t.IsBalanceTree() << endl;
}

8.AVL树的删除

AVL树的删除相对于插入麻烦了一些,我这里只讲解思路,因为AVL树的删除在面试中和笔试中不常考,所以感兴趣的可以自己实现一下:

(1)按照二叉搜索树的方法进行删除操作;

(2)从删除结点开始往上,更新平衡因子,如果是parent的左子树被删除,平衡因子++;如果是右子树被删除,平衡因子--;

(3)如果平衡因子为0,停止更新;

(4)如果平衡因子为1/-1,继续往上更新;

(5)如果平衡因子为2/-2,则需要进行旋转操作,旋转不是我们新增结点的旋转,至于如何选择需要各位自己去画图理解(ps:本篇博客花了我8个小时的时间,画图已经让煮波微丝了),最好还是用手去画图,电脑上的画图我是为了增强理解而来,电脑画图建议用比较好的画图软件。最后再进行更新平衡因子操作,停止更新。

(6)遇到根结点,如果根结点的平衡因子是1/-1,也停止往上更新。

基本思路和AVL树的插入差不多。

9.总结

AVL树是C++中一个特别抽象的东西,我也只是实现了AVL树的插入操作,一般情况下,如果实现删除操作,代码就差不多700行左右,AVL树要理解最重要的就是画图,只有画图才能真正理解AVL树的旋转操作,特别是手敲代码的时候,可能会遇到很多的错误,包括但不限于:遗漏步骤、条件判断错误、更新平衡因子不全的问题。虽然说我的代码现在是经过测试过没问题的,但是我还是找了很久才找到错误的,这种大程序建议各位能测试一个函数就测试一个函数,否则最后一起测试找错误难度很高!

虽然AVL树难度高,但是只要学会了AVL树,你也会发现AVL树其实也就那样,学C++就是要靠理解其内部逻辑,AVL树这种内部逻辑也就是需要画图来理解的,因为你自己设计出来一个AVL树进行测试那么难度比较高,各位可以根据画图+代码的形式一步一步的了解如何实现AVL树的。

好了,这一讲就到这里结束了,下一讲将进行讲解:C++进阶-红黑树(难度较高),红黑树难度也是比较高的一个,它还比较抽象,但是它又比较重要,所以建议下篇博客不要缺席哦!

喜欢的可以一键三连,下讲再见!

相关推荐
小灰灰搞电子18 分钟前
Qt Quick 粒子系统详解
开发语言·qt
wadesir22 分钟前
Python获取网页乱码问题终极解决方案 | Python爬虫编码处理指南
开发语言·爬虫·python
As_wind_23 分钟前
Go 语言学习之测试
开发语言·学习·golang
望获linux1 小时前
【Linux基础知识系列】第五十四篇 - 网络协议基础:TCP/IP
java·linux·服务器·开发语言·架构·操作系统·嵌入式软件
刚入坑的新人编程1 小时前
暑期算法训练.3
c++·算法
liupenglove1 小时前
云端【多维度限流】技术方案设计,为服务稳定保驾护航
java·开发语言·网络
平哥努力学习ing1 小时前
C语言内存函数
c语言·开发语言·算法
科大饭桶1 小时前
数据结构自学Day8: 堆的排序以及TopK问题
数据结构·c++·算法·leetcode·二叉树·c
minji...1 小时前
数据结构 栈(2)--栈的实现
开发语言·数据结构·c++·算法·链表
minji...1 小时前
数据结构 栈(1)
java·开发语言·数据结构