【C++】AVL树(动图详解)

img

文章目录

  • 一、前言
  • 二、AVL树的概念(引入bf)
  • 三、AVL节点树的定义
  • 四、AVL树的基本框架
  • 五、AVL树的旋转
    • 5.1左单旋(新节点插入较高右子树的右侧---右右:左单旋)
      • 例一(h==0)
      • 例二(h==1)
      • 例三(抽象图)
      • 代码讲解
        • 1.更新双亲节点
        • 2.处理局部子树问题
        • 3.更新平衡因子
        • 4.代码汇总
      • 代码总结(俩孩子三双亲)
    • 5.2左单旋(新节点插入较高左子树的左侧---左左:右单旋)
      • 例一(h==0)
      • 例二(h==1)
      • 例三(抽象图)
      • 代码总结(代码解释见左单旋)
    • 5.3左右双旋(新节点插入较高左子树的右侧---左右:先左单旋再右单旋)
      • 例一(h==0)
      • 例二(h==1)
      • 例三(抽象图)
      • 代码讲解
    • 5.4右左双旋(新节点插入较高右子树的左侧---右左:先右单旋再左单旋)
      • 抽象图
  • 六、AVL树的插入
  • 七、AVL树相关的一些测试
  • 八、完整代码
    • AVL.h
    • AVL.cpp
  • 九、难点总结
    • 旋转的四个原则
    • 在进行旋转处理后就无需继续往上更新平衡因子了
    • 旋转口诀是插入较高... 不要把这个较高给忽略掉
    • 写代码的时候,头脑要清楚,不要忘了回溯parent的值(向上分配)
    • 插入代码更新平衡因子的条件是while(parent)
    • 对于单旋,利用俩孩子三双亲原则;对于双旋,难点在于平衡因子更新,得画出插入前和旋转后的图分析平衡因子(三种情况)
    • 双旋得提前记录subLR或者subRL的bf(旋转完了都找不到了)
    • 单旋得提前记录parent的_parent,也就是代码中的ppNode
    • 单旋的时候,解决局部子树问题时,总会忘记向上分配

一、前言

在上一篇文章中对 set、multiset、map、multimap 进行了简单的介绍,在其文档介绍中发现,这几个容器都有一个共同点:其底层都是借助二叉搜索树来实现的。但是==二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成为单支树==,时间复杂度会退化成 O(N) ,因此 set、map 等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。那么今天就让我们对 set 和 map 的底层结构一探究竟。


二、AVL树的概念(引入bf)

二叉搜索树虽然可以缩短查找的效率,但是如果数据有序或者接近有序,二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率就会变得低下。

因此,两位俄罗斯的数学家 G.M.Adelson-Velskii 和E.M.Landis 在 1962 年发明了一种解决上述问题的方法:当向二叉搜索树中插入新节点后,如果能保证每个节点的左右子树高度之差的绝对值不超过 1(超过 1 需要对树中的结点进行旋转,后续会讲,也被叫做动手术),即可降低树的高度,从而减少平均搜索长度

一颗 AVL 树或者是空树,或者是具有以下性质的二叉搜索树:

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

下面是一个AVL树加上平衡因子(balance factor)的图,其中比节点大的放在右边,比节点小的放在左边,平衡因子的计算是通过右子树的高度-左子树的高度:

image-20231114110454922

小Tips:如果一颗二叉搜索树是高度平均的,它就是 AVL 树。如果它有 n 个结点,其高度可保持在 O(log2^n),搜索时间复杂度为 O(log2^n)。平衡二叉搜索树中平衡并不是说要让平衡因子始终保持 0,这是做不到的,以两个结点为例,根节点的平衡因子只可能是 1 或者 -1,不可能是 0。二叉搜索树在实际中基本不太可能实现完全平衡,但是理论上可以,即满二叉树。后面我们学的多叉平衡搜索树在现实中可以做到完全平衡。


三、AVL节点树的定义

我们接下来就准备手撕 AVL 树了,关键点在于俩个地方:

  • 二叉搜索树的插入
  • 更新平衡因子
template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode(const pair<K, V>& kv = pair<K, V>())
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}

	pair<K, V> _kv;//存储key和value
	AVLTreeNode<K, V>* _left = nullptr;//指向左孩子
	AVLTreeNode<K, V>* _right = nullptr;//指向右孩子
	AVLTreeNode<K, V>* _parent = nullptr;//指向父亲结点
	int _bf = 0;//平衡因子
};

节点为什么置空?

