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

news2024/11/27 2:42:06

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猫

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

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

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

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.尝试使…

金蝶云星空签出元数据提示“数据中心业务对象版本高于应用版本”

文章目录 数据中心业务对象版本高于应用版本签出元数据提示建议 数据中心业务对象版本高于应用版本 签出元数据提示 建议 每次签出元数据前&#xff0c;先获取最新的代码后再签出&#xff0c;如果还是提示&#xff0c;那就根据你的情况选择版本。

leetcode算法之位运算

目录 1.判断字符是否唯一2.丢失的数字3.两整数之和4.只出现一次的数字II5.消失的两个数字6.位1的个数7.比特位计数8.汉明距离 1.判断字符是否唯一 判断字符是否唯一 class Solution { public:bool isUnique(string astr) {//利用鸽巢原理做优化if(astr.size()>26) return…

置信区间的介绍和使用

一、理论 计算置信区间的具体方法通常取决于所选择的统计分布以及所使用的估计方法。在正态分布下&#xff0c;以样本均值为中心的置信区间的计算是比较常见的&#xff0c;尤其是当样本大小足够大时&#xff0c;根据中心极限定理&#xff0c;样本均值的分布接近正态分布。 以…

提升提测质量之研测共建 | 京东云技术团队

一、序 日常研测工作演绎 你是否也有同样的困惑&#xff1f; 跟进的需求&#xff0c;就在提测前一秒&#xff0c;被告知不能如期提测了&#xff0c;研测计划被打乱&#xff1b; 提测的功能&#xff0c;犹如遇到不好的购物体验&#xff0c;缺斤短两&#xff0c;与prd预期不符…

什么是圆锥的准线?

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

来文心中国行厦门站,感受大模型落地生花的进展!

11月22日&#xff0c;文心中国行将走进厦门。届时&#xff0c;政府、高校及企业的相关专家将现场分享AI和大模型最新进展&#xff0c;从人工智能政策解读&#xff0c;到大模型底层技术&#xff0c;再到产教融合下的空间感知与计算&#xff0c;产业创新应用洞察及实践案例等等&a…

uniapp form表单提交事件手动调用

背景&#xff1a; UI把提交的按钮弄成了图片&#xff0c;之前的button不能用了。 <button form-type"submit">搜索</button> 实现&#xff1a; html&#xff1a; 通过 this.$refs.fd 获取到form的vue对象。手动调用里面的_onSubmit()方法。 methods:…

安全函数使用及源码

几个常规内存函数经常会使用的怀疑人生&#xff0c;现在整理一下 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 以下对内存函数进行整理。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、资源 MSDN网址 …

智慧能源太阳能光伏数据采集终端钡铼技术4G无线RTU

智慧能源太阳能光伏系统在当今的能源行业中扮演着越来越重要的角色&#xff0c;而钡铼技术有限公司的4G无线RTU&#xff08;远程终端单元&#xff09;作为数据采集终端&#xff0c;为智慧能源太阳能光伏系统的监测和管理提供了全新的解决方案。 首先&#xff0c;钡铼技术的4G无…

新的 Reptar CPU 缺陷影响英特尔台式机和服务器系统

英特尔修复了其现代台式机、服务器、移动和嵌入式 CPU 中的一个高严重性 CPU 漏洞&#xff0c;包括最新的 Alder Lake、Raptor Lake 和 Sapphire Rapids 微架构。 攻击者可以利用该缺陷&#xff08;追踪为CVE-2023-23583并被描述为“冗余前缀问题”&#xff09;来升级权限、获…

宝藏!宝藏!在线就能制作产品图册

哇塞&#xff01;太棒了&#xff01;你听说过吗&#xff1f;现在只要在线就能制作产品图册&#xff0c;简直是个宝藏&#xff01; 制作产品图册对于很多公司和企业来说&#xff0c;可是个不小的挑战。不仅要花费大量时间和精力&#xff0c;还要投入不少金钱&#xff0c;真是让…

「Verilog学习笔记」用3-8译码器实现全减器

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 分析 首先列出3-8译码器和全减器的真值表 全减器真值表如下 3-8译码器真值表如下 timescale 1ns/1nsmodule decoder_38(input E ,input A0 …

总结 CNN 模型:将焦点转移到基于注意力的架构

一、说明 在计算机视觉时代&#xff0c;卷积神经网络&#xff08;CNN&#xff09;几十年来一直是主导范式。直到 2021 年 Vision Transformers (ViTs) 出现&#xff0c;这个领域才开始发生变化。现在&#xff0c;是时候采用受 Transformer 架构启发的基于注意力的模型了&#x…

巾帼调查队开展实务调查技能,促全职妈妈联增收

2024年11月14日上午&#xff0c;由罗湖区妇联主办、罗湖区懿米阳光公益发展中心承办的“巾帼调查队—社区女性增值计划”项目第三期活动在罗湖区妇儿大厦六楼成功举办&#xff0c;30名阳光妈妈及全职妈妈参与了此次调查实务技巧培训。 在培训开始之前&#xff0c;巾帼调查队的创…

OSPF开放最短路径优先(Open Shortest Path First)协议

OSPF开放最短路径优先(Open Shortest Path First)协议 为克服RIP的缺点(限制网络规模&#xff0c;坏消息传得慢)在1989年开发出来的原理很简单&#xff0c;但实现很复杂使用了Dijkstra提出的最短路径算法SPF(Shortest Path First)采用分布式的链路状态协议(link state protoco…

Python字符串类型

目录 目标 版本 官方文档 书写格式 字符串合并 常用函数 字母转小写&#xff08;首字母转大写&#xff09; 字母转小写&#xff08;适用于在国际化环境中&#xff0c;忽略字母大小写进行比较的场景&#xff09; 字母转小写&#xff08;适用于非国际化环境中&#xff0…

在计算机领域如神一般存在的人都有哪些?

✍️作者简介&#xff1a;沫小北/码农小北&#xff08;专注于Android、Web、TCP/IP等技术方向&#xff09; &#x1f433;博客主页&#xff1a;沫小北/码农小北 开源中国、稀土掘金、51cto博客、博客园、知乎、简书、慕课网、CSDN &#x1f514;如果文章对您有一定的帮助请&…

IDEA远程一键部署SpringBoot到Docker

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