AVL树(高度平衡二叉搜索树)的模拟实现

目录

为什么需要存在AVL树?

AVL树的概念

AVL树的基础框架

Insert函数

实现过程

Insert的整体代码

AVL树的Insert的时间复杂度

如何检验一棵树是否为平衡二叉搜索树呢?

Erase函数

实现过程

Erase的整体代码

最终测试

AVLTree的整体代码

AVLTree的性能(应用场景)


为什么需要存在AVL树?

讲解二叉搜索树BSTree时,我们说过BSTree的缺陷,如下:

问题:对于一棵二叉搜索树,我们要查找某个节点的时间复杂度是多少呢?有人肯定抢答说是O(logN),毕竟在找目标节点时,每次能排除一半的错误选项。那是不是O(logN)呢?

答案:不是O(logN),只有在BSTree的结构比较理想的情况下去BSTree上找某个节点时,它的时间复杂度才会是O(logN),什么叫做比较理想的情况下呢?意思是:这棵BSTree上的节点分布比较均匀,近似一棵完全二叉树。假如情况不够理想,那时间复杂度会是什么样的呢?在最坏的情况下的时间复杂度是O(N),N表示节点总数。如下图左半部分所示就是一种最坏的情况,当插入节点的关键码有序或者接近有序时,二叉树就会退化成近似于是单枝树或者就是单枝树,而单枝树这样的结构和链表就没区别了;如下图右半部分所示,即使不是单枝树,但当BSTree的节点分布不够均匀时,即不近似于一个完全二叉树时,它的时间复杂度可能会略微优化,但本质上和单枝树还是一个量级。

接下来公布正确答案:在BSTree中查找一个节点的时间复杂度为O(h),h表示树的高度。

BSTree的缺陷:在上文中讲解为什么需要存在二叉搜索树的部分也说过,BSTree这样的结构存在的意义就是查找的效率比vector或者list这样的基础数据结构要高,但现在我们知道了在极端情况下,BSTree也会退化成近似于链表的单枝树,所以BSTree的查找效率比链表高就成了一个空谈,这就是BSTree的缺陷。

为了解决BSTree的缺陷,我们就会对BSTree做一个旋转操作让它保持平衡,这也是我们以后会讲解的平衡二叉搜索树,平衡二叉搜索树有两种,一种叫AVL树,一种叫红黑树。注意这里的平衡是指树的高度是平衡的,通过控制树的高度,让树平衡,对于平衡树来说,查找的时间复杂度就真的是O(logN)了,假如有1000个节点,则找某个节点真的只需要10次(2^10=1000),在100w个节点中找某个节点则只需20次,在10亿个节点中找某个节点则只需30次,而对于非平衡的BSTree来说,其效率就只能看运气了。平衡搜索二叉树本质还是搜索树,和BSTree唯一的区别:因为在同样数量的节点下平衡树的高度更低,所以查找效率更高,其他的性质都是一样的。

所以因为BSTree存在很明显的缺陷,所以才需要存在AVL树。

AVL树的概念

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

AVL树全名叫做高度平衡二叉搜索树,它是通过控制高度让二叉搜索树保持平衡的。

一棵AVL树(可能为空树),是具有以下性质的二叉搜索树:

1.左右子树高度之差(简称平衡因子)的绝对值不超过1,也就是平衡因子的值只能为-1、0、1。

2.它的左右子树都是AVL树。

注意,平衡因子并不是必须的,即实现AVL树可以不用平衡因子的方案,有其他实现AVL树的方案,但咱们这里实现AVL树时就使用平衡因子。

当前节点的平衡因子=当前节点的右子树的高度-当前节点的左子树的高度。

如下图就是一棵平衡二叉搜索树。

如下图就不是一棵平衡二叉搜索树,因为存在高度不平衡的节点,比如3、7。其他节点都是平衡的。 

如果一棵二叉搜索树的高度是平衡的,它就是AVL树。如果它有n个结点,那高度可保持在O(logN),所以搜索的时间复杂度就是O(logN)了,此时在10亿个数据中找一个数据最多只要找30次(因为2的30次方等于10亿),这已经非常高效了。

AVL树的基础框架

因为AVL树(即平衡二叉搜索树)本质也是二叉搜索树BSTree,所以AVL树的框架和BSTree的框架是大致相同的,只不过AVL树在BSTree的基础上,AVL树的节点改成了三叉链,即增加了一个指向父亲节点的指针成员,然后还增加了一个平衡因子成员,这个指向父亲节点的指针成员就是服务于平衡因子的,详情见Insert函数。

基础框架如下代码所示。

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
    AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
    AVLTree<T>()
		:_root(nullptr)
	{}

private:
	Node* _root;
};

Insert函数

实现过程

平衡二叉搜索树也是二叉搜索树,所以AVL树的插入和二叉搜索树有很多共同点。

AVL树的插入过程可以分为三步:

1. 按照二叉搜索树的方式插入新节点

2. 更新被插入节点的所有祖先的平衡因子

3.如果更新完平衡因子后,发现平衡因子的绝对值大于1,说明平衡被破坏,进行调整(旋转),如果小于1,则无需调整。当前节点的平衡因子=当前节点的右子树的高度-当前节点的左子树的高度。

所以AVLTree的插入=BSTree的插入+更新平衡因子(+旋转【这一步不一定需要做】)

回看BSTree的插入的思路,如下图。 

借助BSTree插入节点的思路我们可以编写完AVL的插入的第一阶段,如下代码所示。

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
    AVLTree<T>()
		:_root(nullptr)
	{}

	bool Insert(const T&x)
	{
		if (_root == nullptr)
		{
			_root = new Node(x);
			return true;
		}
		else
		{
			Node* cur = _root;
			Node* curParent = nullptr;
			//先找插入位置,即空位置
			while (cur!=nullptr)
			{
				if (x > cur->_data)
				{
					curParent = cur;
					cur = cur->_right;
				}
				else if (x < cur->_data)
				{
					curParent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}
			//找到空位置,开始插入
			if (x > curParent->_data)
			{
				cur = new Node(x);
				curParent->_right = cur;
				cur->_parent = curParent;
			}
			else
			{
				cur = new Node(x);
				curParent->_left = cur;
				cur->_parent = curParent;
			}
			//插入完毕后,开始控制平衡,这是Insert第二阶段的任务,未编写完毕
           
             
			return true;
		}
	}

private:
	Node* _root;
};

节点已经插入完毕了,接下来只需要探讨该如何控制树的高度平衡即可。

需要知道的几个知识点:

1.对于刚插入的新节点,它自身的平衡因子_bf肯定为0(因为没有左孩子和右孩子,所以该节点右子树的高度-左子树的高度肯定为0),所以以它为根节点的子树是不需要被调整的。

2.对于新插入的节点,它只会影响到它所有祖先的平衡因子,所以插入新节点后,可能要对它所有的祖先节点的平衡因子进行更新;如果某节点不是新插入节点的祖先,那就不会受到影响,比如下图新插入节点为10,3不是10的祖先,所以3的平衡因子一定无需进行更新,同理,1、4、0、2、6也一定无需更新平衡因子。

3.cur插入后,curParent的平衡因子一定要更新。在cur插入之前,curParent的平衡因子分为三种情况: - 1、0、1;注意不可能为-2或者+2,因为在插入cur节点前,整棵树是平衡的。

在cur插入后,分以下两种情况:其一,如果cur插入到curParent的左侧,只需给curParent的平衡因子-1即可。其二,如果cur插入到curParent的右侧,只需给curParent的平衡因子+1即可。为什么呢?因为咱们规定平衡因子的值=右子树的高度 - 左子树高度。

在cur插入后,curParent的平衡因子被更新后可能有两种情况:(注意不可能是正负2,否则在插入cur前,整棵树就不平衡了)第一是0、第二是正负1,对于这两种情况,又有两种处理方案,如下所示:

第一种处理方案:在cur插入后,如果curParent的平衡因子被更新为0(可结合下图思考),说明插入之前curParent的平衡因子一定为正负1,只是插入cur后被调整成0了,此时整棵树的高度已经平衡(原因在下一段),无需再更新curParent的所有祖先节点的平衡因子,插入cur节点成功。

问题:为什么在第一种处理方案中说整棵树的高度已经平衡了,无需再更新curParent的所有祖先节点的平衡因子呢?

答案:(如上图)注意在cur插入前,整棵树就是平衡的,所以整棵树的任意子树也都是平衡的,包括以curParent的父亲节点7为根节点的子树。在cur插入后,curParent的平衡因子变成0,这意味着以curParent为根节点的树的高度不变,而以curParent为根节点的树又是所有【以curParent的不同祖先节点为根节点】的树的子树,子树的高度不变,那这若干个父树的高度当然也不变(父树即是若干个以curParent的不同祖先节点为根节点的树),既然这些父树在插入cur前就已经是平衡的了,插入cur后这些父树的高度又没变,当然所有父树都还是高度平衡的,综上所述,插入cur后,整棵树的高度不变,整棵树依然是平衡的,所以当插入cur,然后curParent的平衡因子被调整成0后,无需再更新curParent的所有祖先节点的平衡因子。

分割线————

第二种处理方案:在cur插入后,如果curParent的平衡因子被更新为正负1(可结合上图思考),说明插入前curParent的平衡因子一定为0,只是插入cur后被更新成了正负1,这说明以curParent为根的树的高度增加了,此时就需要通过指针p向上更新一次curParent的父亲节点的平衡因子(原因在下面),即让一个指针p从curParent位置向上走一次(正是因为有这个向上走的需求,所以才在基础框架中把Node节点设置成了三叉链,记录当前节点的父亲节点的地址),然后更新一次目前指针p所指向节点的平衡因子,更新目前p指向的节点的平衡因子的规则是:如果上一次p指向的节点是目前p指向节点的左孩子,那就等价于目前p指向节点的左子树的高度增加,所以目前p指向节点的平衡因子就减1;如果上一次p指向的节点是目前p指向节点的右孩子,那就等价于目前p指向节点的右子树的高度增加,所以目前p指向节点的平衡因子就加1(因为平衡因子的值等于右子树的高度-左子树的高度)。更新完curParent的父亲节点的平衡因子后,平衡因子的值会有3种可能,对这3种可能又有各自的处理动作,关于这3种可能的详情请继续往下看。

首先要知道几个知识点。(现在假设以节点A为根节点的树叫树A,以节点B为根节点的树叫树B,以节点C为根节点的树叫树C,以节点D为根节点的树叫树D)

知识点1:给任意一个节点X插入一个孩子节点时(孩子节点可能是儿子,也可能是孙子、孙孙子),如果以节点X为根节点的树的高度不变,那所有以【节点X的祖先节点】为根节点的树的高度都不会变。

知识点2:(结合上图思考)给任意一个节点A插入一个孩子节点时(孩子节点可能是儿子,也可能是孙子、孙孙子),只要一棵树A的高度增加了,那以【节点A的父亲节点】为根节点的树B是有可能会增加高度的,如何判断树B是否增加了高度呢?树A要么是树B根节点的左子树,要么是树B根节点的右子树,当树A的高度增加后,树A作为树B根节点的左右子树中的一个,如果树A的高度大于了树B根节点的另一个子树的高度,那么树B的高度就增加,反之高度就不变。

知识点3:(结合上图思考)根据知识点2,我们知道了树A的高度增加后,以【树A根节点的父亲节点】为根的树B的高度可能会增加。但现在有一个问题,树A的高度增加后,除了以【树A根节点的父亲节点】为根的树B的高度可能增加外,以【树A根节点的其他祖先节点】为根的树C、树D...的高度可能会增加吗?答案:这取决于树A的高度增加后,树B的高度是否增加,如果树B的高度都没增加,根据知识点1可得,树C、树D等等一切以【树A根节点的其他祖先节点】为根节点的树的高度都不可能会增加;如果树B的高度增加了,根据知识点2可得,树C的高度是有可能增加的,但现在还无法确定树D的高度是否有可能会增加,因为树D的高度是否有可能会增加又取决于树C的高度是否增加,如果树C的高度确定会增加,根据知识点2可得,树D的高度有可能会增加,反之如果树C的高度确定不会增加,根据知识点1可得,树D的高度不可能会增加。所以从这可以得到一个结论,以任意一个节点A为根节点的树的高度增加后,只能确定以【节点A的父亲节点】为根节点的树的高度是有可能增加的,无法确定以【节点A的其他祖先节点(比如爷爷节点,曾爷爷节点)】为根节点的树的高度是否有可能增加,即当前层节点的高度增加后,只能确定上一层的高度是否有可能增加,无法确定上上层及以上的节点的高度是否有可能增加,上上层的情况得依赖上一层的情况。

问题1:为什么在上面的第二种处理方案中说:需要通过指针p向上更新一次curParent的父亲节点的平衡因子呢?

答案1:现在cur插入后,curParent的平衡因子被更新为正负1,这说明现在以curParent为根节点的树的高度增加了,根据上面的知识点2,只要以curParent为根节点的树的高度增加了,那以【curParent的父亲节点】为根节点的树的高度是有可能会增加的(注意只是有可能,而不是一定,为什么呢?理论原因见上面3个知识点,具体示例如下图。),而只要以【curParent的父亲节点】为根节点的树的高度增加,那这棵树就有高度不平衡的可能(原因在下面一段),所以综上所述,以【curParent的父亲节点】为根节点的树是有高度不平衡的可能的,正是因为以【curParent的父亲节点】为根节点的树有高度不平衡的可能,才需要通过指针p向上更新一次curParent的父亲节点的平衡因子,来确定以【curParent的父亲节点】为根节点的树到底是否平衡。

 问题2:为什么上面答案1说只要以【curParent的父亲节点】为根节点的树的高度增加,那这棵树就有高度不平衡的可能呢?

答案2:现在我们只插入一个cur节点,因为新插入节点只有一个,所以在任意一棵树T中插入cur节点时,要么插在树T根节点的左子树中,要么插在树T根节点的右子树中,总之最多是只能给树T的根节点的左右子树中的一个子树增加高度的,在这种只能给树T的根节点的左右子树中的一个子树增加高度的情况下如果树T的高度还增加了,说明要么是在插入cur前,树T的根节点的左右子树的高度就相等,随便在一个子树插入cur后导致T的高度增加,此时T根节点的左右子树高度差为1;要么是在插入cur前,树T的根节点的左右子树的高度就不相等,在更高的子树中插入cur后导致T的高度增加,此时T根节点的左右子树高度差为2。综上所述,插入cur后,此时树T的根节点的左子树和右子树的高度一定是不相等的,如果高度差,即树T的根节点的平衡因子的值(即根节点的右子树高度-左子树高度的值)的绝对值大于1,那么树T的高度就不平衡,如果绝对值等于1,那么树T的高度就平衡,绝对值不可能为0(因为树T的左右子树的高度一定不相等)。综上所述,在只新插入一个节点的情况下,只要任意一棵树T的高度增加了,树T就有可能不平衡,就包括以【curParent的父亲节点】为根节点的这棵树。(注意有一种特殊情况,树T是棵空树时,插入节点cur后,cur就是根节点,此时虽然T的高度也增加了,但一定是平衡的)