在AVL树中有很多的节点需要封装,而且对于没有值的节点一定要置空,将来我们单旋的时候有一个小插曲 if(SUbL) 如果我们初始化的时候没有置空的话,这个条件的书写很明显就是不对的

为什么引入parent节点?

使AVL树变成三叉链,是为了方便将来沿着路径一直向上更新,当然有利有弊:往回找的时候方便了,修改的时候麻烦了,除了修改孩子,还得修改双亲

平衡因子的引入:

最重要的还是平衡因子的封装加入AVL树节点定义的大家庭


四、AVL树的基本框架

template<class K, class V>
	class AVLTree
	{
		typedef AVLTreeNode<K, V> Node;
	private:
		Node* _root = nullptr;
	};

我们把上面结构体定义出来的AVLTreeNode<K, V>重新命名成 Node 方便之后命名轻松

私有变量是我们的根节点 _root


五、AVL树的旋转

正常来说,应该先写插入再写旋转,但是由于插入代码中,更新平衡因子的时候需要调用旋转的接口,在不同的情况有不同的旋转方式,但是我们还不了解旋转,所以我书写的时候先书写的是旋转

首先,凡事皆有原则,我们的旋转也不例外:

  1. 子树左右高度不超过1
  2. 旋转过程中必须保持它是搜索树
  3. 更新调整孩子节点的平衡因子
  4. 降低这颗子树的高度

这个原则必须牢记于心,这样的话更有利于帮助我们理解后续的四种旋转

5.1左单旋(新节点插入较高右子树的右侧—右右:左单旋)

AVL左单旋

左单旋的定义:新节点插入较高右子树的右侧—右右:左单旋

动图中,我们的a,b,c分别代表==子树==,并不是节点,所以接下来我们举俩个具体的a,b,c例子,以及抽象图的总结

例一(h==0)

image-20231114185237905

例二(h==1)

image-20231114185221726

由于我们上方说到,我们的旋转都是满足我们的旋转四大原则的,杭哥在讲这个例子的时候说:b(也就是45)放在30的右边是正好满足二叉搜索树,但是我想难道60左边放25不可以吗?

image-20231114185631519

后来我想了想,发现60的左侧的数字,一定是(30,60),而不是(0,60),所以b一定放在30右侧满足旋转规则这个问题告一段落

例三(抽象图)

image-20231114183301853

等学到后期,看抽象图的困难程度就会大大降低,在本图中,我们有很多变量,都是为了和接下来的代码进行匹配

ppNode:担心旋转树为子树,从而旋转时丢失根与上方联系所创造出来的节点

parent:是旋转的函数参数,也是这个子树(或者树)旋转前的根

SubR:parent的右侧

SubRL:parent的右侧的左侧

以上如此多节点的设置,正是为了保证节点的丢失,以及三叉链中不光更新孩子,还得更新父亲的特性

代码讲解

根据上方小羊精美的图,我们不难写出以下代码:

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

这段代码是我自己在没理解旋转之前写的天花板代码,但是还有很多问题我并没有考虑:

1. 我只更新了孩子节点,也就是说我只做了向下分配

2. 我的parent节点已经丢失,所以也得向上分配(这里有个小插曲)

3. 如果这个树是局部子树怎么办?

4. AVL 树最根本的平衡因子更新没有体现

根据以上问题,我们还得继续写出代码

image-20231114183301853

1.更新双亲节点

根据抽象图,我们可以看出:b的_parent节点变成了parent也就是30,parent 的 _parent 节点变成了 SUbR ,也就是30的双亲变成了60

subRL->_parent = parent;
parent->_parent = subR;

这个时候小插曲来了:我们的b是一个子树,如果子树b为空,它还有双亲节点吗?所以我们得用if条件判断一下

但是为什么parent不用判断是否为空? 实则在抽象图中就可以明白,为什么赋予这俩个变量30和60,就是说明parent和SubR不可为空,否则旋转问题无法继续讨论

if (subRL!=nullptr)
{
   subRL->_parent = parent;
}
parent->_parent = subR;
2.处理局部子树问题

由于我们的parent节点在旋转的过程中已经不是该树的根了,所以一开始初始化的时候,记得记录30的双亲节点

void RotateL(Node* parent)
{
    Node* subR = parent->_right;
	Node* subRL = subR->_left;
    Node* ppNode=parent->_parent;//这里
    parent->_right=subRL;
    subR->_left=parent;
    
    //双亲
    if (subRL!=nullptr)
    {
      subRL->_parent = parent;
    }
    parent->_parent = subR;
}

这里分俩种情况:

  1. 是独立子树,那么就更新_root节点,并且将其 _parent 节点置空
  2. 如果是局部子树,若为ppNode左子树,就让其left连接SubR;若为ppNode右子树,就让其right连接SubR
    //判断是否为局部子树 需要在parent被修改之前 写出ppNode
    if(ppNode==nullptr)
    {
        //说明是完整的树
        _root=subR;
        _root->_parent=nullptr;
    }
    else
    {
        if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 哈哈 当年的自己提出这个问题 现在的我自己解决了
        {
            ppNode->_left=subR;
        }
        else
        {
            ppNode->_right=subR;
        }
        //这里还有一句话
    }

这个时候,else语句中还是只有向下分配,所以别忘了加上subR->_parent = ppNode;

3.更新平衡因子

30和60的平衡因子全部为0,也就是parent和SubR的_bf为0

parent->_bf = subR->_bf = 0;
4.代码汇总
void RotateL(Node* parent)
{
    //俩个孩子 三个双亲 好好品这句话
    //1.更新孩子节点(向下分配)
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* ppNode=parent->_parent;//为了避免是局部子树

    parent->_right=subRL;
    subR->_left=parent;
    //2.更新双亲节点(往回分配)
    if(subRL!=nullptr)//往回分配的小插曲 空节点没有双亲节点
    {
        subRL->_parent=parent;
    }
    parent->_parent=subR;

    //3.判断是否为局部子树 需要在parent被修改之前 写出ppNode
    if(ppNode==nullptr)
    {
        //说明是完整的树
        _root=subR;
        _root->_parent=nullptr;
    }
    else
    {
        if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 
        {
            ppNode->_left=subR;
        }
        else
        {
            ppNode->_right=subR;
        }
        subR->_parent = ppNode;
    }
     //4.更新平衡因子
    parent->_bf=subR->_bf=0;//更新平衡因子
}

代码总结(俩孩子三双亲)

我们在写AVL旋转的时候,一定要格局打开:我们在旋转树内的节点的时候,殊不知根节点已经岌岌可危,所以这就是局部子树的问题,而且向下分配完了别忘了向上分配,三叉链使得往上找轻松了,但也使得旋转时维护树的成本变高了

AVL的代码逻辑可以理解成俩个孩子三个双亲:因为我们的局部子树其实也是双亲问题

a1a711a80ea763653c472cacf65a0cd

同时别忘了b子树的小插曲,而且局部子树 else 的时候也容易忘记向上分配,同时最后也别忘了平衡因子的更新

5.2左单旋(新节点插入较高左子树的左侧—左左:右单旋)

AVL右单旋

例一(h==0)

image-20231115113539536

例二(h==1)

image-20231115114533515

例三(抽象图)

image-20231115115746398

代码总结(代码解释见左单旋)

 void RotateR(Node* parent)
    {
        //俩个孩子 三个双亲 好好品这句话
        //1.更新孩子节点(向下分配)
        Node* subL=parent->_left;
        Node* subLR=subL->_right;
        Node* ppNode=parent->_parent;//为了避免是局部子树
        parent->_left=subLR;
        subL->_right=parent;
        //2.更新双亲节点(往回分配)
        if(subLR!=nullptr)//往回分配的小插曲 空节点没有双亲节点
        {
            subLR->_parent=parent;
        }
        parent->_parent=subL;
        //3.判断是否为局部子树 需要在parent被修改之前 写出ppNode
        if(ppNode==nullptr)
        {
            //说明是完整的树
            _root=subL;
            _root->_parent=nullptr;
        }
        else
        {
            if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 哈哈 当年的自己提出这个问题 现在的我自己解决了
            {
                ppNode->_left=subL;
            }
            else
            {
                ppNode->_right=subL;
            }
            subL->_parent = ppNode;//局部子树else总会忘记向上分配
        }
        //4.更新平衡因子
        parent->_bf=subL->_bf=0;//更新平衡因子
    }

5.3左右双旋(新节点插入较高左子树的右侧—左右:先左单旋再右单旋)

左右双旋

先说结论:我们需要对parent->left进行左单旋 再对parent进行右单旋即可

为什么我们需要引入左右单旋这个场景呢?我们的普通单旋为什么解决不了问题了,请看以下这些情况

image-20231116201847483

例一(h==0)

先把弯的缕直,再进行单旋

image-20231116202703939

例二(h==1)

image-20231116203401501

例三(抽象图)