在讲解第二种处理方案时我们说过:更新完curParent的父亲节点的平衡因子后,平衡因子的值会有3种可能,对这3种可能又有各自的处理动作。之前没有对这3种可能进行讲解,现在咱们来说一说它。

3种可能的示例图如上图所示,讲解如下文所示。

(结合上图思考)3种可能中的其一是平衡因子为正负1:如果指针p从curParent位置向上走一次,更新完curParent的父亲节点的平衡因子后,平衡因子的值为正负1,说明在更新前,curParent的父亲节点的平衡因子的值为0。凭什么一定是0呢?因为正负1只有通过0+1或者0-1得到(不可能通过正负2得到正负1,因为如果在更新curParent的父亲节点的平衡因子前,平衡因子就已经为正负2了,说明在插入cur前整棵树就不平衡了,我们不会允许这种情况发生),比如当curParent是curParent的父亲节点的左孩子时,则curParent的父亲节点的平衡因子经过减1后,从0更新成了-1;当curParent是curParent的父亲节点的右孩子时,则curParent的父亲节点的平衡因子经过加1后,从0更新成了1。而不管curParent的父亲节点的平衡因子是从0变成-1,还是从0变成了1,都说明在curParent的父亲节点为根节点的树A中,开始树A根节点的左右子树的高度是相等的,后来要么是树A根节点的左子树高度增加了,要么是树A根节点的右子树高度增加了。这就可以证明以curParent的父亲节点为根节点的树A的高度是增加了的(从这也能看出来,只要一个节点的平衡因子被更新成正负1,那以该节点为根节点的树的高度一定增加了),既然以curParent的父亲节点为根节点的树A的高度增加了,那根据上文的知识点2可得,以【树A根节点的父亲节点】为根节点的树的高度是有可能会增加的,上文中说过了,只要一棵树的高度可能会增加,那这棵树也有可能会不平衡,所以指针p又得向上走,从指向curParent的父亲节点变成指向curParent的父亲节点的父亲节点,看看以【curParent的父亲节点的父亲节点】为根节点的树是否平衡。所以综上所述,可得到一个结论,p从指向curParent节点开始(包括curParent节点),只要p指向的节点的平衡因子为正负1,说明以p指向的节点为根节点的树A的高度肯定是增加了的,以p指向节点的父亲节点为根节点的树B的高度就可能会不平衡,此时p就需要往上走一次(注意只是一次),然后更新平衡因子看看是否树B平衡,如果更新平衡因子后,值又是正负1,此时p就又需要往上走一次,也就是说,只要p更新完一个节点的平衡因子后,平衡因子为正负1,p就得向上走一次。注意p向上更新平衡因子时,最多走到根节点处。

这里额外提一下,如果p向上更新平衡因子时,从curParent开始真走到了整棵树的根节点,无法再继续向上走了,说明所指向的这么多个节点的平衡因子被更新后,这些平衡因子的绝对值也没有一个是大于1的、也没有一个是等于0的,而是全部等于1,这就说明整棵树的高度在插入cur节点后依然是平衡的,无需进行调整,插入cur节点成功。为什么整棵树的高度依然是平衡的,无需调整呢?指针p从curParent位置开始不断向上走,直到走到整棵树的根节点,无法再继续向上走时,所指向的这么多个节点的平衡因子被更新后,这些平衡因子的绝对值也没有一个是大于1的、也没有一个是等于0的,而是全部等于1,这意味着插入cur节点后,以【p指向的curParent的每一个祖先节点】为根节点的树的高度都增加了,上文中说过,只要一棵树的高度增加,就有不平衡的可能,所以以curParent的祖先节点作为根节点的树都有可能不平衡,而指针p在向上走的过程中,依次指向的就是curParent节点的这些祖先节点,【p指向的节点的平衡因子的绝对值没有一个是大于1的、也没有一个是等于0的,而是全部等于1】这句话等价于【现在每一个以curParent的祖先节点作为根节点的树中,根节点的右子树和左子树的差的绝对值都不超过1,也不等于0】,这等价于每一个以curParent的祖先节点作为根节点的树都平衡,既然每一棵可能会不平衡的子树现在都能确定它是平衡的了,那整棵树也就平衡的了。

(结合上图思考)3种可能中的其二是平衡因子为0:如果指针p从curParent位置向上走一次更新curParent的父亲节点的平衡因子时,平衡因子被更新成了0,此时也说明整棵树的高度在插入cur节点后依然是平衡的,p指针无需继续向上走,整棵树无需进行调整,插入cur节点成功。为什么整棵树无需进行调整呢?在cur插入前,整棵树就是平衡的,所以整棵树的任意子树也都是平衡的。cur节点插入后,指针p从curParent位置向上更新一次curParent的父亲节点的平衡因子时,平衡因子被更新成了0,说明更新前,平衡因子的值一定为正负1。凭什么一定是正负1呢?0只有通过1减1或者-1加1得到,比如curParent是curParent父亲节点的左孩子,根据上文中的指针p更新平衡因子的更新规则,那curParent的父亲节点的平衡因子经过减1后,从1更新成了0;当curParent是curParent的父亲节点的右孩子时,则curParent的父亲节点的平衡因子经过加1后,从-1更新成了0。而不管curParent的父亲节点的平衡因子是从1变成0,还是从-1变成了0,都说明在curParent的父亲节点为根节点的树A中,开始树A根节点的左右子树的高度一定是不相等的,要么是树A根节点的左子树高度比较低,要么是树A根节点的右子树高度比较低,插入cur节点后,让较低的子树的高度增加了,从而让树A根节点的左右子树的高度相等了,所以curParent的父亲节点的平衡因子才会被更新成0(从这也能看出来,只要一个节点的平衡因子被更新成0,那以该节点为根节点的树的高度一定不变),这说明以该平衡因子为0的节点作为根节点的这棵树A的高度不变,在插入cur节点前,树A是平衡的,在插入cur后,树A的高度不变,当然也是平衡的,而以平衡因子为0的节点作为根节点的树A又是所有【以平衡因子为0的节点的不同祖先节点为根节点的树】的子树,子树的高度不变,那这若干个父树的高度当然也不变(父树即是以平衡因子为0的节点的不同祖先节点作为根节点的树),既然这些父树在插入cur前就已经是平衡的了,插入cur后这些父树的高度又没变,当然所有父树都还是高度平衡的,综上所述,在插入cur节点后,树A是平衡的,树A的所有父树也是平衡的,这就相当于整棵树都是平衡的,所以才说整棵树无需进行调整。从这里可得到一个结论:当插入cur后,指针p从curParent开始(包括curParent),只要有指针p指向的节点的平衡因子被调整成0,p指针就无需继续向上走更新p指向节点的祖先节点的平衡因子,整棵树无需进行调整(因为已经平衡了),插入cur节点成功。

(结合上图思考)3种可能中的其三是是平衡因子为正负2:如果指针p从curParent位置向上走一次更新curParent的父亲节点的平衡因子时,平衡因子被更新成了正负2,这说明更新前,平衡因子的值一定为正负1,即左右子树高度差为1,现在新节点cur插入到了【curParent的父亲节点】的左右子树中较高的一边,此时高度差就为2了,这说明【以目前指针p指向的节点为根的子树】的高度已经不平衡了,既然这棵子树不平衡了,那就破坏了整棵树的平衡,整棵树也就不平衡了,此时就需要进行旋转调整,控制平衡,当以指针p指向的节点(即平衡因子为正负2的节点)为根节点的子树被旋转调整平衡后,那么这棵子树平衡了,整棵树也就平衡了。这也表明在一次插入节点的过程中,最多只会旋转一次,对以【指针p指向的平衡因子为2的节点】为根节点的不平衡的子树进行旋转调整后,这棵子树平衡后,就不会再发生旋转了,插入就结束了,p指针无需继续向上走,整棵树无需进行调整,插入cur节点成功。为什么呢?答案:对以【指针p指向的平衡因子为正负2的节点】为根节点的不平衡的子树被旋转调整完毕后,该子树就平衡了,并且该子树的根节点的平衡因子会被更新成0,正是因为平衡因子被更新成了0,所以根据上一段的结论可得,p指针无需继续向上走更新其他祖先节点的平衡因子,整棵树无需进行调整,插入cur节点成功。凭什么平衡因子被更新成了0,p指针就无需继续向上走更新其他祖先节点的平衡因子、整棵树就无需进行调整、插入cur节点就成功了呢?原因在3种可能中的其二部分已经全部讲解过了,详情原因请见上一段,这里就简单回顾下:以【指针p指向的平衡因子为2的节点】为根节点的不平衡的子树经过旋转控制平衡后,该子树的根节点的平衡因子被更新成了0,这意味着和cur节点被插入该子树前相比,该子树插入cur节点后的高度没变,该子树的高度不变,那以【该子树的根节点的不同祖先节点】为根节点的若干父树的高度当然也不变,子树和所有父树的高度都没变,整棵树的高度也就没变,在插入cur前,整棵树是平衡的,插入cur后,现在高度不变,整棵树当然还是平衡的,整棵树都已经平衡了,所以就什么都不用做了,所以在一次插入节点的过程中,最多只会旋转一次,对以【指针p指向的平衡因子为2的节点】为根节点的不平衡的子树进行旋转调整后,这棵子树平衡后,就不需要再发生旋转了,插入就结束了,p指针无需继续向上走更新其他祖先节点的平衡因子,整棵树无需进行调整,插入cur节点成功。那该如何旋转呢?咱们把压力减轻一点,放在下文中说。

分割线————

借助上面这些理论,我们可以编写完AVL的插入的第二阶段,如下代码所示。注意下面代码其实可以写的更简洁,即可以不用定义p和q这两个指针,直接使用cur和curParent指针即可,但因为上面讲解理论部分时我们使用了指针p,说让p不断向上走,更新curParent的祖先节点的平衡因子,所以下面代码也使用了指针p,但因为光一个指针p完成不了【更新curParent的祖先节点的平衡因子】这个任务,所以又定义了一个指针q辅助p。

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
    AVLTree<T>()
		:_root(nullptr)
	{}

	bool Insert(const T&x)
	{
		if (_root == nullptr)
		{
			_root = new Node(x);
			return true;
		}
		else
		{
			Node* cur = _root;
			Node* curParent = nullptr;
			//先找插入位置,即空位置
			while (cur!=nullptr)
			{
				if (x > cur->_data)
				{
					curParent = cur;
					cur = cur->_right;
				}
				else if (x < cur->_data)
				{
					curParent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}
			//找到空位置,开始插入
			if (x > curParent->_data)
			{
				cur = new Node(x);
				curParent->_right = cur;
				cur->_parent = curParent;
			}
			else
			{
				cur = new Node(x);
				curParent->_left = cur;
				cur->_parent = curParent;
			}
			//插入完毕后,开始控制平衡,这是第二阶段的任务
			
			//先更新curParent的平衡因子
			if (cur == curParent->_left)
				curParent->_bf--;
			else
				curParent->_bf++;
			//然后判断curParent是哪种情况,要使用哪种解决方案
			//curParent的bf为0,属于第一种情况,使用第一种处理方案
			if (curParent->_bf == 0)
			{
				return true;
			}
			//curParent的bf为正负1,属于第二种情况,使用第二种处理方案
			else if(abs(curParent->_bf)==1)
			{
				//在当前情况下,需要让指针p不断向上走,更新curParent的祖先节点的平衡因子,为了p能完成【更新curParent的祖先节点的平衡因子】这个任务,设置了指针q辅助p,否则p就不知道要对指向节点的bf进行+1还是-1
				Node* q = cur;
				Node* p = curParent;
				while (p != nullptr)
				{
					//该if分支对应文中所说的3种可能中的其二
					if (p->_bf == 0)
					{
						return true;
					}
					//如果每次进入循环,都走这个分支,该if分支对应文中所说的3种可能中的其一
					else if (abs(p->_bf) == 1)
					{
						q = q->_parent;
						p = p->_parent;
						if (p != nullptr && q == p->_right)
							p->_bf++;
						else if (p != nullptr && q == p->_left)
							p->_bf--;
					}
					//该if分支对应文中所说的3种可能中的其三,整棵树已经不平衡了,需要进行旋转操作,控制平衡
					else if (abs(p->_bf) == 2)
					{
						//这是Insert的第三阶段的任务,未编写完毕
					}
				}
			}
			
			return true;
		}
	}

private:
	Node* _root;
};

在上面讲解第二种处理方案中的、3种可能中的其三部分时,咱们还差一步旋转操作没有讲解,接下来咱们说说该如何旋转,控制平衡。(人们都把控制平衡的方法称为旋转,但笔者认为方法本身的做法和旋转这个动作没有什么联系,有点牵强)

当指针p指向节点的平衡因子为正负2时,以这个节点为根的子树就不平衡了,从而破坏了整棵树的平衡,整棵树也就不平衡了,此时就需要对这棵子树进行旋转调整,控制平衡,当子树被旋转调整平衡后,就不再破坏整棵树的平衡,整棵树也就平衡了。p指向的节点的平衡因子为正负2时,会分4种情况,每种情况的旋转方案都不一样,如下。

第一种情况:新节点插入了较高左子树的左侧(简称左左),此时需要右单旋控制平衡。解释一下前面的这句话,如下图,以30为根的子树是60的左子树,60的左子树比60的右子树的高度更高,所以把以30为根的树称为较高左子树,新插入的节点又在30的左子树,所以称为:“新节点插入了较高左子树的左侧(简称左左)”。(注意下面a、b、c都是高度为h的高度平衡二叉搜索树,h>=0)

说一下,你只要知道接下来所说的旋转方法可以控制平衡即可,不要问方法的依据是什么,不要问这样的方法是通过什么得出来的,这是别人经过穷举、排除、大量实验后所发现的一种规律。

(这里说一下,有一种记忆右单旋的方法,比如对以节点p为根节点的树进行右单旋时,(结合上图区域2思考)就是把节点p往下拉,让节点p连带着p的右子树一起往下拉,把p往下拉到subL的右下方,让p成为subL的右孩子,然后让p接管subL的右子树subLR)