image-20231116205231268

代码讲解

此时我们可以基本写出以下代码:

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

        RotateL(parent->_left);
        RotateR(parent);
    }

但是非常遗憾的是:我们的抽象图看似汇集了所有情况,实则只有60节点左侧插入的情况,但是左右双旋的平衡因子更新本质上是有三种情况的:

  1. 60的左节点插入
  2. 60的右节点插入
  3. 60本身就是插入节点

我们将这个状态的SubLR的平衡因子记录下来为bf 因为旋转完之后,我们很难考察是哪个位置插入了节点,所以在旋转之前记录bf

image-20231116210058045

image-20231116211147973

从以上情况分析平衡因子就不再吃力了,完整代码如下:

void RotateLR(Node* parent)
    {
        Node* subL=parent->_left;
        Node* subLR=subL->_right;
        int bf=subLR->_bf;//为了判读平衡因子改变的三种情况

        RotateL(parent->_left);
        RotateR(parent);

        if(bf==-1)//subLR左子树插入
        {
            subL->_bf=0;
            subLR->_bf=0;
            parent->_bf=1;
        }
        if(bf==1)//subLR右子树插入
        {
            subL->_bf=-1;
            subLR->_bf=0;
            parent->_bf=0;
        }
        if(bf==0)//本身就是新节点被插入
        {
            subL->_bf=0;
            subLR->_bf=0;
            parent->_bf=0;
        }
    }

总结:分析平衡因子的时候,应该同时对照着插入前和插入俩次旋转完成后的图像去判断平衡因子的大小

5.4右左双旋(新节点插入较高右子树的左侧—右左:先右单旋再左单旋)

先对parent的right进行右单旋,再对parent进行左单旋即可

在这个例子中,我们只绘出抽象图,以方便对照着写出代码,不在举出详细例子

抽象图

image-20231117105406690

接着,我们需要讨论出旋转之前的以及插入到60的左,60的右,包括60是新节点的三种情况的平衡因子,这里不再赘述,类似左右双旋

void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);

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

六、AVL树的插入

AVL树插入结点时有以下三个步骤:

  1. 按照二叉搜索树的插入方法,找到待插入位置。
  2. 找到待插入位置后,将待插入结点插入到树中。
  3. 更新平衡因子,如果出现不平衡,则需要进行旋转。

因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:

  1. 待插入结点的key值比当前结点小就插入到该结点的左子树。
  2. 待插入结点的key值比当前结点大就插入到该结点的右子树。
  3. 待插入结点的key值与当前结点的key值相等就插入失败。

如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。

bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
}

以上这段代码只是单纯的插入代码,如果我们不更新平衡因子的话,那么AVL存在的意义也就没有了,所以我们得更新平衡因子

与二叉搜索树插入结点不同的是,AVL树插入结点后需要更新树中结点的平衡因子,因为插入新结点后可能会影响树中某些结点的平衡因子。

由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。

image-20231117110442106

所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:

  1. 新增结点在parent的右边,parent的平衡因子 ++
  2. 新增结点在parent的左边,parent的平衡因子 --。

每更新完一个结点的平衡因子后,都需要进行以下判断:(注:这里的parent不是最上面那个节点新增节点的祖先是parent

  • 如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
  • 如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
  • 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。(得动手术)

判断理由说明:

parent更新后的平衡因子分析
-1或1只有0经过 ++或-- 操作后会变成-1/1,说明新结点的插入使得parent的左子树或右子树增高了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子
0(恰好插入较矮的那边)只有-1/1经过 ++或-- 操作后会变成0,说明新结点插入到了parent左右子树当中高度较矮的一棵子树,插入后使得parent左右子树的高度相等了,此操作并没有改变以parent为根结点的子树的高度,从而不会影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。
-2或2此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。
注意: parent的平衡因子在插入更新之前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。

而在最坏情况下,我们更新平衡因子时会一路更新到根结点。例如下面这种情况:(所以我们的代码得写成while(parent)

三叉链结构的优势体现: 由于我们插入结点后需要倒着往上进行平衡因子的更新,所以我们将AVL树结点的结构设置为了三叉链结构,这样我们就可以通过父指针找到其父结点,进而对其平衡因子进行更新。当然,我们也可以不用三叉链结构,可以在插入结点时将路径上的结点存储到一个栈当中,当我们更新平衡因子时也可以通过这个栈来更新祖先结点的平衡因子,但是相对较麻烦。

若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,这时我们需要对以该结点为根结点的树进行旋转处理,而旋转处理分为四种,在进行分类之前我们首先需要进行以下分析:

我们将插入结点称为cur,将其父结点称为parent,那么我们更新平衡因子时第一个更新的就是parent结点的平衡因子,更新完parent结点的平衡因子后,若是需要继续往上进行平衡因子的更新,那么我们必定要执行以下逻辑:

cur = parent;
parent = parent->_parent;

这里我想说明的是:当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

理由如下:

若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。

那么我们按照cur是新增节点来进行推导,就会发现有bug:

如果cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:

  1. 其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
  2. 其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。

综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

根据此结论,我们可以将旋转处理分为以下四类:

  1. 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。(左左)
  2. 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。(左子树右侧)
  3. 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。(右子树左侧)
  4. 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。(右右)

并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了

加上更新平衡因子后的插入完整代码:

	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		// 1、更新平衡因子
		while (parent) // parent为空,也就更新到根
		{
			// 新增在右,parent->bf++;
			// 新增在左,parent->bf--;
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			// 是否继续更新依据:子树的高度是否变化
			// 1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1
			// 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
			// 2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,
			// parent所在子树高度变了,继续往上更新
			// 3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则
			// 就地处理--旋转

			// 旋转:
			// 1、让这颗子树左右高度不超过1
			// 2、旋转过程中继续保持他是搜索树
			// 3、更新调整孩子节点的平衡因子
			// 4、让这颗子树的高度跟插入前保持一致
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 旋转
				if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(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;
			}
			else
			{
				assert(false);
			}
		}

		return true;
	}

七、AVL树相关的一些测试

AVL 树是在二叉搜索树的基础上加入了平衡的限制,因此要验证 AVL 树,可以分为两步:

  • 验证其为二叉搜索树。如果中序遍历可以得到一个有序的序列,就说明为二叉搜索树。
  • 验证其为平衡树。每个结点左右子树高度差的绝对值不超过 1 。其次检查结点的平衡因子是否计算正确。
public:
	//中序
	void Inorder()
	{
		return _Inorder(_root);
	}
	//检测平衡因子
	bool IsBlance()
	{
		return _IsBalance(_root);
	}
private:
	//中序
	void _Inorder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
	
		_Inorder(root->_left);
		cout << root->_kv.first << ' ';
		_Inorder(root->_right);
	
		return;
	}
	
	
	//求树的高度
	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;
	}
	
	//检测平衡因子
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		if (rightHeight - leftHeight != root->_bf)
		{
			cout << "平衡因子异常:" << root->_bf << endl;
			return false;
		}

		return abs(leftHeight - rightHeight) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

/*test.c*/
int main()
{
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	wcy::AVLTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
		t.Inorder();
		cout << endl;
		if (t.IsBlance())
		{
			cout << "成功插入:" << e << endl;
		}
		else
		{
			cout << e << "插入失败" << endl;
		}
	}
	return 0;
}

image-20231117113209822

八、完整代码

AVL.h

#include <assert.h>
#include <time.h>
#include<algorithm>
#include<iostream>
using namespace std;
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;  // balance factor

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

template<class K, class V>
struct AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:

/*----------------------------------------------插入------------------------------------------------*/
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		// 1、更新平衡因子
		while (parent) // parent为空,也就更新到根
		{
			// 新增在右,parent->bf++;
			// 新增在左,parent->bf--;
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			// 是否继续更新依据:子树的高度是否变化
			// 1、parent->_bf == 0说明之前parent->_bf是 1 或者 -1
			// 说明之前parent一边高一边低,这次插入填上矮的那边,parent所在子树高度不变,不需要继续往上更新
			// 2、parent->_bf == 1 或 -1 说明之前是parent->_bf == 0,两边一样高,现在插入一边更高了,
			// parent所在子树高度变了,继续往上更新
			// 3、parent->_bf == 2 或 -2,说明之前parent->_bf == 1 或者 -1,现在插入严重不平衡,违反规则
			// 就地处理--旋转

			// 旋转:
			// 1、让这颗子树左右高度不超过1
			// 2、旋转过程中继续保持他是搜索树
			// 3、更新调整孩子节点的平衡因子
			// 4、让这颗子树的高度跟插入前保持一致
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 旋转
				if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(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;
			}
			else
			{
				assert(false);
			}
		}

		return true;
	}