在插入新节点后,p指向的节点60的平衡因子变成了-2,此时以【p指向的节点60】为根的树不再平衡,需要右单旋调整平衡。右单旋的代码该怎么做呢?非常简单,只需把上图区域2的各个指针的指向改成区域3的各个指针的指向即可。这里只说说注意事项,其余的只需看上图就能把代码给写出来,不必多说。注意事项一:写代码时要分两种情况,第一,当p不是整棵树的根节点,p只是一个子树的根节点时,在这种情况下,我们把p的父亲节点叫p_parent,把p的左孩子节点叫subL,subL的右孩子节点叫subLR,这时我们要考虑子树被旋转调整平衡后,把子树和整棵树挂接起来,做法也很简单,让p_parent和subL互相连接即可。第二,当p就是整棵树的根节点,上图不是一棵子树,而是整棵树时,此时p没有父亲节点,在这种情况下,把p的左孩子节点叫subL,subL的右孩子节点叫subLR,整棵树被旋转调整平衡后,需要更新整棵树的根节点,即让_root指向subL,还要把subL节点的_parent成员设置成nullptr,因为整棵树的根节点是没有父亲节点的。注意事项二:Node节点的成员是三叉链,不要忘了给节点的_parent指针赋值。注意事项三:不管p指向的节点是子树的根节点还是整棵树的根节点,都要注意h可能为0,也就是说以subLR为根节点的树可能是棵空树,所以解引用subLR指针获取节点的_parent成员前,记得判断subLR指针是否空,避免解引用空指针报错。注意事项四:别忘了把subL和p指向节点的平衡因子都更新成0。

代码如下。说一下,因为根据上面的流程图我们知道了旋转后的子树的形状是怎样的,知道了旋转后的一些节点的平衡因子是怎样的,所以我们旋转的代码实际上就是面向答案编程,不需要什么逻辑。所以,编写旋转的代码时,重点就在于你心里要知道,这棵不平衡的树在旋转前的形状是什么样的,这棵不平衡的树在旋转后的形状是什么样的,只有这样才能通过代码让这棵树从旋转前的样子变成旋转后的样子。

#pragma once
#include<iostream>
using namespace std;

template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
private:
    void RotateR(Node* p)
	{
		if (p != _root)
		{
			Node* subL = p->_left;
			Node* subLR = subL->_right;
			Node* p_parent = p->_parent;

			subL->_right = p;
			subL->_parent = p_parent;

			if (p == p_parent->_right)
				p_parent->_right = subL;
			else
				p_parent->_left = subL;

			p->_left = subLR;
			p->_parent = subL;
			
			if (subLR != nullptr)
				subLR->_parent = p;

			p->_bf = 0;
			subL->_bf = 0;
		}
		else
		{
			Node* subL = p->_left;
			Node* subLR = subL->_right;

			_root = subL;
			subL->_right = p;
			subL->_parent = nullptr;

			p->_left = subLR;
			p->_parent = subL;

			if (subLR != nullptr)
				subLR->_parent = p;

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

private:
	Node* _root;
};

上面RotateR函数的代码执行完一次后,即右单旋转一次后,这棵不平衡的子树也就被调整平衡了,从而整棵树也就平衡了,指针p无需继续往上走更新其他祖先节点的平衡因子,插入节点成功,Insert插入函数结束。(指针p无需继续向上走更新其他祖先节点的平衡因子的原因已经在【3种可能中的其三】部分讲过了)

分割线————

第二种情况,新节点插入了较高右子树的右侧(简称右右),此时需要左单旋控制平衡。解释一下前面的这句话,如下图,以60为根的子树是30的右子树,30的右子树比30的左子树的高度更高,所以把以60为根的树称为较高右子树,新插入的节点又在60的右子树,所以称为:“新节点插入了较高右子树的右侧(简称右右)”。(注意下面a、b、c都是高度为h的高度平衡二叉搜索树,h>=0)

说一下,你只要知道接下来所说的旋转方法可以控制平衡即可,不要问方法的依据是什么,不要问这样的方法是通过什么得出来的,这是别人经过穷举、排除、大量实验后所发现的一种规律。

(这里说一下,有一种记忆左单旋的方法,比如对以节点p为根节点的树进行左单旋时,(结合上图区域2思考)就是把节点p往下拉,让节点p连带着p的左子树一起往下拉,把p往下拉到subR的左下方,让p成为subR的左孩子,然后让p接管subR的左子树subRL)

在插入新节点后,p指向的节点30的平衡因子变成了2,此时以【p指向的节点30】为根的树不再平衡,需要左单旋调整平衡。左单旋的代码该怎么做呢?非常简单,只需把上图区域2的各个指针的指向改成区域3的各个指针的指向即可。这里只说说注意事项,其余的只需看上图就能把代码给写出来,不必多说。注意事项一:写代码时要分两种情况,第一,当p不是整棵树的根节点,p只是一个子树的根节点时,在这种情况下,我们把p的父亲节点叫p_parent,把p的右孩子节点叫subR,subR的左孩子节点叫subRL,这时我们要考虑子树被旋转调整平衡后,把子树和整棵树挂接起来,做法也很简单,让p_parent和subR互相连接即可。第二,当p就是整棵树的根节点,上图不是一棵子树,而是整棵树时,此时p没有父亲节点,在这种情况下,我们把p的右孩子节点叫subR,subR的左孩子节点叫subRL,整棵树被旋转调整平衡后,需要更新整棵树的根节点,即让_root指向subR,还要把subR节点的_parent成员设置成nullptr,因为整棵树的根节点是没有父亲节点的。注意事项二:Node节点的成员是三叉链,不要忘了给节点的_parent指针赋值。注意事项三:不管p指向的节点是子树的根节点还是整棵树的根节点,都要注意h可能为0,也就是说以subRL为根节点的树可能是棵空树,所以解引用subRL指针获取节点的_parent成员前,记得判断subRL指针是否空,避免解引用空指针报错。注意事项四:别忘了把subR和p指向节点的平衡因子都更新成0。

代码如下。说一下,因为根据上面的流程图我们知道了旋转后的子树的形状是怎样的,知道了旋转后的一些节点的平衡因子是怎样的,所以我们旋转的代码实际上就是面向答案编程,不需要什么逻辑。所以,编写旋转的代码时,重点就在于你心里要知道,这棵不平衡的树在旋转前的形状是什么样的,这棵不平衡的树在旋转后的形状是什么样的,只有这样才能通过代码让这棵树从旋转前的样子变成旋转后的样子。

#pragma once
#include<iostream>
using namespace std;

template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
private:
    void RotateL(Node* p)
	{
		if (_root != p)
		{
			Node* subR = p->_right;
			Node* subRL = subR->_left;
			Node* p_parent = p->_parent;

			subR->_left = p;
			subR->_parent = p_parent;

			p->_parent = subR;
			p->_right = subRL;

			if (p == p_parent->_right)
				p_parent->_right = subR;
			else
				p_parent->_left = subR;

			if(subRL!=nullptr)
				subRL->_parent = p;

			p->_bf = 0;
			subR->_bf = 0;
		}
		else
		{
			Node* subR = p->_right;
			Node* subRL = subR->_left;

			_root = subR;
			subR->_left = p;
			subR->_parent = nullptr;

			p->_right = subRL;
			p->_parent = subR;

			if (subRL != nullptr)
				subRL->_parent = p;

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

private:
	Node* _root;
};

上面RotateL函数的代码执行完一次后,即左单旋转一次后,这棵不平衡的子树也就被调整平衡了,从而整棵树也就平衡了,指针p无需继续往上走更新其他祖先节点的平衡因子,插入节点成功,Insert插入函数结束。(指针p无需继续向上走更新其他祖先节点的平衡因子的原因已经在【3种可能中的其三】部分讲过了)

————分割线

第三种情况,新节点插入了较高左子树的右侧(简称左右),此时需要左右双旋控制平衡,即先左单旋再右单旋。解释一下前面的这句话,如下图,以30为根的子树是60的左子树,60的左子树比60的右子树的高度更高,所以把以30为根的树称为较高左子树,新插入的节点又在30的右子树,所以称为:“新节点插入了较高左子树的右侧简称左右)”。(注意下面a、b、c都是高度为h的高度平衡二叉搜索树,h>=0)

说一下,你只要知道接下来所说的旋转方法可以控制平衡即可,不要问方法的依据是什么,不要问这样的方法是通过什么得出来的,这是别人经过穷举、排除、大量实验后所发现的一种规律。

左右双旋会分3种情况。从上图可以看到,在新节点插入较高左子树的右侧时,单靠对子树右单旋已经无法控制平衡,旋转前子树根节点的平衡因子为-2,子树不平衡,旋转后子树根节点的平衡因子为2,依然不平衡。这时就需要左右双旋了,即先对以subL为根节点的树左单旋,再对以p为根节点的树右单旋,为了方便演示,咱们可以把【上图的区域1和区域2的形状】画成【如下图1的区域1和区域2的形状】,前后两者是等价的,这种情况是h不为0,新插入节点在subLR的左子树,此时左右双旋的流程可以如下图1所示,这是第一种情况。注意在下图1的区域2中,不管新插入节点在subLR的左子树还是右子树,都需要左右双旋调整平衡,他俩的区别只不过是在最终左右双旋旋转完毕后更新平衡因子时给的值不同,所以也可以把【上图的区域1和区域2的形状】画成【如下图二的区域1和区域2的形状】,这前后两者也是等价的,这种情况是h不为0,新插入节点在subLR的右子树,此时左右双旋的流程可以如下图2所示,这是第二种情况。还有一种特殊情况,如下图3,当h的高度为0时,此时subLR节点压根就不存在,所以此时新插入节点既不可能在subLR的左子树,也不可能在subLR的右子树,subLR节点只能是新插入节点本身,这种情况对比【情况一和情况二】,区别也只不过是在最终左右双旋旋转完毕后更新平衡因子时,给的平衡因子的值不同。所以综上所述,左右双旋共分3种情况:第一种,h不为0,新插入节点在subLR的左子树;第2种,h不为0,新插入节点在subLR的右子树;第3种,h=0,新插入节点就是subLR。左右双旋的这3种情况之间只有一个区别,那就是左右双旋完毕后更新平衡因子时给的值不同。3种情况下的左右双旋的流程图如下3张图。这些流程图是怎么来的呢?因为我们心里提前知道了左右双旋就是先对以subL为根节点的树左单旋,再对以p为根节点的树右单旋,所以结合上文中左单旋和右单旋的做法,就可以画出以下流程图。

流程图1如下。这是第一种情况,h不为0,新插入节点在subLR的左子树;

流程图2如下。这是第2种情况,h不为0,新插入节点在subLR的右子树;

流程图3如下。这是第3种情况,h=0,新插入节点就是subLR。

我们知道【左右双旋就是先对以subL为根节点的树左单旋,再对以p为根节点的树右单旋】后,再结合上面的流程图,左右双旋的代码也就非常好写了。

左右双旋的代码非常简单。因为前面说了,【左右双旋的这3种情况之间只有一个区别,那就是左右双旋完毕后更新平衡因子时给的值不同】,并且前面还说了,【左右双旋就是先对以subL为根节点的树左单旋,再对以p为根节点的树右单旋】,所以不管是哪种情况,都统一先对以subL为根节点的树左单旋,这只需要调用RotateL(subL)函数即可做到(当然你也可以不复用函数,流程图被画出来后,你可以按照它手动把区域2的各个指针的指向改成区域3的各个指针的指向,其实本质上RotateL(subL)的功能也就是把区域2的各个指针的指向改成区域3的各个指针的指向);再都统一对以p为根节点的树右单旋,这只需要调用RotateR(p)即可做到(当然你也可以不复用函数,流程图被画出来后,你可以按照它手动把区域3的各个指针的指向改成区域4的各个指针的指向,其实本质上RotateR(p)的功能也就是把区域3的各个指针的指向改成区域4的各个指针的指向)。这些步骤在3种情况下都是一模一样的,最后将subL、subLR、p的平衡因子更新即可完成左右双旋。如何更新平衡因子呢?直接面向答案编程,观察上面3张图的区域4,如果是第1种情况,则让subL->_bf=0、subLR->_bf=0、p->_bf=1;如果是第2种情况,则让subL->_bf=-1、subLR->_bf=0、p->_bf=0;如果是第3种情况,则让subL->_bf=0、subLR->_bf=0、p->_bf=0。那更新平衡因子时如何判断是哪种情况呢?通过【在插入新节点后,左单旋和右单旋前】这个时间点的subLR节点的平衡因子判断,即通过上面3张图中的区域2的subLR节点的平衡因子判断,如果是-1,则说明新节点插入到了subLR的左子树,这就是第1种情况;如果是1,则说明新节点插入到了subLR的右子树,这就是第2种情况;如果subLR的平衡因子为0,则说明subLR就是新插入节点本身,这是第3种情况。

走到这里可以发现:因为一开始就给出了答案:【左右双旋就是先对以subL为根节点的树左单旋,再对以p为根节点的树右单旋】。根据这个答案,我们就能把左右双旋的所有情况的流程图给画出来,而根据上面的流程图,我们就又知道了旋转后的子树的形状是怎样的,知道了旋转后的一些节点的平衡因子是怎样的,所以我们旋转的代码实际上就是面向答案编程,不需要什么逻辑。所以,编写旋转的代码时,重点就在于你心里要知道,这棵不平衡的树在旋转前的形状是什么样的,这棵不平衡的树在旋转后的形状是什么样的,只有这样才能通过代码让这棵树从旋转前的样子变成旋转后的样子。但目前编写双旋的代码时,因为我们心里知道双旋可以通过复用两个单旋实现,不用从0开始造轮子,所以心里可以不用知道这棵不平衡的树在旋转前的形状是什么样的,不用知道这棵不平衡的树在旋转后的形状是什么样的,直接复用单旋函数即可;但如果编写双旋时选择不复用两个单旋函数,选择从0开始造轮子,那心里就要知道这棵不平衡的树在每次单旋前的形状是什么样的,这棵不平衡的树在每次单旋后的形状是什么样的,只有这样才能通过代码让这棵树从双旋前的样子变成双旋后的样子。

左右双旋代码如下。

#pragma once
#include<iostream>
using namespace std;

template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
private:
    //LR表示左右双旋
	void RotateLR(Node* p)
	{
		Node* subL = p->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;//在【插入新节点后,左右双旋前】的这个时间段,subLR节点的平衡因子是作为【左右双旋旋转完毕后更新subL、subLR、p节点的平衡因子】的依据的,但因为下面调用RotateR或者RotateL函数时,内部会有修改subLR节点的平衡因子的逻辑,所以提前记录值,避免值丢失了

		RotateL(subL);
		RotateR(p);
		if (bf == -1)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			p->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			p->_bf = 0;
		}
		else if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			p->_bf = 0;
		}
		
	}

private:
	Node* _root;
};

问题:写左右双旋的代码时按理说也有一些注意事项,比如注意事项一:写代码时也要分两种情况,第一,当p不是整棵树的根节点,p只是一个子树的根节点时,在这种情况下,我们把p的父亲节点叫p_parent,这时我们要考虑子树被旋转调整平衡后,把子树和整棵树挂接起来,让p_parent和subLR互相连接。第二,当p就是整棵树的根节点,上面3张图中的树不是一棵子树,而是整棵树时,此时p没有父亲节点,在这种情况下,整棵树被左右双旋调整平衡后,需要更新整棵树的根节点,即让_root指向subLR,还要把subLR节点的_parent成员设置成nullptr,因为整棵树的根节点是没有父亲节点的。又比如注意事项二:Node节点的成员是三叉链,不要忘了给节点的_parent指针赋值。但为什么上面代码没有体现出这些注意事项呢?