/*----------------------------------------------旋转------------------------------------------------*/
    void RotateL(Node* parent)
    {
    //俩个孩子 三个双亲 好好品这句话
    //1.更新孩子节点(向下分配)
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* ppNode=parent->_parent;//为了避免是局部子树

    parent->_right=subRL;
    subR->_left=parent;
    //2.更新双亲节点(往回分配)
    if(subRL!=nullptr)//往回分配的小插曲 空节点没有双亲节点
    {
        subRL->_parent=parent;
    }
    parent->_parent=subR;

    //3.判断是否为局部子树 需要在parent被修改之前 写出ppNode
    if(ppNode==nullptr)
    {
        //说明是完整的树
        _root=subR;
        _root->_parent=nullptr;
    }
    else
    {
        if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 
        {
            ppNode->_left=subR;
        }
        else
        {
            ppNode->_right=subR;
        }
        
        subR->_parent = ppNode;
    }
     //4.更新平衡因子
    parent->_bf=subR->_bf=0;//更新平衡因子
    }
    void RotateR(Node* parent)
    {
        //俩个孩子 三个双亲 好好品这句话
        //1.更新孩子节点(向下分配)
        Node* subL=parent->_left;
        Node* subLR=subL->_right;
        Node* ppNode=parent->_parent;//为了避免是局部子树
        parent->_left=subLR;
        subL->_right=parent;
        //2.更新双亲节点(往回分配)
        if(subLR!=nullptr)//往回分配的小插曲 空节点没有双亲节点
        {
            subLR->_parent=parent;
        }
        parent->_parent=subL;
        //3.判断是否为局部子树 需要在parent被修改之前 写出ppNode
        if(ppNode==nullptr)
        {
            //说明是完整的树
            _root=subL;
            _root->_parent=nullptr;
        }
        else
        {
            if(ppNode->_left==parent)//虽然parent被旋转了 但是上层的ppNode还是连着parent节点 哈哈 当年的自己提出这个问题 现在的我自己解决了
            {
                ppNode->_left=subL;
            }
            else
            {
                ppNode->_right=subL;
            }
            subL->_parent = ppNode;
        }
        //4.更新平衡因子
        parent->_bf=subL->_bf=0;//更新平衡因子
    }
    void RotateLR(Node* parent)
    {
        Node* subL=parent->_left;
        Node* subLR=subL->_right;
        int bf=subLR->_bf;//为了判读平衡因子改变的三种情况

        RotateL(parent->_left);
        RotateR(parent);

        if(bf==-1)//subLR左子树插入
        {
            subL->_bf=0;
            subLR->_bf=0;
            parent->_bf=1;
        }
        if(bf==1)//subLR右子树插入
        {
            subL->_bf=-1;
            subLR->_bf=0;
            parent->_bf=0;
        }
        if(bf==0)//本身就是新节点被插入
        {
            subL->_bf=0;
            subLR->_bf=0;
            parent->_bf=0;
        }
    }

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);

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

	void Inorder()
	{
		_Inorder(_root);
	}

	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_Inorder(root->_right);
	}

	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		
		int lh = Height(root->_left);
		int rh = Height(root->_right);

		return lh > rh ? lh + 1 : rh + 1;
	}
	
	bool IsBalance()
	{
		return IsBalance(_root);
	}

	bool IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);

		if (rightHeight - leftHeight != root->_bf)
		{
			cout <<root->_kv.first<<"平衡因子异常" << endl;
			return false;
		}

		return abs(rightHeight - leftHeight) < 2
			&& IsBalance(root->_left)
			&& IsBalance(root->_right);
	}

private:
	Node* _root = nullptr;
};

//void TestAVLTree()
//{
//	//int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
//	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
//	//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
//	AVLTree<int, int> t;
//	for (auto e : a)
//	{
//		t.Insert(make_pair(e, e));
//	}
//
//	t.Inorder();
//
//	cout << t.IsBalance() << endl;
//}

void TestAVLTree()
{
	srand(time(0));
	const size_t N = 10000;
	AVLTree<int, int> t;
	for (size_t i = 0; i < N; ++i)
	{
		size_t x = rand();
		t.Insert(make_pair(x, x));
		//cout << t.IsBalance() << endl;
	}

	//t.Inorder();

	cout << t.IsBalance() << endl;
}

AVL.cpp

#include"AVL.h"
#include<iostream>
#include<algorithm>
using namespace std;
/*test.c*/
int main()
{
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
		t.Inorder();
		cout << endl;
        if(t.IsBalance())
		{
			cout << "成功插入:" << e << endl;
		}
		else
		{
			cout << e << "插入失败" << endl;
		}
	}
	return 0;
}



九、难点总结

旋转的四个原则

  1. 子树左右高度不超过1
  2. 旋转过程中必须保持它是搜索树
  3. 更新调整孩子节点的平衡因子
  4. 降低这颗子树的高度

在进行旋转处理后就无需继续往上更新平衡因子了

因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了

旋转口诀是插入较高… 不要把这个较高给忽略掉

写代码的时候,头脑要清楚,不要忘了回溯parent的值(向上分配)

插入代码更新平衡因子的条件是while(parent)

对于单旋,利用俩孩子三双亲原则;对于双旋,难点在于平衡因子更新,得画出插入前和旋转后的图分析平衡因子(三种情况)

双旋得提前记录subLR或者subRL的bf(旋转完了都找不到了)

单旋得提前记录parent的_parent,也就是代码中的ppNode

单旋的时候,解决局部子树问题时,总会忘记向上分配

希望给大家带来帮助!!

happy猫

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/156442.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C/C++算法-----------------------双指针详解技巧及例题

双指针 基本介绍降低时间复杂度降低时间复杂度例题 验证回文串判断是否为环反转链表总结 基本介绍 双指针&#xff08;two poinnters&#xff09;实际上是一种算法编程里的一种思想&#xff0c;它更像是一种编程思想&#xff0c;提供看非常高的算法效率&#xff0c;一般来说双…

FL Studio水果软件2025中文破解版注册机

你是否体验过Tomorrowland现场万人蹦迪的的激情&#xff1f;又是否加入过“死墙&#xff08;Mosh pit&#xff1a;一种Bass音乐节常有的娱乐方式&#xff09;”的狂欢盛宴&#xff1f;随着时代发展&#xff0c;以电子音乐为代表的数字音乐已然象征着时尚与潮流。在这股风靡全球…

fiddler展示接口的响应时间

最近项目组迁移了一个新项目&#xff0c;想对比迁移前后访问菜单的响应时间是否有变化&#xff0c;因为没需求文档&#xff0c;所以只有靠fiddler一个个的抓接口来看&#xff0c;开发经理想要看具体每个接口耗时&#xff0c;虽然点击接口&#xff0c;在页面上也能看到接口响应时…

HackTheBox-Starting Point--Tier 2---Markup

文章目录 一 Markup测试过程1.1 打点1.2 权限获取1.3 权限升级 二 题目 一 Markup测试过程 1.1 打点 1.端口扫描 nmap -A -Pn -sC 10.129.95.1922.访问web网站&#xff0c;登录口爆破发现存在弱口令admin&#xff1a;password 3.抓包&#xff0c;发现请求体是XML格式 4.尝试使…

购买阿里云服务器需要多少钱?活动价3000元-5000元的阿里云服务器汇总

购买阿里云服务器需要多少钱&#xff1f;如果我们只有3000元-5000元的预算可以购买什么实例规格和配置的阿里云服务器呢&#xff1f;因为阿里云服务器价格是由实例规格、配置、带宽等众多配置决定的&#xff0c;所以&#xff0c;目前阿里云活动中的价格在3000元-5000元的云服务…

购买阿里云服务器需要多少钱?活动价2000元-3000元的阿里云服务器汇总

购买阿里云服务器需要多少钱&#xff1f;如果我们只有2000元-3000元的预算可以购买什么实例规格和配置的阿里云服务器呢&#xff1f;因为阿里云服务器价格是由实例规格、配置、带宽等众多配置决定的&#xff0c;所以&#xff0c;目前阿里云活动中的价格在2000元-3000元的云服务…

一文揭秘共享wifi二维码项目推广技巧!

随着无线网络的普及和移动互联网的快速发展&#xff0c;共享WiFi已成为人们生活中不可或缺的一部分。共享WiFi二维码项目作为一个独具创意的共享项目&#xff0c;将二维码推广与共享WiFi相结合&#xff0c;不仅可以提升品牌曝光度&#xff0c;还能为用户提供便捷的上网体验。那…

什么是圆锥的准线?

定曲线C叫做锥面的准线&#xff0c;构成曲面的每一条直线叫做母线。

RedHat公司及红帽认证介绍丨红帽认证等级介绍

RedHat公司及红帽认证介绍 红帽公司成立工1993年&#xff0c;是全球首家收入超10亿美元的开源公司&#xff0c;总部位于美国&#xff0c;分支机构遍布全球。红帽公司作头全球领先的开源和Linux系统提供商&#xff0c;其产品已被业界广泛认可并使用&#xff0c;尤其是RHEL系统在…