答案:没错,写左右双旋的代码时的确有这些注意事项,但因为我们左右双旋的代码是复用的左单旋函数RotateL和右单旋函数RotateR,在RotateL和RotateR函数中对这些注意事项已经处理过了,所以这些注意事项不是没有在左右双旋的代码中体现,只是体现的方式不易发现,它是通过RotateL和RotateR函数间接体现的。

上面RotateLR函数的代码执行完一次后,即左右双旋一次后,这棵不平衡的子树也就被调整平衡了,从而整棵树也就平衡了,指针p无需继续往上走更新其他祖先节点的平衡因子,插入节点成功,Insert插入函数结束。(指针p无需继续向上走更新其他祖先节点的平衡因子的原因已经在【3种可能中的其三】部分讲过了)

这里额外说一下,因为一开始就给出了答案:【左右双旋就是先对以subL为根节点的树左单旋,再对以p为根节点的树右单旋】。根据这个答案,我们就能把左右双旋的所有情况的流程图给画出来,而根据上面的流程图,我们就又知道了左右双旋旋转后的子树的形状是怎样的,知道了右左双旋旋转后的一些节点的平衡因子是怎样的,所以我们可以直接面向答案进行右左双旋,即从0开始造轮子,把上面几种情况中的区域2的各个指针的指向直接改成区域4的各个指针的指向,然后把平衡因子更新,这样即可完成各自情况下的右左双旋。但我们上面没有这么做,而是先调用RotateR(subR)把区域2的各个指针的指向改成区域3的各个指针的指向,然后再调用subL(p)把区域3的各个指针的指向改成区域4的各个指针的指向,然后把平衡因子更新,通过这样完成各自情况下的右左双旋。为什么要绕一层呢?这么做是为了让右左双旋的代码复用右单旋和左单旋的代码。

分割线————

第四种情况,新节点插入了较高右子树的左侧(简称右左),此时需要右左双旋控制平衡,即先右单旋再左单旋。解释一下前面的这句话,如下图,以60为根的子树是30的右子树,30的右子树比30的左子树的高度更高,所以把以60为根的树称为较高右子树,新插入的节点又在60的左子树,所以称为:“新节点插入了较高右子树的左侧(简称右左)”。(注意下面a、b、c都是高度为h的高度平衡二叉搜索树,h>=0)

说一下,你只要知道接下来所说的旋转方法可以控制平衡即可,不要问方法的依据是什么,不要问这样的方法是通过什么得出来的,这是别人经过穷举、排除、大量实验后所发现的一种规律。

右左双旋会分3种情况。从上图可以看到,在新节点插入较高右子树的左侧时,单靠对以p为根节点的子树左单旋已经无法控制平衡,旋转前子树根节点的平衡因子为2,子树不平衡,旋转后子树根节点的平衡因子为-2,依然不平衡。这时就需要右左双旋了,即先对以subR为根节点的树右单旋,再对以p为根节点的树左单旋,为了方便演示,咱们可以把【上图的区域1和区域2的形状】画成【如下图1的区域1和区域2的形状】,前后两者是等价的,这种情况是h不为0,新插入节点在subRL的左子树,此时右左双旋的流程可以如下图1所示,这是第一种情况。注意在下图1的区域2中,不管新插入节点在subRL的左子树还是右子树,都需要右左双旋调整平衡,他俩的区别只不过是在最终右左双旋旋转完毕后更新平衡因子时给的值不同,所以也可以把【上图的区域1和区域2的形状】画成【如下图二的区域1和区域2的形状】,这前后两者也是等价的,这种情况是h不为0,新插入节点在subRL的右子树,此时右左双旋的流程可以如下图2所示,这是第二种情况。还有一种特殊情况,如下图3,当h的高度为0时,此时subRL节点压根就不存在,所以此时新插入节点既不可能在subRL的左子树,也不可能在subRL的右子树,subRL节点只能是新插入节点本身,这种情况对比【情况一和情况二】,区别也只不过是在最终右左双旋旋转完毕后更新平衡因子时,给的平衡因子的值不同。所以综上所述,右左双旋共分3种情况:第一种,h不为0,新插入节点在subRL的左子树;第2种,h不为0,新插入节点在subRL的右子树;第3种,h=0,新插入节点就是subRL节点本身。右左双旋的这3种情况之间只有一个区别,那就是右左双旋完毕后更新平衡因子时给的值不同。3种情况下的右左双旋的流程图如下3张图。这些流程图是怎么来的呢?因为我们心里提前知道了右左双旋就是先对以subR为根节点的树右单旋,再对以p为根节点的树左单旋,所以结合上文中左单旋和右单旋的做法,就可以画出以下流程图。

流程图1如下。第一种情况,h不为0,新插入节点在subRL的左子树。

流程图2如下。第二种情况,h不为0,新插入节点在subRL的右子树。

流程图3如下。第三种情况,h=0,新插入节点就是subRL节点本身。

我们知道【右左双旋就是先对以subR为根节点的树右单旋,再对以p为根节点的树左单旋】后,再结合上面的流程图,右左双旋的代码也就非常好写了。

右左双旋的代码非常简单。因为前面说了,【右左双旋的这3种情况之间只有一个区别,那就是右左双旋完毕后更新平衡因子时给的值不同】,并且前面还说了,【右左双旋就是先对以subR为根节点的树右单旋,再对以p为根节点的树左单旋】,所以不管是哪种情况,都统一先对以subR为根节点的树右单旋,这只需要调用RotateR(subR)函数即可做到(当然你也可以不复用函数,流程图被画出来后,你可以按照它手动把区域2的各个指针的指向改成区域3的各个指针的指向,其实本质上RotateR(subR)的功能也就是把区域2的各个指针的指向改成区域3的各个指针的指向);再都统一对以p为根节点的树左单旋,这只需要调用RotateL(p)即可做到(当然你也可以不复用函数,流程图被画出来后,你可以按照它手动把区域3的各个指针的指向改成区域4的各个指针的指向,其实本质上RotateL(p)的功能也就是把区域3的各个指针的指向改成区域4的各个指针的指向)。这些步骤在3种情况下都是一模一样的,最后将subR、subRL、p的平衡因子更新即可完成右左双旋。如何更新平衡因子呢?直接面向答案编程,观察上面3张图的区域4,如果是第1种情况,则让subR->_bf=1、subRL->_bf=0、p->_bf=0;如果是第2种情况,则让subR->_bf=0、subRL->_bf=0、p->_bf=-1;如果是第3种情况,则让subR->_bf=0、subRL->_bf=0、p->_bf=0;那更新平衡因子时如何判断是哪种情况呢?通过【在插入新节点后,右单旋和左单旋前】这个时间点的subRL节点的平衡因子判断,即通过上面3张图中的区域2的subRL节点的平衡因子判断,如果是-1,则说明新节点插入到了subRL的左子树,这就是第1种情况;如果是1,则说明新节点插入到了subRL的右子树,这就是第2种情况;如果subRL的平衡因子为0,则说明subRL就是新插入节点本身,这是第3种情况。

走到这里可以发现:因为一开始就给出了答案:【右左双旋就是先对以subR为根节点的树右单旋,再对以p为根节点的树左单旋】。根据这个答案,我们就能把右左双旋的所有情况的流程图给画出来,而根据上面的流程图,我们就又知道了旋转后的子树的形状是怎样的,知道了旋转后的一些节点的平衡因子是怎样的,所以我们旋转的代码实际上就是面向答案编程,不需要什么逻辑。所以,编写旋转的代码时,重点就在于你心里要知道,这棵不平衡的树在旋转前的形状是什么样的,这棵不平衡的树在旋转后的形状是什么样的,只有这样才能通过代码让这棵树从旋转前的样子变成旋转后的样子。但目前编写双旋的代码时,因为我们心里知道双旋可以通过复用两个单旋实现,不用从0开始造轮子,所以心里可以不用知道这棵不平衡的树在旋转前的形状是什么样的,不用知道这棵不平衡的树在旋转后的形状是什么样的,直接复用单旋函数即可;但如果编写双旋时选择不复用两个单旋函数,选择从0开始造轮子,那心里就要知道这棵不平衡的树在每次单旋前的形状是什么样的,这棵不平衡的树在每次单旋后的形状是什么样的,只有这样才能通过代码让这棵树从双旋前的样子变成双旋后的样子。

左右双旋代码如下。

#pragma once
#include<iostream>
using namespace std;

template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
private:
    //RL表示右左双旋
	void RotateRL(Node* p)
	{
		Node* subR = p->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;//在【插入新节点后,左右双旋前】的这个时间段,subRL节点的平衡因子是作为【右左双旋旋转完毕后更新subR、subRL、p节点的平衡因子】的依据的,但因为下面调用RotateR或者RotateL函数时,内部会有修改subLR节点的平衡因子的逻辑,所以提前记录值,避免值丢失了

		//右单旋的第一种写法,模拟RotateR的流程,因为subR不是整棵树的根节点,所以我们清楚右单旋的树不是整棵树,所以这里不用分情况
		Node* p1 = subR;
		Node* p1_parent = p1->_parent;
		Node* subL1 = p1->_left;
		Node* subL1R = subL1->_right;

		subL1->_right = p1;
		subL1->_parent = p1_parent;

		p1->_left = subL1R;
		p1->_parent = subL1;

		if (p1 == p1_parent->_right)
			p1_parent->_right = subL1;
		else
			p1_parent->_left = subL1;

		if (subL1R != nullptr)
			subL1R->_parent = p1;
		//在模拟RotateR时,我们就不更新平衡因子了,因为没有意义,即使更新了,这里的平衡因子也只是一个中间态,最后在右左双旋完毕后还会再更新平衡因子的
		//模拟RotateR结束

		//右单旋的第二种写法,不模拟RotateR的流程,直接复用RotateR的代码
		//RotateR(subR);


		//RotateL就不模拟了,直接复用RotateL函数的代码
		RotateL(p);

		if (bf == -1)
		{
			subR->_bf = 1;
			subRL->_bf = 0;
			p->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			p->_bf = -1;
		}
		else if (bf == 0)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			p->_bf = 0;
		}
	}

private:
	Node* _root;
};

问题:写右左双旋的代码时按理说也有一些注意事项,比如注意事项一:写代码时也要分两种情况,第一,当p不是整棵树的根节点,p只是一个子树的根节点时,在这种情况下,我们把p的父亲节点叫p_parent,这时我们要考虑子树被旋转调整平衡后,把子树和整棵树挂接起来,让p_parent和subRL互相连接。第二,当p就是整棵树的根节点,上面3张图中的树不是一棵子树,而是整棵树时,此时p没有父亲节点,在这种情况下,整棵树被右左双旋调整平衡后,需要更新整棵树的根节点,即让_root指向subRL,还要把subRL节点的_parent成员设置成nullptr,因为整棵树的根节点是没有父亲节点的。又比如注意事项二:Node节点的成员是三叉链,不要忘了给节点的_parent指针赋值。但为什么上面代码没有体现出这些注意事项呢?

答案:没错,写右左双旋的代码时的确有这些注意事项,但因为我们右左双旋的代码是复用的右单旋函数RotateR和左单旋函数RotateL,在RotateR和RotateL函数中对这些注意事项已经处理过了,所以这些注意事项不是没有在右左双旋的代码中体现,只是体现的方式不易发现,它是通过RotateR和RotateL函数间接体现的。

上面RotateRL函数的代码执行完一次后,即右左双旋一次后,这棵不平衡的子树也就被调整平衡了,从而整棵树也就平衡了,指针p无需继续往上走更新其他祖先节点的平衡因子,插入节点成功,Insert插入函数结束。(指针p无需继续向上走更新其他祖先节点的平衡因子的原因已经在【3种可能中的其三】部分讲过了) 

这里额外说一下,因为一开始就给出了答案:【右左双旋就是先对以subR为根节点的树右单旋,再对以p为根节点的树左单旋】。根据这个答案,我们就能把右左双旋的所有情况的流程图给画出来,而根据上面的流程图,我们就又知道了右左双旋旋转后的子树的形状是怎样的,知道了右左双旋旋转后的一些节点的平衡因子是怎样的,所以我们可以直接面向答案进行右左双旋,即从0开始造轮子,把上面几种情况中的区域2的各个指针的指向直接改成区域4的各个指针的指向,然后把平衡因子更新,这样即可完成各自情况下的右左双旋。但我们上面没有这么做,而是先调用RotateR(subR)把区域2的各个指针的指向改成区域3的各个指针的指向,然后再调用subL(p)把区域3的各个指针的指向改成区域4的各个指针的指向,然后把平衡因子更新,通过这样完成各自情况下的右左双旋。为什么要绕一层呢?这么做是为了让右左双旋的代码复用右单旋和左单旋的代码。

————所有旋转讲解完毕————

Insert的整体代码

走到这里,终于把旋转讲解完了,借助上面这些理论,我们终于可以编写AVL的插入的第三阶段(也是最终阶段)的代码了,最终版的 Insert整体代码如下所示。

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	AVLTree<T>()
		:_root(nullptr)
	{}

	bool Insert(const T&x)
	{
		if (_root == nullptr)
		{
			_root = new Node(x);
			return true;
		}
		else
		{
			Node* cur = _root;
			Node* curParent = nullptr;
			//先找插入位置,即空位置
			while (cur!=nullptr)
			{
				if (x > cur->_data)
				{
					curParent = cur;
					cur = cur->_right;
				}
				else if (x < cur->_data)
				{
					curParent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}
			//找到空位置,开始插入
			if (x > curParent->_data)
			{
				cur = new Node(x);
				curParent->_right = cur;
				cur->_parent = curParent;
			}
			else
			{
				cur = new Node(x);
				curParent->_left = cur;
				cur->_parent = curParent;
			}
			//插入完毕后,开始控制平衡,这是第二阶段的任务
			
			//先更新curParent的平衡因子
			if (cur == curParent->_left)
				curParent->_bf--;
			else
				curParent->_bf++;
			//然后判断curParent是哪种情况,要使用哪种解决方案
			//curParent的bf为0,属于第一种情况,使用第一种处理方案
			if (curParent->_bf == 0)
			{
				return true;
			}
			//curParent的bf为正负1,属于第二种情况,使用第二种处理方案
			else if(abs(curParent->_bf)==1)
			{
				//在当前情况下,需要让指针p不断向上走,更新curParent的祖先节点的平衡因子,为了p能完成【更新curParent的祖先节点的平衡因子】这个任务,设置了指针q辅助p,否则p就不知道要对指向节点的bf进行+1还是-1
				Node* q = cur;
				Node* p = curParent;
				while (p != nullptr)
				{
					//该if分支对应文中所说的3种可能中的其二
					if (p->_bf == 0)
					{
						return true;
					}
					//如果每次进入循环,都走这个分支,该if分支对应文中所说的3种可能中的其一
					else if (abs(p->_bf) == 1)
					{
						q = q->_parent;
						p = p->_parent;
						if (p != nullptr && q == p->_right)
							p->_bf++;
						else if (p != nullptr && q == p->_left)
							p->_bf--;
					}
					//该if分支对应文中所说的3种可能中的其三
					else if (abs(p->_bf) == 2)
					{
						//以p指向节点为根的子树不平衡,开始旋转调整,控制平衡,这是第三阶段(最终阶段)的任务

						//第一种情况:新节点插入了较高左子树的左侧(简称左左)
						if (p->_bf == -2 && q->_bf == -1)
						{
							RotateR(p);
						}
						//第二种情况,新节点插入了较高右子树的右侧(简称右右)
						if (p->_bf==2&&q->_bf==1)
						{
							RotateL(p);
						}
						//第三种情况,新节点插入了较高左子树的右侧(简称左右)
						if (p->_bf==-2&&q->_bf==1)
						{
							RotateLR(p);
						}
						//第四种情况,新节点插入了较高右子树的左侧(简称右左)
						if (p->_bf==2&&q->_bf==-1)
						{
							RotateRL(p);
						}
						break;

					}
				}
			}
			
			return true;
		}
	}
private:
	void RotateR(Node* p)
	{
		if (p != _root)
		{
			Node* subL = p->_left;
			Node* subLR = subL->_right;
			Node* p_parent = p->_parent;

			subL->_right = p;
			subL->_parent = p_parent;

			if (p == p_parent->_right)
				p_parent->_right = subL;
			else
				p_parent->_left = subL;

			p->_left = subLR;
			p->_parent = subL;
			
			if (subLR != nullptr)
				subLR->_parent = p;

			p->_bf = 0;
			subL->_bf = 0;
		}
		else
		{
			Node* subL = p->_left;
			Node* subLR = subL->_right;

			_root = subL;
			subL->_right = p;
			subL->_parent = nullptr;

			p->_left = subLR;
			p->_parent = subL;

			if (subLR != nullptr)
				subLR->_parent = p;

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

	void RotateL(Node* p)
	{
		if (_root != p)
		{
			Node* subR = p->_right;
			Node* subRL = subR->_left;
			Node* p_parent = p->_parent;

			subR->_left = p;
			subR->_parent = p_parent;

			p->_parent = subR;
			p->_right = subRL;

			if (p == p_parent->_right)
				p_parent->_right = subR;
			else
				p_parent->_left = subR;

			if(subRL!=nullptr)
				subRL->_parent = p;

			p->_bf = 0;
			subR->_bf = 0;
		}
		else
		{
			Node* subR = p->_right;
			Node* subRL = subR->_left;

			_root = subR;
			subR->_left = p;
			subR->_parent = nullptr;

			p->_right = subRL;
			p->_parent = subR;

			if (subRL != nullptr)
				subRL->_parent = p;

			p->_bf = 0;
			subR->_bf = 0;
		}
		
	}
	//LR表示左右双旋
	void RotateLR(Node* p)
	{
		Node* subL = p->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;//在【插入新节点后,左右双旋前】的这个时间段,subLR节点的平衡因子是作为【左右双旋旋转完毕后更新subL、subLR、p节点的平衡因子】的依据的,但因为下面调用RotateR或者RotateL函数时,内部会有修改subLR节点的平衡因子的逻辑,所以提前记录值,避免值丢失了

		RotateL(subL);
		RotateR(p);
		if (bf == -1)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			p->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			p->_bf = 0;
		}
		else if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			p->_bf = 0;
		}
	}

	//RL表示右左双旋
	void RotateRL(Node* p)
	{
		Node* subR = p->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;//在【插入新节点后,左右双旋前】的这个时间段,subRL节点的平衡因子是作为【右左双旋旋转完毕后更新subR、subRL、p节点的平衡因子】的依据的,但因为下面调用RotateR或者RotateL函数时,内部会有修改subLR节点的平衡因子的逻辑,所以提前记录值,避免值丢失了

		//右单旋的第一种写法,模拟RotateR的流程,因为subR不是整棵树的根节点,所以我们清楚右单旋的树不是整棵树,所以这里不用分情况
		Node* p1 = subR;
		Node* p1_parent = p1->_parent;
		Node* subL1 = p1->_left;
		Node* subL1R = subL1->_right;

		subL1->_right = p1;
		subL1->_parent = p1_parent;

		p1->_left = subL1R;
		p1->_parent = subL1;

		if (p1 == p1_parent->_right)
			p1_parent->_right = subL1;
		else
			p1_parent->_left = subL1;

		if (subL1R != nullptr)
			subL1R->_parent = p1;
		//在模拟RotateR时,我们就不更新平衡因子了,因为没有意义,即使更新了,这里的平衡因子也只是一个中间态,最后在右左双旋完毕后还会再更新平衡因子的
		//模拟RotateR结束

		//右单旋的第二种写法,不模拟RotateR的流程,直接复用RotateR的代码
		//RotateR(subR);


		//RotateL就不模拟了,直接复用RotateL函数的代码
		RotateL(p);

		if (bf == -1)
		{
			subR->_bf = 1;
			subRL->_bf = 0;
			p->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			p->_bf = -1;
		}
		else if (bf == 0)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			p->_bf = 0;
		}
	}

private:
	Node* _root;
};

AVL树的Insert的时间复杂度

接下来分析一下Insert的时间复杂度,AVL树的插入=BSTree的插入+更新平衡因子(+旋转),注意插入节点不一定会旋转,只是可能会旋转,所以这里打了个括号。

<<二叉搜索树(BSTree)的介绍与模拟实现>>一文中说过,二叉搜索树的插入的时间复杂度为O(h),h表示树的高度,而因为AVLTree也是搜索树,所以AVLTree的插入的时间复杂度也为O(h),又因为AVLTree的高度是平衡的,所以AVLTree的高度为logN,N表示树的节点总数,所以AVLTree的插入的时间复杂度为O(logN)。

然后是更新平衡因子所用的时间复杂度,指针p不断向上走更新平衡因子最多会更新高度次,所以更新平衡因子的时间复杂度也为O(logN),N为节点总数。

然后是旋转的时间复杂度,观察各类旋转函数Rotate的代码可以发现,旋转的时间复杂度是O(1)。

所以综合起来,在N个节点中Insert插入一个节点的时间复杂度为O(logN)+O(logN)+O(1)=O(logN)。

如何检验一棵树是否为平衡二叉搜索树呢?

首先检验这棵树是否为搜索树,如果是搜索树,再检验这棵搜索树是否具有平衡性。这里笔者只说明如何检验平衡性,关于如何检验是否为搜索树就不详细说明了(比较粗糙的检验方法是中序遍历打印一下,看看是否为升序且没有重复元素,中序遍历Inorder的代码如下)。

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	AVLTree<T>()
		:_root(nullptr)
	{}

public:
	void Inorder()
	{
		_Inorder(_root);
	}
private:
	void _Inorder(Node*root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_data << ' ';
		_Inorder(root->_right);
	}

private:
	Node* _root;
};

那如何检验搜索树是否平衡呢?

有人可能会说:可以层序遍历打印一下。这种方法在节点少的时候的确能起作用,毕竟能肉眼检查嘛,但节点多的时候,肉眼就看不过来了,所以不太合适。

真正的检验方法:编写一个检验函数用于计算每一个节点的右子树和左子树的高度差,如果每一个节点的右子树和左子树的高度差的绝对值都不超过1,那么整棵树就一定平衡了。

问题:有人可能会说,高度差不就是平衡因子吗,在Insert节点后,节点的平衡因子不是已经被更新过了吗,那直接遍历所有节点检查一遍有没有节点的平衡因子的绝对值大于1不就可以了,干嘛又编写一个检验函数来计算高度差?

答案:注意当前的情景,我们之所以检验一棵树是否为平衡二叉搜索树,是因为我们不确定自己模拟实现的AVL树的Insert接口是否正确,不确定插入节点后节点们形成的结构是不是AVL树,如果咱们编写的Insert接口有问题,比如在旋转子树时,更新平衡因子时给的值有问题,那这个平衡因子就不能作为判断是否平衡的依据。所以,只有编写一个接口计算每一个节点的右子树和左子树的高度差,我们才能清楚这棵树是否真正的高度平衡,比较神之一手的是,我们还可以通过这个接口,检测是否有节点的平衡因子的值异常,即检测是否有节点的平衡因子的值不等于右子树和左子树的高度差。

代码如下。bool isbalance()就是上面所说的检验函数。

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
    AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
    int height(Node*root)
	{
		if (root == nullptr)
			return 0;
		int left_height = height(root->_left);
		int right_height = height(root->_right);
		return max(left_height, right_height) + 1;
	}
public:
	bool isbalance()
	{
		return _isbalance(_root);
	}
private:
	bool _isbalance(Node* root)
	{
		if (root == nullptr)
			return true;
		int left = height(root->_left);
		int right = height(root->_right);
		int bf = right - left;

        if(abs(bf)!=root->_bf)
            cout<<"平衡因子设置异常";

		if (abs(bf) >= 2)
			return false;
		else
		{
			bool x = _isbalance(root->_left);
			bool y = _isbalance(root->_right);
			return x && y;
		}
	}
private:
	Node* _root;
};

Erase函数

实现过程

平衡二叉搜索树也是二叉搜索树,所以AVL树的删除和二叉搜索树的删除有很多共同点。

AVL树的删除过程可以分为三步:

1. 按照二叉搜索树的方式删除目标节点

(如果你忘记了BSTree删除节点的思路,请先回顾<<二叉搜索树的介绍与模拟实现>>一文,否则很难继续往下谈)

2. 更新被删除节点的祖先节点的平衡因子

3.如果更新完祖先的平衡因子后,发现平衡因子的绝对值大于1,说明树的高度的平衡被破坏,需要进行调整(旋转);如果平衡因子的绝对值等于1,则无需调整;如果平衡因子的绝对值等于0,则需要继续向上更新祖先节点的平衡因子。当前节点的平衡因子=当前节点的右子树的高度-当前节点的左子树的高度。

所以AVLTree的删除=BSTree的删除+更新平衡因子(+旋转【这一步不一定需要做】)。

根据BSTree的Erase的思路,我们就能编写完AVLTree的Erase的第一阶段,代码如下。