CentOs 7 PHP安装和配置

目录 1 安装epel源 2 安装REMI源 3 安装yum源管理工具 4 安装PHP7.3 5 启动php服务 6 设置PHP 6.1 查找安装包 6.2 查找PHP安装位置 6.3 查找php配置文件位置 6.4 配置PHP 6.5 设置快捷命令 6.6 查看php版本 6.7 更新php 1 安装epel源 yum -y install epel-release 2 安…

数据仓库基础

ODS&#xff08;Operational Data Store&#xff09;层&#xff1a;操作数据存储。存放原始数据&#xff0c;直接加载原始日志、数据&#xff0c;起到备份数据的作用。数据采用压缩&#xff0c;减少磁盘存储空间。创建分区表&#xff0c;防止后续的全表扫描。 DWD(data wareho…

MatrixOne 支持多样化生态工具

近日&#xff0c;云原生数据库 MatrixOne 支持多样化生态工具&#xff0c;包括&#xff1a;数据集成工具、BI 工具和数据计算引擎这三类生态工具。 云原生数据库使得传统数据库得以充分结合云服务的免运维、高弹性、高可扩展、高可用、高性价比优势&#xff0c;又顺应了云端应…

IDEA远程一键部署SpringBoot到Docker

IDEA是Java开发利器&#xff0c;Spring Boot是Java生态中最流行的微服务框架&#xff0c;docker是时下最火的容器技术&#xff0c;那么它们结合在一起会产生什么化学反应呢&#xff1f; 一、开发前准备 1. Docker安装 可以参考&#xff1a;https://docs.docker.com/install/ 2…

创作者等级终于升到4级了

写了两个月的文章&#xff0c;终于等到4级了。发文纪念一下&#xff1a;

蔚来面试题

文章目录 1.解释脏读/不可重复读/幻读不可重复读和幻读的区别2.索引失效的场景有哪些?3.Explain执行计划用过吗?4.Type字段有什么信息?5.binlog和redolog区别?6.Redis基本数据类型7.有序集合底层实现数据结构?8.跳表插入数据的过程?为什么要这样设计呢?添加流程9.线程池…

关于圆通物流在AppLink上的操作

在使用物流系统时&#xff0c;我们会出现订单变化而导致物流轨迹发生改动&#xff0c;如果反馈不及时会造成额外的工作量以及会出现人为的操作失误&#xff0c;我们尝试借助AppLink来实现圆通物流在发生变化时的信息同步 登录开放平台 复制右侧登录地址登录圆通速递管理后台&…

python数据处理作业10:将图中数据创建一个DataFrame,保存为csv格式,读取该文件,删除体育成绩,并添加语文,数学和英语的平均成绩

每日小语 心无旁骛&#xff0c;专心于事业的追求&#xff0c;就会忘掉许多烦恼&#xff0c;找到许多努力过程中的快乐。——黑格尔 gpt import pandas as pd# 创建DataFrame并保存为CSV文件 data {姓名: [张三, 李四, 王五, 赵六],语文成绩: [80, 90, 85, 75],数学成绩: [7…

<Linux>(极简关键、省时省力)《Linux操作系统原理分析之Linux 进程管理 1》(5)

《Linux操作系统原理分析之Linux 进程管理 1》&#xff08;5&#xff09; 4 Linux 进程管理4.1 Linux 进程概述4.1.1 Linux 进程的组成4.1.2 Linux 进程在处理机上的执行状态4.1.3 进程空间和系统空间4.1.4 进程上下文和系统上下文 4 Linux 进程管理 4.1 Linux 进程概述 4.1.…

恭喜顺利结项 | 开源之夏 2023openGauss项目结项结果公示

祝贺&#xff01; 激动人心的时刻终于到来&#xff01;开源之夏 2023 活动结项审核结果正式出炉。此次&#xff0c;openGauss深度参与活动&#xff0c;发布12个openGauss相关项目&#xff0c;10个项目进入开发周期&#xff0c;最终有8个项目成功结项。恭喜所有成功结项的同学&…

三菱FX3U小项目—机床定时器延时启动

目录 一、项目描述 二、IO口分配 三、项目程序 四、总结 一、项目描述 为了防止工人操作失误&#xff0c;启动按钮需要按住1s后&#xff0c;设备才启动&#xff0c;启动后第一台电机启动10s后第二台电机自动启动&#xff0c;当按下停止按钮时&#xff0c;两台电机同时停止。…