在AVL树的Erase中更新平衡因子的规则,如果被删的节点是父亲节点的左孩子,则父亲节点的平衡因子要+1;如果被删的节点是父亲节点的右孩子,则父亲节点的平衡因子要-1。

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	AVLTree<T>()
		:_root(nullptr)
	{}

	bool Erase(const T& x)
	{
		//这4个指针不一定都会被用到,如果被删除的节点只有左孩子或者只有右孩子,则只会用到前两个指针;如果被删除的节点既有左孩子,又有右孩子,则只会用到后两个指针
		Node* cur = _root;
		Node* curParent = cur->_parent;
		Node* Rmin=nullptr;
		Node* RminParent=nullptr;

		//flag数组用于确定上面4个指针中到底哪两个指针会被用到,辅助后序把用到的两个指针赋值给q和p,辅助完成平衡因子的更新
		int flag[2] = { 0,0 };
		//先找需要删除的目标节点
		while (cur != nullptr)
		{
			if (x > cur->_data)
			{
				curParent = cur;
				cur = cur->_right;
			}
			else if (x < cur->_data)
			{
				curParent = cur;
				cur = cur->_left;
			}
			//找到了需要删除的目标节点,开始删除
			else
			{
				//要删除的结点只有左孩子结点
				if (cur->_right == nullptr)
				{
					//要删除的节点是整棵树的根节点时,走这里
					if (cur == _root)
					{
						_root = cur->_left;
						if (_root != nullptr)//防止解引用空指针
						{
							_root->_parent = nullptr;
						}
						delete cur;
						return true;
					}
					//要删除的节点不是整棵树的根节点时,走这里
					else
					{
						flag[0] = 1;
						if (cur == curParent->_left)
						{
							curParent->_left = cur->_left;
							curParent->_bf++;
						}
						else
						{
							curParent->_right = cur->_left;
							curParent->_bf--;
						}
						delete cur;
					}
					
				}
				//要删除的结点只有右孩子结点
				else if (cur->_left == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_right;
						if (_root != nullptr)//防止解引用空指针
						{
							_root->_parent = nullptr;
						}
						delete cur;
						return true;
					}
					else
					{
						flag[0] = 1;
						if (cur == curParent->_left)
						{
							curParent->_left = cur->_right;
							curParent->_bf++;
						}
						else
						{
							curParent->_right = cur->_right;
							curParent->_bf--;
						}
						delete cur;
					}
				
				}
				//要删除的结点有左、右孩子结点
				else
				{	
					flag[1] = 1;
					Rmin = cur->_right;
					RminParent = cur;
					while (Rmin->_left != nullptr)
					{
						RminParent = Rmin;
						Rmin = Rmin->_left;
					}

					cur->_data = Rmin->_data;
					//这里先更新平衡因子,再把指针指向改变,以防解引用空指针报错
					//先更新平衡因子
					if (Rmin == RminParent->_left)
						RminParent->_bf++;
					else
						RminParent->_bf--;
					//再改变指针指向
					if (Rmin == RminParent->_left)
						RminParent->_left = Rmin->_right;
					else
						RminParent->_right = Rmin->_right;
										
					delete Rmin;

				}
//走到这里,已经删除了某个节点,并更新了该节点的父亲节点的平衡因子,第一阶段的代码就结束了,马上开始第二阶段的任务,即根据该被删除的节点的父亲节点的平衡因子控制平衡。
                /*
                
                第二阶段的代码未编写
                  
                                */
			
		//没有在BSTree中找到关键码为key的节点,删除失败,直接return false
		return false;		
	}

节点已经删除完毕了,接下来只需要探讨该如何控制树的高度平衡即可。

首先要知道,在AVLTree中删除一个节点时,真正被删除,即物理空间都被删除的节点必定是一个度为1或者0的节点,即要么是只有左孩子的节点,要么是只有右孩子的节点,要么是没有孩子的节点。为什么呢?因为AVLTree的删除遵循BSTree的删除,所以当你要删除一个既有左孩子又有右孩子的节点A时,会自动找一个只有左孩子或者只有右孩子或者左右孩子都没有的替死鬼节点B,然后通过删除B来达到删除A的效果。而当需要被删除的节点A只有左孩子或者只有孩子时,需要被删除的节点A被删除后,不用更新该节点的所有孩子节点的平衡因子(孩子表示可能是儿子,也可能是孙子、孙孙子),为什么呢?因为在删除节点前整棵树就是平衡的,所以所有子树都是平衡的,就包括以【被删除的节点的孩子节点】为根节点的子树,删除节点后,以【被删除的节点的孩子节点】为根节点的子树的高度又不变,当然不用更新被删除节点的所有孩子节点的平衡因子。

————分割线————

在目前节点A被删除前,节点A的父亲节点B的平衡因子分为三种情况: - 1、0、1;注意不可能为-2或者+2,因为在删除节点A前,整棵树是平衡的。

在节点A被删除后,更新节点A的父亲节点B的平衡因子时分以下两种情况:其一,如果A是B的左孩子,则给节点B的平衡因子加1即可。其二,如果A是B的右孩子,则给节点B的平衡因子减1即可。为什么呢?因为咱们规定平衡因子的值=右子树的高度 - 左子树高度。

在节点A被删除后,节点A的父亲节点B的平衡因子被更新后可能有3种情况:第一是正负1、第二是正负2,第三是0,对于这3种情况,又有3种处理方案,如下所示:

第一种处理方案:在节点A被删除后,如果节点A的父亲节点B的平衡因子被更新为正负1(可结合下图思考),说明在节点A被删除前,节点B的平衡因子一定为0,只是删除节点A后被调整成正负1了,比如根据平衡因子的更新规则,如果A是B的左孩子,删除A后,B的平衡因子加1,从0变成1,如果A是B的右孩子,删除A后,B的平衡因子减1,从0变成-1。删除A后,B的平衡因子被更新成正负1,说明此时整棵树的高度不变(原因在下一段),无需再更新节点B的所有祖先节点的平衡因子,删除节点A成功。

问题1:为什么上一段说,此时整棵树的高度不变,无需再更新节点B的所有祖先节点的平衡因子,删除节点A成功呢?

答案1:因为删除A前,B的平衡因子为0,说明B的左右子树的高度一定是相等的,删除A后,B的平衡因子被更新成正负1,说明要么是B的左子树的高度降1,要么是B的右子树的高度降1,即B的左右子树的高度差只有1,所以树B依然是平衡的,同时因为以B为根节点的树的高度不变,所以以B的祖先节点为根节点的所有树的高度都不变,在删除A前,它们都是平衡的,删除A后,它们的高度又不变,所以以B的祖先节点为根节点的所有树依然是平衡的,综上所述,整棵树都是平衡的,所以无需再更新节点B的所有祖先节点的平衡因子,删除节点A成功。

————分割线————

第二种处理方案:在节点A被删除后,如果节点A的父亲节点B的平衡因子被更新为正负2(可结合下图思考),说明在节点A被删除前,节点B的平衡因子一定为正负1,节点B的左右子树的高度差为1,只是删除节点A后,把节点B的左右子树中高度较低的一方的高度降低了1后,节点B的平衡因子被调整成正负2了,比如根据平衡因子的更新规则,如果A是B的左孩子,删除A后,因为B的左子树高度降低1,所以B的平衡因子加1,从1变成2,如果A是B的右孩子,删除A后,因为B的右子树的高度降低了1,所以B的平衡因子减1,从-1变成-2。删除A后,B的平衡因子被更新成正负2,说明以节点B为根节点的树已经不平衡了,需要进行旋转调整平衡。

如何旋转呢?接下来咱们说一说。

删除目标节点后,p从目标节点开始向上走一次更新目标节点的父亲节点的X的平衡因子时,X及其X的另一个孩子节点(要删除的目标节点只是X的两个孩子节点的其中之一)的平衡因子有以下6种情况。每种情况按照各自给的旋转方法去旋即可控制平衡。

这里分析一下Insert和Erase的旋转,目的是探索Erase的旋转相比Insert有什么不同。

Insert插入节点时需要旋转是因为新插入了节点导致树的高度增加,并且树的高度增加后还不平衡。而因为旋转的本质就是降低树的高度,所以在插入节点并旋转完毕平衡后,被旋转的树的高度对比插入节点前的高度是不变的(插入高度+1,旋转高度-1,高度不变),它的高度不变,那以被旋转后的树的根节点的祖先节点为根节点的所有树的高度都不会变,而因为在新插入节点前,它们是平衡的,插入新节点后,所有树的高度不变,当然还是平衡的,所以整棵树都是平衡的,所以在Insert时发生旋转后,一定是无需让指针p从被旋转后的树的根节点向上继续更新平衡因子的。

Erase删除结点时需要旋转是因为删除了节点并且导致树的高度不变,并且树的根节点的平衡因子为正负2,即虽然树的高度不变,但树的高度是不平衡的。而因为旋转的本质就是降低树的高度,在删除了树中的某个节点并且树发生了旋转后,树的高度是有可能降低的(只是可能,不是一定,下面6张图都是示例),而下面知识点1中说过,【只要在一棵树1中删除了某个节点导致树1的高度降低了,就有可能导致以树1根节点的父亲节点为根节点的树2的高度降低或者不变】,而下面知识点2说过,【只要一棵树1的某个节点被删除后导致树1的高度降低,就有可能导致并且以树1根节点的父亲节点为根节点的树2的高度不变,而只要树2的高度不变,则就有可能导致树2不平衡】,综上所述,1、在删除节点并发生了旋转后,只要被旋转的树的高度降低,则以被旋转后的树的根节点的父亲节点为根节点的树的高度是有可能不变的,进而有可能导致以被旋转的树的根节点的父亲节点为根节点的树不平衡,所以指针p需要从被旋转的树的根节点开始向上走一次更新平衡因子,看看以被旋转的树的根节点的父亲节点为根节点的树到底是否平衡。2、在删除节点并发生了旋转后,树被调整平衡了,只要此时被旋转的树的高度不变,则以该树根节点的祖先节点为根节点的所有树的高度都不会变,在删除前,它们都是平衡的,在删除后,高度又不变,当然也都是平衡的,所以整棵树都是平衡的,所以在这种情况下,指针p不需要从被旋转的树的根节点开始向上走一次更新平衡因子(注意只是一次),比如下面旋转处理的六种情况当中,若属于情况2或情况5,那么在旋转后指针p不需要从被旋转后的树的根节点开始往上更新一次平衡因子,因为这两种情况旋转后被旋转的树的高度并没有发生变化。当然其他4种情况是需要指针p从被旋转后的树的根节点开始向上走一次更新平衡因子的。

1.(示例如下图)当p指向的节点X的平衡因子为  - 2,X的左孩子的平衡因子为 - 1时,进行右单旋。 

2.(示例如下图)当p指向的节点X的平衡因子为  - 2,X的左孩子的平衡因子为0时,也进行右单旋。因为旋转后,被旋转的树的高度不变,所以旋转完毕后p无需向上走更新平衡因子。

注意:在该种情况下,更新树的根节点p和根节点的左孩子subL的平衡因子时,因为右单旋函数内部会把p和subL的平衡因子都更新成0,但根据下图最右边部分的平衡因子的情况可以看出这是不符合实情的,所以在右单旋函数调用结束后,需要在右单旋函数外面把p和subL的平衡因子分别改成-1和1。

3.(示例如下图)当p指向的节点X的平衡因子为  - 2,X的左孩子的平衡因子为1时,进行左右双旋。(注意,不要看下图中进行左右旋转后的3个节点的平衡因子为0,就在代码中无脑把这三个节点的平衡因子设置为0,这是不正确的,因为下图只是左右双旋的3种情况中的一种,有哪三种呢?在上文讲解insert时我们详细说明过了左右双旋如何更新平衡因子,请回看上文)

4.(示例如下图)当p指向的节点X的平衡因子为 2,X的右孩子的平衡因子为1时,进行左单旋。

5.(示例如下图)当p指向的节点X的平衡因子为 2,X的右孩子的平衡因子为0时,也进行左单旋。因为旋转后,被旋转的树的高度不变,所以旋转完毕后p无需向上走更新平衡因子。

注意:在该种情况下,更新树的根节点p和根节点的右孩子subR的平衡因子时,因为左单旋函数内部会把p和subR的平衡因子都更新成0,但根据下图最右边部分的平衡因子的情况可以看出这是不符合实情的,所以在左单旋函数调用结束后,需要在左单旋函数外面把p和subR的平衡因子分别改成1和-1。

6.(示例如下图)当p指向的节点X的平衡因子为 2,X的右孩子的平衡因子为 - 1时,进行右左双旋。(注意,不要看下图中进行右左旋转后的3个节点的平衡因子为0,就在代码中无脑把这三个节点的平衡因子设置为0,这是不正确的,因为下图只是右左双旋的3种情况中的一种,有哪三种呢?在上文讲解insert时我们详细说明过了右左双旋如何更新平衡因子,请回看上文)

————分割线———— 

第三种处理方案:在节点A被删除后,如果节点A的父亲节点B的平衡因子被更新为0(可结合下图思考),说明在节点A被删除前,节点B的平衡因子一定为正负1,节点B的左右子树的高度差一定为1,只是删除节点A后,把节点B左右子树中高的一方的高度降低1后,节点B的左右子树高度相等了,节点B的平衡因子被调整成0了,比如根据平衡因子的更新规则,如果A是B的左孩子,删除A后,因为B的左子树高度减1(A被删,B的左子树的高度一定减1,因为A是B的左子树的根节点),所以B的平衡因子加1,从-1变成0,如果A是B的右孩子,删除A后,因为B的右子树高度减1,所以B的平衡因子减1,从1变成0。此时以节点B为根节点的树的高度降低,所以就需要指针p从节点B开始向上更新一次节点B的父亲节点C的平衡因子(原因在下文中),指针p更新完节点B的父亲节点C的平衡因子后,根据C的平衡因子的值,又有3种可能,对这3种可能又有各自的处理动作,关于这3种可能的详情请继续往下看。

首先得知道几个知识点。

知识点1:(结合下图思考)任意一个节点A被删除后,以节点A的父亲节点B为根节点的树的高度可能变低,也可能不变。为什么呢?节点A要么是节点B左子树的根节点,要么是节点B右子树的根节点,如果节点A被删除了,节点A所在的子树的高度一定会降低1(因为A是子树根节点),如果此时节点A所在子树的高度大于等于节点B的另一棵子树,则说明以节点A的父亲节点B为根节点的树的高度变低,如果节点A所在子树的高度小于节点B的另一颗子树,则以节点A的父亲节点B为根节点的树的高度不变。从这可得到一个泛型的结论:不管是因为节点A被删除了,还是节点A的孩子节点(孩子节点包括儿子,孙子,孙孙子等)被删除了,只要删除后导致以节点A为根节点的树的高度降低,就有可能导致以节点A的父亲节点B为根节点的树的高度降低或者不变。或者换句话说,只要在一棵树1中删除了某个节点导致树1的高度降低了,就有可能导致以树1根节点的父亲节点为根节点的树2的高度降低或者不变。

知识点2:在知识点1中说过,【任意一个节点A被删除后,以节点A的父亲节点B为根节点的树的高度可能变低,也可能不变】。

这里要说的是,(结合下图1思考)如果任意一个节点A被删除后,以节点A的父亲节点B为根节点的树的高度不变,则以B为根节点的树有可能不平衡。为什么呢?节点A要么是节点B左子树的根节点,要么是节点B右子树的根节点,如果节点A被删除了,节点A所在的子树的高度一定会降低1(因为A是子树根节点),同时因为此时以节点B为根节点的树的高度不变,这意味着要么是删除A前,节点B的左右子树高度相等,然后随便一棵子树的高度降低1,此时节点B的左右子树高度差为1,以B为根节点的树平衡;要么是删除A前,节点B的左右子树高度就不相等,高度差为1(不可能为2,否则在删除A前就已经不平衡了),此时节点A是节点B左右子树中较低的一棵子树的根节点,然后把节点A删除后,节点B的左右子树高度差为2,此时就不平衡了。从这可得到一个泛型的结论1:不管是因为节点A被删除了,还是节点A的孩子节点(孩子节点包括儿子,孙子,孙孙子等)被删除了,只要删除后导致以节点A为根节点的树的高度降低了,就有可能导致以节点A的父亲节点B为根节点的树的高度不变,而只要以节点A的父亲节点B为根节点的树的高度不变,就有可能导致以节点A的父亲节点B为根节点的树不平衡。或者换句话说,只要一棵树1的某个节点被删除后导致树1的高度降低,就有可能导致并且以树1根节点的父亲节点为根节点的树2的高度不变,而只要树2的高度不变,则就有可能导致树2不平衡。

然后要说的是,(结合下图2思考)如果任意一个节点A被删除后,以节点A的父亲节点B为根节点的树的高度降低,则以节点B为根节点的树一定是平衡的。为什么呢?因为节点A被删除前,以节点A的父亲节点B为根节点的树就是平衡的,现在节点A被删除后,以节点B为根节点的树的高度还降低了,所以以节点B为根节点的树一定依然是平衡的。//——分割线——//(结合下图3思考)虽然此时以节点B为根节点的树肯定是平衡的,但有一个问题,现在删除了B的孩子节点A,导致以节点B为根节点的树的高度降低了,根据上面知识点1的泛型结论可得,是有可能导致以节点B的父亲节点C为根节点的树的高度降低或者不变的,重点就是有可能导致以节点B的父亲节点C为根节点的树的高度不变,只要以节点B的父亲节点C为根节点的树的高度不变,根据上一段中的泛型结论1可得,树C的高度是有可能不平衡的。从这可得到一个泛型的结论2:不管是因为节点A被删除了,还是节点A的孩子节点(孩子节点包括儿子,孙子,孙孙子等)被删除了,只要删除后导致以节点A为根节点的树的高度降低,就有可能导致以节点A的父亲节点B为根节点的树的高度降低,而只要以节点A的父亲节点B为根节点的树的高度降低,根据知识点1的泛型结论可得,就有可能导致以B的父亲节点C为根节点的树的高度不变,而只要以B的父亲节点C为根节点的树的高度不变,根据上一段的泛型结论1可得,就有可能导致以节点B的父亲节点C为根节点的树不平衡。或者换句话说,只要一棵树1的某个节点被删除后导致树1的高度降低,就有可能导致以树1根节点的父亲节点为根节点的树2的高度降低,而只要树2的高度降低,则有可能导致以树2根节点的父亲节点为根节点的树3的高度不变,而只要树3的高度不变,则有可能导致树3不平衡。

图1如下。 

图2如下。 

图3如下。 

问题:为什么上面第三种处理方案中说,删除节点A后(节点A是节点B的孩子节点),以节点B为根节点的树的高度降低后,就需要指针p从节点B开始向上更新一次节点B的父亲节点C的平衡因子呢?

答案:(示例如下图)根据上面的知识点1,删除了以节点B为根节点的树中的某个节点导致该树的高度降低后,以节点B的父亲节点C为根节点的树的高度就有可能不变或者变低,根据知识点2,删除了以节点B为根节点的树中的某个节点导致该树的高度降低后,只要以节点B的父亲节点C为根节点的树的高度不变,以C为根节点的树就有可能不平衡。正是因为以C为根节点的树可能不平衡,所以需要指针p从节点B开始向上更新一次节点B的父亲节点C的平衡因子。

在第三种处理方案中我们说过,更新完节点B的父亲节点C的平衡因子后,根据C的平衡因子的值,又有3种可能,对这3种可能又有各自的处理动作,接下来咱们说说这3种可能。

3种可能中的其一是:C的平衡因子为正负1。如果指针p从被删除节点A的父亲节点B开始向上走一次,更新完B的父亲节点C的平衡因子后,C的平衡因子的值为正负1,说明C的平衡因子在更新前一定是0,C的左右子树的高度一定是相等的,删除A后,C的平衡因子被更新成正负1,说明要么是C的左子树的高度降1,要么是C的右子树的高度降1,即C的左右子树的高度差只有1,所以树C依然是平衡的,同时因为以C为根节点的树的高度不变,所以以C的祖先节点为根节点的所有树的高度都不变,在删除A前,它们都是平衡的,删除A后,它们的高度又不变,所以以C的祖先节点为根节点的所有树依然是平衡的,指针p无需从C开始向上更新一次C的父亲节点的平衡因子,整棵树都是平衡的,删除节点A成功。从前面这个实际案例可以举一反三推导出一个泛型的结论:指针p从被删除节点A的父亲节点B开始(包括B)到整棵树的根节点结束,只要更新完p指向的节点X的平衡因子后,发现值是正负1,则指针p无需继续向上更新平衡因子,删除函数结束,删除节点A成功。

3种可能中的其二是:C的平衡因子为0。如果指针p从被删除节点A的父亲节点B开始向上走一次,更新完B的父亲节点C的平衡因子后,C的平衡因子的值为0,说明C的平衡因子在更新前一定是正负1,节点C的左右子树的高度差为1,删除A后,C的平衡因子被更新成0,说明删除A后把节点C的左右子树中较高的一方的高度降低了1,此时节点C的左右子树的高度相等,所以以节点C为根节点的树是平衡的,但注意,此时以节点C为根节点的树的高度是降低了的,根据上面知识点2的泛型结论2可得,就有可能导致以节点C的父亲节点D为根节点的树的高度不变,从而导致以节点D为根节点的树不平衡,既然以节点D为根节点的树有可能不平衡,所以此时p需要从节点C开始向上走一次更新节点D的平衡因子,看看以节点D为根节点的树到底是否平衡。从前面这个实际案例可以举一反三推导出一个泛型的结论:指针p从被删除节点A的父亲节点B开始(包括B)到整棵树的根节点结束,只要更新完p指向的节点X的平衡因子后,发现值是0,则表示删除目标节点后,以节点X为根节点的树的高度是降低了的,则就有可能导致以节点X的父亲节点Y为根节点的树的高度不变,而只要以节点X的父亲节点Y为根节点的树的高度不变,就有可能导致以节点Y为根节点的树不平衡,既然以节点Y为根节点的树有可能不平衡,则指针p就需要向上走一次更新X节点的父亲节点Y的平衡因子,然后根据平因子的值看看到底是否平衡。最多走到根节点更新完根节点的平衡因子后结束Erase函数。

3种可能中的其三是:C的平衡因子为正负2。如果指针p从被删除节点A的父亲节点B开始向上走一次,更新完B的父亲节点C的平衡因子后,C的平衡因子的值为正负2,说明C的平衡因子在更新前一定是正负1,节点C的左右子树的高度差为1,删除A后,C的平衡因子被更新成正负2,说明删除A后把节点C的左右子树中较低的一方的高度降低了1,此时节点C的左右子树的高度差为2,所以以节点C为根节点的树就不平衡了,需要对以节点C为根节点的树进行旋转控制平衡。如何旋转呢?上文中的旋转部分已经全部说过了。旋转后,这棵被旋转的树就平衡了,然后根据这棵旋转后的树的根节点的平衡因子决定指针p是否需要继续向上走更新旋转后的树的根节点的父亲节点的平衡因子,如果为0,根据上一段的结论,则p需要向上走一次更新平衡因子;如果为正负1,根据上上段的结论,则p无需向上走更新平衡因子,Erase函数结束,删除目标节点成功。从前面这个实际案例可以举一反三推导出一个泛型的结论:指针p从被删除节点A的父亲节点B开始(包括B)到整棵树的根节点结束,只要更新完p指向的节点X的平衡因子后,发现值是正负2,就说明以节点X为根节点的树不平衡,就需要对它做旋转处理,如何旋转在上文已经说过了,旋转完毕后,这个树就平衡了,然后根据旋转后的树的根节点的平衡因子决定下一步该怎么走。

走到这里,理论知识就全部讲解完毕了,咱们就可以开始Erase的第二阶段也就是最终阶段的代码了。

Erase的整体代码

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	AVLTree<T>()
		:_root(nullptr)
	{}

	bool Erase(const T& x)
	{
		//这4个指针不一定都会被用到,如果被删除的节点只有左孩子或者只有右孩子,则只会用到前两个指针;如果被删除的节点既有左孩子,又有右孩子,则只会用到后两个指针
		Node* cur = _root;
		Node* curParent = cur->_parent;
		Node* Rmin=nullptr;
		Node* RminParent=nullptr;

		//flag数组用于确定上面4个指针中到底哪两个指针会被用到,辅助后序把用到的两个指针赋值给q和p,辅助完成平衡因子的更新
		int flag[2] = { 0,0 };
		//先找需要删除的目标节点
		while (cur != nullptr)
		{
			if (x > cur->_data)
			{
				curParent = cur;
				cur = cur->_right;
			}
			else if (x < cur->_data)
			{
				curParent = cur;
				cur = cur->_left;
			}
			//找到了需要删除的目标节点,开始删除
			else
			{
				//要删除的结点只有左孩子结点
				if (cur->_right == nullptr)
				{
					//要删除的节点是整棵树的根节点时,走这里
					if (cur == _root)
					{
						_root = cur->_left;
						if (_root != nullptr)//防止解引用空指针
						{
							_root->_parent = nullptr;
						}
						delete cur;
						return true;
					}
					//要删除的节点不是整棵树的根节点时,走这里
					else
					{
						flag[0] = 1;
						if (cur == curParent->_left)
						{
							curParent->_left = cur->_left;
							curParent->_bf++;
						}
						else
						{
							curParent->_right = cur->_left;
							curParent->_bf--;
						}
						delete cur;
					}
					
				}
				//要删除的结点只有右孩子结点
				else if (cur->_left == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_right;
						if (_root != nullptr)//防止解引用空指针
						{
							_root->_parent = nullptr;
						}
						delete cur;
						return true;
					}
					else
					{
						flag[0] = 1;
						if (cur == curParent->_left)
						{
							curParent->_left = cur->_right;
							curParent->_bf++;
						}
						else
						{
							curParent->_right = cur->_right;
							curParent->_bf--;
						}
						delete cur;
					}
				
				}
				//要删除的结点有左、右孩子结点
				else
				{	
					flag[1] = 1;
					Rmin = cur->_right;
					RminParent = cur;
					while (Rmin->_left != nullptr)
					{
						RminParent = Rmin;
						Rmin = Rmin->_left;
					}

					cur->_data = Rmin->_data;
					//这里先更新平衡因子,再把指针指向改变,以防解引用空指针报错
					//先更新平衡因子
					if (Rmin == RminParent->_left)
						RminParent->_bf++;
					else
						RminParent->_bf--;
					//再改变指针指向
					if (Rmin == RminParent->_left)
						RminParent->_left = Rmin->_right;
					else
						RminParent->_right = Rmin->_right;
										
					delete Rmin;

				}
//走到这里,已经删除了某个节点,并更新了该节点的父亲节点的平衡因子,第一阶段的代码就结束了,马上开始第二阶段的任务,即根据该被删除的节点的父亲节点的平衡因子控制平衡。
               	//之前已经删除了某个节点,并更新了该节点的父亲节点的平衡因子,现在根据父亲节点的平衡因子控制平衡
				Node* p=nullptr;
				Node* q=nullptr;
				if (flag[0] == 1)
				{
					q = curParent;			
					p = q->_parent;
				}				
				else if (flag[1] == 1)
				{
					q = RminParent;
					p = q->_parent;
				}

				while (q != nullptr)
				{
					if (abs(q->_bf) == 1)
					{
						return true;
					}
					else if (abs(q->_bf) == 0)
					{
                        //删除目标节点后,如果q指针指向了整棵树的根节点,说明p指针已经把整棵树的根节点的平衡因子更新完毕了,此时直接退出函数,否则p会解引用空指针报错(因为p是q的父亲节点,q指向根节点,p就指向根节点的父亲节点,即p指向nullptr)
						if (q == _root)
							return true;

						if (q == p->_left)
							p->_bf++;
						else
							p->_bf--;

						q = p;
						p = q->_parent;
					}
					else if (abs(q->_bf) == 2)
					{
						if (q->_bf == -2 && q->_left->_bf == -1)
						{
							Node* temp = q->_left;//根据流程图,我们提前就知道子树旋转完毕后的新根节点是谁,所以提前把这个新根节点的地址给记录下来(如果不提前记录,子树的结构旋转后,再找子树的根节点会很麻烦),这么做的目的是在旋转完毕后继续向上更新子树的祖先节点的平衡因子时,就可以通过temp找到子树的祖先节点
							RotateR(q);
							q = temp;
							p = q->_parent;
						}
						else if (q->_bf == -2 && q->_left->_bf == 0)
						{
							//本种情况下子树旋转完毕后虽然无需继续向上更新子树的祖先节点的平衡因子,但也要根据流程图,提前定义temp指针记录子树旋转完毕后的新根节点的地址,因为要根据流程图把旋转后的树的根节点和根节点的右孩子的平衡因子更新
							Node* temp = q->_left;
							RotateR(q);
							temp->_bf = 1;
							temp->_right->_bf = -1;
							return true;
						}
						else if (q->_bf == -2 && q->_left->_bf == 1)
						{
							Node* temp=q->_left->_right;//根据流程图,我们提前就知道子树旋转完毕后的新根节点是谁,所以提前把这个新根节点的地址给记录下来(如果不提前记录,子树的结构旋转后,再找子树的根节点会很麻烦),这么做的目的是在旋转完毕后继续向上更新子树的祖先节点的平衡因子时,就可以通过temp找到子树的祖先节点
							RotateLR(q);
							q = temp;
							p = q->_parent;
						}
						else if (q->_bf == 2 && q->_right->_bf == 1)
						{
							Node* temp=q->_right;//根据流程图,我们提前就知道子树旋转完毕后的新根节点是谁,所以提前把这个新根节点的地址给记录下来(如果不提前记录,子树的结构旋转后,再找子树的根节点会很麻烦),这么做的目的是在旋转完毕后继续向上更新子树的祖先节点的平衡因子时,就可以通过temp找到子树的祖先节点
							RotateL(q);
							q = temp;
							p = q->_parent;
						}
						else if (q->_bf == 2 && q->_right->_bf == 0)
						{
							//本种情况下子树旋转完毕后虽然无需继续向上更新子树的祖先节点的平衡因子,但也要根据流程图,提前定义temp指针记录子树旋转完毕后的新根节点的地址,因为要根据流程图把旋转后的树的根节点和根节点的左孩子的平衡因子更新
							Node* temp = q->_right;
							RotateL(q);
							temp->_bf = -1;
							temp->_left->_bf = 1;
							return true;
						}
						else if (q->_bf == 2 && q->_right->_bf == -1)
						{
							Node* temp=q->_right->_left;//根据流程图,我们提前就知道子树旋转完毕后的新根节点是谁,所以提前把这个新根节点的地址给记录下来(如果不提前记录,子树的结构旋转后,再找子树的根节点会很麻烦),这么做的目的是在旋转完毕后继续向上更新子树的祖先节点的平衡因子时,就可以通过temp找到子树的祖先节点
							RotateRL(q); 
							q = temp;
							p = q->_parent;
						}
					}
				

			
		//没有在BSTree中找到关键码为key的节点,删除失败,直接return false
		return false;		
	}

最终测试

先把数据的个数设置小一点,如果出错方便调试,如下图所示。可以发现没问题,注意这样的检测方案一定要确保isbalance函数是正确的。

测试代码如下。

#include"AVLTree.h"
void test1()
{
	int a[10] = { 9,8,1,3,2,7,6,5,4,0};
	AVLTree<int>a1;
	for (auto e : a)
	{
		a1.Insert(e);
	}
	cout<<"所有值插入完毕后,isbalance返回值为:" << a1.isbalance() <<"		( 1 表示true、平衡)//( 0 表示false、不平衡)" << endl;

	for (int i = 0; i != 10; i++)
	{
		a1.Erase(a[i]);
		cout << "删除"<<a[i]<<"后,isbalance返回值为:" << a1.isbalance() << "			( 1 表示true、平衡)//( 0 表示false、不平衡)" << endl;

	}
}

void main()
{
	test1();
}

然后把数组的数量设置大一点,并放上随机数,如下图。可以发现也没问题。

测试代码如下。

#include"AVLTree.h"

void test2()
{

	int a[1000];
	AVLTree<int>a1;
	for (int i=0;i!=1000;i++)
	{
		a[i]=rand();
	}
	for (int i = 0; i != 1000; i++)
	{
		a1.Insert(a[i]);
	}
	cout << "所有值插入完毕后,isbalance返回值为:" << a1.isbalance() << "		( 1 表示true、平衡)//( 0 表示false、不平衡)" << endl;
	for (int i = 0; i != 1000; i++)
	{
		a1.Erase(a[i]);
		cout << "删除" << a[i] << "后,isbalance返回值为:" << a1.isbalance() << "			( 1 表示true、平衡)//( 0 表示false、不平衡)" << endl;

	}
}

void main()
{
	test2();
}

AVLTree的整体代码

文件AVLTree.h的代码如下。

#pragma once
#include<iostream>
using namespace std;


template<class T>
struct AVLTreeNode
{
	AVLTreeNode()
		:_data()
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	AVLTreeNode(const T&x)
		:_data(x)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}

	T _data;
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf; //balance factor(平衡因子)
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	AVLTree<T>()
		:_root(nullptr)
	{}


	bool Erase(const T& x)
	{
		//这4个指针不一定都会被用到,如果被删除的节点只有左孩子或者只有右孩子,则只会用到前两个指针;如果被删除的节点既有左孩子,又有右孩子,则只会用到后两个指针
		Node* cur = _root;
		Node* curParent = cur->_parent;
		Node* Rmin=nullptr;
		Node* RminParent=nullptr;

		//flag数组用于确定上面4个指针中到底哪两个指针会被用到
		int flag[2] = { 0,0 };
		//先找需要删除的目标节点
		while (cur != nullptr)
		{
			if (x > cur->_data)
			{
				curParent = cur;
				cur = cur->_right;
			}
			else if (x < cur->_data)
			{
				curParent = cur;
				cur = cur->_left;
			}
			//找到了需要删除的目标节点,开始删除
			else
			{
				//要删除的结点只有左孩子结点
				if (cur->_right == nullptr)
				{
					//要删除的节点是整棵树的根节点时,走这里
					if (cur == _root)
					{
						_root = cur->_left;
						if (_root != nullptr)//防止解引用空指针
						{
							_root->_parent = nullptr;
						}
						delete cur;
						return true;
					}
					//要删除的节点不是整棵树的根节点时,走这里
					else
					{
						flag[0] = 1;
						if (cur == curParent->_left)
						{
							curParent->_left = cur->_left;
							curParent->_bf++;
						}
						else
						{
							curParent->_right = cur->_left;
							curParent->_bf--;
						}
						delete cur;
					}
					
				}
				//要删除的结点只有右孩子结点
				else if (cur->_left == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_right;
						if (_root != nullptr)//防止解引用空指针
						{
							_root->_parent = nullptr;
						}
						delete cur;
						return true;
					}
					else
					{
						flag[0] = 1;
						if (cur == curParent->_left)
						{
							curParent->_left = cur->_right;
							curParent->_bf++;
						}
						else
						{
							curParent->_right = cur->_right;
							curParent->_bf--;
						}
						delete cur;
					}
				
				}
				//要删除的结点有左、右孩子结点
				else
				{	
					flag[1] = 1;
					Rmin = cur->_right;
					RminParent = cur;
					while (Rmin->_left != nullptr)
					{
						RminParent = Rmin;
						Rmin = Rmin->_left;
					}

					cur->_data = Rmin->_data;
					//这里先更新平衡因子,再把指针指向改变,以防解引用空指针报错
					//先更新平衡因子
					if (Rmin == RminParent->_left)
						RminParent->_bf++;
					else
						RminParent->_bf--;
					//再改变指针指向
					if (Rmin == RminParent->_left)
						RminParent->_left = Rmin->_right;
					else
						RminParent->_right = Rmin->_right;
										
					delete Rmin;

				}
				//之前已经删除了某个节点,并更新了该节点的父亲节点的平衡因子,现在根据父亲节点的平衡因子控制平衡
				Node* p=nullptr;
				Node* q=nullptr;
				if (flag[0] == 1)
				{
					q = curParent;			
					p = q->_parent;
				}				
				else if (flag[1] == 1)
				{
					q = RminParent;
					p = q->_parent;
				}

				while (q != nullptr)
				{
					if (abs(q->_bf) == 1)
					{
						return true;
					}
					else if (abs(q->_bf) == 0)
					{
                        //删除目标节点后,如果q指针指向了整棵树的根节点,说明p指针已经把整棵树的根节点的平衡因子更新完毕了,此时直接退出函数,否则p会解引用空指针报错(因为p是q的父亲节点,q指向根节点,p就指向根节点的父亲节点,即p指向nullptr)
						if (q == _root)
							return true;

						if (q == p->_left)
							p->_bf++;
						else
							p->_bf--;

						q = p;
						p = q->_parent;
					}
					else if (abs(q->_bf) == 2)
					{
						if (q->_bf == -2 && q->_left->_bf == -1)
						{
							Node* temp = q->_left;//根据流程图,我们提前就知道子树旋转完毕后的新根节点是谁,所以提前把这个新根节点的地址给记录下来(如果不提前记录,子树的结构旋转后,再找子树的根节点会很麻烦),这么做的目的是在旋转完毕后继续向上更新子树的祖先节点的平衡因子时,就可以通过temp找到子树的祖先节点
							RotateR(q);
							q = temp;
							p = q->_parent;
						}
						else if (q->_bf == -2 && q->_left->_bf == 0)
						{
							//本种情况下子树旋转完毕后虽然无需继续向上更新子树的祖先节点的平衡因子,但也要根据流程图,提前定义temp指针记录子树旋转完毕后的新根节点的地址,因为要根据流程图把旋转后的树的根节点和根节点的右孩子的平衡因子更新
							Node* temp = q->_left;
							RotateR(q);
							temp->_bf = 1;
							temp->_right->_bf = -1;
							return true;
						}
						else if (q->_bf == -2 && q->_left->_bf == 1)
						{
							Node* temp=q->_left->_right;//根据流程图,我们提前就知道子树旋转完毕后的新根节点是谁,所以提前把这个新根节点的地址给记录下来(如果不提前记录,子树的结构旋转后,再找子树的根节点会很麻烦),这么做的目的是在旋转完毕后继续向上更新子树的祖先节点的平衡因子时,就可以通过temp找到子树的祖先节点
							RotateLR(q);
							q = temp;
							p = q->_parent;
						}
						else if (q->_bf == 2 && q->_right->_bf == 1)
						{
							Node* temp=q->_right;//根据流程图,我们提前就知道子树旋转完毕后的新根节点是谁,所以提前把这个新根节点的地址给记录下来(如果不提前记录,子树的结构旋转后,再找子树的根节点会很麻烦),这么做的目的是在旋转完毕后继续向上更新子树的祖先节点的平衡因子时,就可以通过temp找到子树的祖先节点
							RotateL(q);
							q = temp;
							p = q->_parent;
						}
						else if (q->_bf == 2 && q->_right->_bf == 0)
						{
							//本种情况下子树旋转完毕后虽然无需继续向上更新子树的祖先节点的平衡因子,但也要根据流程图,提前定义temp指针记录子树旋转完毕后的新根节点的地址,因为要根据流程图把旋转后的树的根节点和根节点的左孩子的平衡因子更新
							Node* temp = q->_right;
							RotateL(q);
							temp->_bf = -1;
							temp->_left->_bf = 1;
							return true;
						}
						else if (q->_bf == 2 && q->_right->_bf == -1)
						{
							Node* temp=q->_right->_left;//根据流程图,我们提前就知道子树旋转完毕后的新根节点是谁,所以提前把这个新根节点的地址给记录下来(如果不提前记录,子树的结构旋转后,再找子树的根节点会很麻烦),这么做的目的是在旋转完毕后继续向上更新子树的祖先节点的平衡因子时,就可以通过temp找到子树的祖先节点
							RotateRL(q); 
							q = temp;
							p = q->_parent;
						}
					}
				}					
			}
		}
		//没有在BSTree中找到关键码为key的节点,删除失败,直接return false
		return false;		
	}





	bool Insert(const T&x)
	{
		if (_root == nullptr)
		{
			_root = new Node(x);
			return true;
		}
		else
		{
			Node* cur = _root;
			Node* curParent = nullptr;
			//先找插入位置,即空位置
			while (cur!=nullptr)
			{
				if (x > cur->_data)
				{
					curParent = cur;
					cur = cur->_right;
				}
				else if (x < cur->_data)
				{
					curParent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}
			//找到空位置,开始插入
			if (x > curParent->_data)
			{
				cur = new Node(x);
				curParent->_right = cur;
				cur->_parent = curParent;
			}
			else
			{
				cur = new Node(x);
				curParent->_left = cur;
				cur->_parent = curParent;
			}
			//插入完毕后,开始控制平衡,这是第二阶段的任务
			
			//先更新curParent的平衡因子
			if (cur == curParent->_left)
				curParent->_bf--;
			else
				curParent->_bf++;
			//然后判断curParent是哪种情况,要使用哪种解决方案
			//curParent的bf为0,属于第一种情况,使用第一种处理方案
			if (curParent->_bf == 0)
			{
				return true;
			}
			//curParent的bf为正负1,属于第二种情况,使用第二种处理方案
			else if(abs(curParent->_bf)==1)
			{
				//在当前情况下,需要让指针p不断向上走,更新curParent的祖先节点的平衡因子,为了p能完成【更新curParent的祖先节点的平衡因子】这个任务,设置了指针q辅助p,否则p就不知道要对指向节点的bf进行+1还是-1
				Node* q = cur;
				Node* p = curParent;
				while (p != nullptr)
				{
					//该if分支对应文中所说的3种可能中的其二
					if (p->_bf == 0)
					{
						return true;
					}
					//如果每次进入循环,都走这个分支,该if分支对应文中所说的3种可能中的其一
					else if (abs(p->_bf) == 1)
					{
						q = q->_parent;
						p = p->_parent;
						if (p != nullptr && q == p->_right)
							p->_bf++;
						else if (p != nullptr && q == p->_left)
							p->_bf--;
					}
					//该if分支对应文中所说的3种可能中的其三
					else if (abs(p->_bf) == 2)
					{
						//以p指向节点为根的子树不平衡,开始旋转调整,控制平衡,这是第三阶段(最终阶段)的任务

						//第一种情况:新节点插入了较高左子树的左侧(简称左左)
						if (p->_bf == -2 && q->_bf == -1)
						{
							RotateR(p);
						}
						//第二种情况,新节点插入了较高右子树的右侧(简称右右)
						if (p->_bf==2&&q->_bf==1)
						{
							RotateL(p);
						}
						//第三种情况,新节点插入了较高左子树的右侧(简称左右)
						if (p->_bf==-2&&q->_bf==1)
						{
							RotateLR(p);
						}
						//第四种情况,新节点插入了较高右子树的左侧(简称右左)
						if (p->_bf==2&&q->_bf==-1)
						{
							RotateRL(p);
						}
						break;
					}
				}
			}
			
			return true;
		}
	}
private:
	void RotateR(Node* p)
	{
		if (p != _root)
		{
			Node* subL = p->_left;
			Node* subLR = subL->_right;
			Node* p_parent = p->_parent;

			subL->_right = p;
			subL->_parent = p_parent;

			if (p == p_parent->_right)
				p_parent->_right = subL;
			else
				p_parent->_left = subL;

			p->_left = subLR;
			p->_parent = subL;
			
			if (subLR != nullptr)
				subLR->_parent = p;

			p->_bf = 0;
			subL->_bf = 0;
		}
		else
		{
			Node* subL = p->_left;
			Node* subLR = subL->_right;

			_root = subL;
			subL->_right = p;
			subL->_parent = nullptr;

			p->_left = subLR;
			p->_parent = subL;

			if (subLR != nullptr)
				subLR->_parent = p;

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

	void RotateL(Node* p)
	{
		if (_root != p)
		{
			Node* subR = p->_right;
			Node* subRL = subR->_left;
			Node* p_parent = p->_parent;

			subR->_left = p;
			subR->_parent = p_parent;

			p->_parent = subR;
			p->_right = subRL;

			if (p == p_parent->_right)
				p_parent->_right = subR;
			else
				p_parent->_left = subR;

			if(subRL!=nullptr)
				subRL->_parent = p;

			p->_bf = 0;
			subR->_bf = 0;
		}
		else
		{
			Node* subR = p->_right;
			Node* subRL = subR->_left;

			_root = subR;
			subR->_left = p;
			subR->_parent = nullptr;

			p->_right = subRL;
			p->_parent = subR;

			if (subRL != nullptr)
				subRL->_parent = p;

			p->_bf = 0;
			subR->_bf = 0;
		}
		
	}
	//LR表示左右双旋
	void RotateLR(Node* p)
	{
		Node* subL = p->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;//在【插入新节点后,左右双旋前】的这个时间段,subLR节点的平衡因子是作为【左右双旋旋转完毕后更新subL、subLR、p节点的平衡因子】的依据的,但因为下面调用RotateR或者RotateL函数时,内部会有修改subLR节点的平衡因子的逻辑,所以提前记录值,避免值丢失了

		RotateL(subL);
		RotateR(p);
		if (bf == -1)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			p->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			p->_bf = 0;
		}
		else if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			p->_bf = 0;
		}
	}

	//RL表示右左双旋
	void RotateRL(Node* p)
	{
		Node* subR = p->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;//在【插入新节点后,左右双旋前】的这个时间段,subRL节点的平衡因子是作为【右左双旋旋转完毕后更新subR、subRL、p节点的平衡因子】的依据的,但因为下面调用RotateR或者RotateL函数时,内部会有修改subLR节点的平衡因子的逻辑,所以提前记录值,避免值丢失了

		//右单旋的第一种写法,模拟RotateR的流程,因为subR不是整棵树的根节点,所以我们清楚右单旋的树不是整棵树,所以这里不用分情况
		Node* p1 = subR;
		Node* p1_parent = p1->_parent;
		Node* subL1 = p1->_left;
		Node* subL1R = subL1->_right;

		subL1->_right = p1;
		subL1->_parent = p1_parent;

		p1->_left = subL1R;
		p1->_parent = subL1;

		if (p1 == p1_parent->_right)
			p1_parent->_right = subL1;
		else
			p1_parent->_left = subL1;

		if (subL1R != nullptr)
			subL1R->_parent = p1;
		//在模拟RotateR时,我们就不更新平衡因子了,因为没有意义,即使更新了,这里的平衡因子也只是一个中间态,最后在右左双旋完毕后还会再更新平衡因子的
		//模拟RotateR结束

		//右单旋的第二种写法,不模拟RotateR的流程,直接复用RotateR的代码
		//RotateR(subR);


		//RotateL就不模拟了,直接复用RotateL函数的代码
		RotateL(p);

		if (bf == -1)
		{
			subR->_bf = 1;
			subRL->_bf = 0;
			p->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			p->_bf = -1;
		}
		else if (bf == 0)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			p->_bf = 0;
		}
	}

	int height(Node*root)
	{
		if (root == nullptr)
			return 0;
		int left_height = height(root->_left);
		int right_height = height(root->_right);
		return max(left_height, right_height) + 1;
	}
public:
	bool isbalance()
	{
		return _isbalance(_root);
	}
private:
	bool _isbalance(Node* root)
	{
		if (root == nullptr)
			return true;
		int left = height(root->_left);
		int right = height(root->_right);
		int bf = right - left;
		if (root->_bf != bf)
		{
			cout <<root->_data <<"的平衡因子异常"<<endl;
		}
		if (abs(bf) >= 2)
			return false;
		else
		{
			bool x = _isbalance(root->_left);
			bool y = _isbalance(root->_right);
			return x && y;
		}
	}
private:
	Node* _root;
};

文件test.cpp的代码如下。

#include"AVLTree.h"

void test2()
{

	int a[1000];
	AVLTree<int>a1;
	for (int i=0;i!=1000;i++)
	{
		a[i]=rand();
	}
	for (int i = 0; i != 1000; i++)
	{
		a1.Insert(a[i]);
	}
	cout << "所有值插入完毕后,isbalance返回值为:" << a1.isbalance() << "		( 1 表示true、平衡)//( 0 表示false、不平衡)" << endl;
	for (int i = 0; i != 1000; i++)
	{
		a1.Erase(a[i]);
		cout << "删除" << a[i] << "后,isbalance返回值为:" << a1.isbalance() << "			( 1 表示true、平衡)//( 0 表示false、不平衡)" << endl;

	}
}


void test1()
{
	int a[10] = { 9,8,1,3,2,7,6,5,4,0};
	AVLTree<int>a1;
	for (auto e : a)
	{
		a1.Insert(e);
	}
	cout<<"所有值插入完毕后,isbalance返回值为:" << a1.isbalance() <<"		( 1 表示true、平衡)//( 0 表示false、不平衡)" << endl;

	for (int i = 0; i != 10; i++)
	{
		a1.Erase(a[i]);
		cout << "删除"<<a[i]<<"后,isbalance返回值为:" << a1.isbalance() << "			( 1 表示true、平衡)//( 0 表示false、不平衡)" << endl;

	}
}

void main()
{
	test2();
}

AVLTree的性能(应用场景)

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即$Log_2(N)$。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值