AVL 树(平衡搜索二叉树) - 介绍及模拟实现

目录

AVL树的背景与概念

1.背景

2.AVL树的定义

AVL 树结点定义

1.键值对(Key - Value Pair)

2.AVL 树结点定义

AVL树插入操作Insert — 非递归(循环)版本

步骤 1:完成搜索二叉树新增结点的插入

1.插入情况分析

2.代码实现

步骤 2:插入新增结点后,更新祖先结点的平衡因子

一、思路分析

1.衡量树平衡状态的方法

3.插入新增结点后,更新祖先结点平衡因子的具体思路

二、代码实现

步骤 3:通过旋转解决 (parent) 祖先所在子树平衡被破坏问题

一、AVL 树旋转的基本概念

二、说明(重点)

三、单旋情况分析

情况1:新增结点插入到AVL树较高右子树的右侧,即在c树插入 — 右右:左单旋

1.左单旋介绍

2.新增结点插入 AVL 树较高右子树的右侧(即在 c 树 插入)时,c树不同高度(h = 0、1、2)触发左单旋的机制分析

3.代码实现

3.1.代码实现中的问题及解决

3.2.代码实现

情况2:新增结点插入到AVL树较高左子树的左侧,即在a树插入 — 左左:右单旋

1.右单旋介绍

2.新增结点插入 AVL 树较高左子树的左侧(即在 a 树 插入)时,a树不同高度(h = 0、1、2)触发右单旋的机制分析

3.代码实现

四、双旋情况分析

情况3:新增结点插入到AVL树较高左子树的右侧,即在b 或 c树插入 — 左右:左右双旋(先左单旋再右单旋)

1.左右双旋介绍

2.新增结点插入到AVL树较高左子树的右侧(即在b 或 c树插入)时,b 或 c 树不同高度(h = 0、1、2)触发左右双旋的机制分析

3.代码实现

(1)双旋操作后平衡因子的调节

(2)左右双旋代码实现

情况4:新增结点插入到AVL树较高右子树的左侧,即在b 或 c树插入 — 右左:右左双旋(先右单旋再左单旋)

1.右左双旋介绍

2.新增结点插入到AVL树较高右子树的左侧(即在b 或 c树插入)时,b 或 c 树不同高度(h = 0、1、2)触发右左双旋的机制分析

3.代码实现

(1)双旋操作后平衡因子的调节

(2)右左双旋代码实现

五、AVL树四种旋转类型总结

1.不同形状的失衡子树对应不同类型的旋转操作。

2.AVL树四种旋转类型介绍

六、AVL 树失衡时四种旋转操作的调用代码

(1)AVL 树失衡时四种旋转操作的调用逻辑与判断依据

(2)四种旋转操作的调用代码 — Insert的实现

AVL树Insert接口测试

一、AVL树中序遍历

二、AVL树的验证 IsBalance - 验证一棵二叉树是否是AVL树

1.AVL 树定义及与平衡树关系

2.判断二叉树是否是 AVL 树的常见错误写法及问题剖析

3.AVL树验证 IsBalance 的实现

三、AVL 树 Insert 接口测试

1.Insert 功能测试

2.Insert 性能测试

AVL树 查找Find

AVL树删除操作Erase — 非递归(循环)版本

一、AVL 树删除操作核心概念

二、二叉搜索树删除的基本情况

三、AVL 树删除操作的实现思路

四、AVL 树删除操作代码实现过程中的问题及解决方法

1.问题一:除结点后父结点及祖先结点平衡因子更新问题

2.问题二:AVL 树删除操作中特殊单旋触发情况详解

3.问题三:AVL 树删除操作中旋转对树高度的影响及判断机制

五、AVL树删除操作Erase 代码实现

AVL树性能

AVL树模拟实现的整个工程代码


AVL树的背景与概念

1.背景

在数据结构中,map、multimap、set、multiset 等关联式容器的底层实现基础是二叉搜索树。二叉搜索树具有这样的特性:对于树中的任意结点,其左子树的所有结点值小于该结点值,右子树的所有结点值大于该结点值。然而,当向二叉搜索树中插入的元素呈现有序或接近有序的状态时,二叉搜索树会逐渐退化为单支树。例如,依次插入 1、2、3、4…… 这样有序的元素,树的形态会像一条链表,此时查找操作的时间复杂度会从理想的 O(logn) 退化到 O(n) ,这极大地降低了查找效率。为了解决这个问题,需要对二叉搜索树进行平衡处理,AVL 树就是一种被广泛应用的平衡二叉搜索树。

2.AVL树的定义

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

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

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

  • 例如在一个 AVL 树结构中,各结点的平衡因子会标注在结点旁,像图中展示的树结构,结点的平衡因子符合要求。
  • 若二叉搜索树高度平衡,即为 AVL 树。若有 n 个结点,其高度可保持在 O(logn),搜索时间复杂度为 O(logn) 。

AVL 树结点定义

1.键值对(Key - Value Pair)

(1)定义与概念:键值对是用于表示数据一一对应关系的基础数据结构。其中 key 作为唯一标识用于定位和检索数据,具有唯一性和确定性;value 存储与 key 相关的具体信息,类型不限,如数值、字符串、对象等 。通过特定 key 能快速准确找到对应 value,在数据存储、检索和处理中应用广泛 。

(2)SGI - STL 中的实现细节

pair - C++ Reference (cplusplus.com)

在 SGI - STL(标准模板库)中,通过 pair 结构体实现键值对概念。其模板定义为 template <class T1, class T2> struct pair 。

  • 类型别名typedef T1 first_type; 为 first 成员变量的类型别名;typedef T2 second_type; 为 second 成员变量的类型别名 。
  • 成员变量T1 first; 存储键值对中的第一个元素,通常对应 key;T2 second; 存储键值对中的第二个元素,通常对应 value 。
  • 构造函数:默认构造函数 pair() : first(T1()), second(T2()) 使用默认值初始化 first 和 second ;带参数的构造函数 pair(const T1& a, const T2& b) : first(a), second(b) 使用传入参数初始化 first 和 second 。
template <class T1, class T2>
struct pair
{
	//定义 first 成员变量的类型别名
	typedef T1 first_type;
	//定义 second 成员变量的类型别名
	typedef T2 second_type;

	//存储键值对中的第一个元素,通常对应 key
	T1 first;
	//存储键值对中的第二个元素,通常对应 value
	T2 second;

	//默认构造函数,使用默认值初始化 first 和 second
	pair() 
		: first(T1()), second(T2())
	{}

	//带参数的构造函数,使用传入的参数初始化 first 和 second
	pair(const T1& a, const T2& b) 
		: first(a), second(b)
	{}
};

2.AVL 树结点定义

(1)定义说明

AVL 树的实现方式有多种,不一定依赖平衡因子_bf来实现树的平衡。然而,在实际应用中,使用平衡因子是一种较为常见且有效的方式。在向 AVL 树中插入结点时,平衡因子可以方便地用于控制左右子树的高度。通过对平衡因子的计算和判断,可以及时发现树是否失衡,并采取相应的调整措施(如旋转操作),进而平衡树的高度,防止 AVL 树退化为单支树,从而保证树的查找性能始终维持在较高水平。

从严格定义来说,一棵 AVL 树要求其所有结点的平衡因子都是 - 1、0、1,并且同时满足二叉搜索树的性质(即左子树结点值小于当前结点值,右子树结点值大于当前结点值),那么这棵树就是 AVL 树。

(2)代码实现

template<class K, class V>
struct AVLTreeNode
{
    AVLTreeNode<K, V>* _left; //指向该结点的左孩子。
    AVLTreeNode<K, V>* _right; //指向该结点的右孩子。

    //增加父结点指针可方便遍历,若无此指针,旋转实现简单但遍历时会复杂。
    //父结点指针在一些操作中,如查找结点的祖先结点、调整树结构时,能够提供便利。
    //例如在进行旋转操作后,需要更新相关结点的父结点指针,以维持树结构的正确性。
    AVLTreeNode<K, V>* _parent; //指向父亲。
    pair<K, V> _kv; //AVL树结点存储的数据类型为pair<key, value>。这使得AVL树可以
                    //存储键值对形式的数据,符合其在关联式容器底层实现中的应用需求。
    int _bf; //该结点的平衡因子
    //balance factor平衡因子,用于控制左右子树高度,值一般为右子树高度 - 左子树高度。
    //通过对平衡因子的监控和调整,能够实现AVL树的平衡。

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

注意:在实现 AVL 树时,给 AVL 树结点增设父结点指针,虽会在旋转操作时带来额外开销 ,但也有显著优势。若无父结点指针,旋转操作的代码实现相对简洁,因为无需考虑父结点指针的更新;而增设父结点指针后,在进行旋转操作时,需要细致处理相关结点父指针的指向变更,这增加了操作的复杂性和代码量 。不过,父结点指针能极大地方便树的遍历操作,比如在查找某个结点的祖先结点,或者从子结点回溯到根结点等场景下,有父结点指针会使实现过程更为便捷高效。因此,是否添加父结点指针,需要在旋转操作的复杂度和遍历等操作的便利性之间权衡抉择。 

AVL树插入操作Insert — 非递归(循环)版本

步骤 1:完成搜索二叉树新增结点的插入

1.插入情况分析

  • 情况 1:若二叉搜索树一开始为空树,直接新增(创建)结点,并将其赋值给_root根结点指针。这是因为空树没有任何结点,新插入的结点自然成为根结点。
  • 情况 2:若二叉搜索树一开始不为空树,按照二叉搜索树的性质查找插入位置,然后插入新增结点。二叉搜索树的性质是左子树的所有结点值小于根结点值,右子树的所有结点值大于根结点值。符合二叉搜索树特性的空树位置就是插入位置。例如,在一个已有的二叉搜索树中依次插入 16、0 ,会根据值的大小比较,在合适的空位置插入结点。

2.代码实现

此代码首先判断树是否为空,若为空则直接插入新结点。若树不为空,通过循环比较键值大小找到插入位置,创建新结点并将其正确连接到树中。

//插入 - 非递归版本(推荐):可以使用非递归就使用非递归,尽量少用递归版本。
//注:AVL树插入结点和搜索二叉树插入结点的思路很像。
bool Insert(const pair<K, V>& kv)
{
    //AVL树插入分为两个步骤:先完成搜索二叉树的插入 + 后完成平衡左右子树的高度
    //步骤1:搜索二叉树的插入
    //若当前AVL树是空树,则直接插入根结点并修改根结点指针_root的指向。
    if (_root == nullptr)
    {
        //注:由于根结点没有父亲,则根结点不用与自己父亲进行链接
        _root = new Node(kv);//插入根结点
        return true;//插入成功
    }
    //parent
    Node* parent = nullptr;//记录指向当前遍历结点的父结点的指针parent。
    Node* cur = _root;//定义遍历当前结点指针cur
    //查找插入位置 - 注:插入位置就是空树位置
    while (cur)
    {
        //插入值key比根结点大,则往右子树中查找插入位置进行插入操作
        if (cur->_kv.first < kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        //插入值key比根结点小,则往左子树中查找插入位置进行插入操作
        else if (cur->_kv.first > kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else//若插入值key与根结点相等,则插入失败
        {
            return false;
        }
    }
    //走到这一步说明找到插入位置(空树位置),且创建新结点值(插入值key所在结点)一定不与自己父亲结点值相等。
    //此时当前结点指针cur指向的位置就是插入位置 - 空树位置
    cur = new Node(kv);//创建新结点
    //由于插入结点不知道插入到自己父亲的左边还是右边,则必须根据搜索二叉树特性(左孩子比根小,右孩子比根大)来判断是在父亲的左边插入,还是在父亲的右边插入。
    if (parent->_kv.first > kv.first)//若插入值key比自己父亲小,则左链接(插入到左边)
    {
        parent->_left = cur;//结点左指针链接上了
    }
    else//若插入值key比自己父亲大,则右链接(插入到右边)
    {
        parent->_right = cur;//结点右指针链接上了
    }
    //走到这一步父结点已经与插入结点完成左/右链接,则此时插入结点需反向与自己的父结点进行链接
    cur->_parent = parent;//插入结点自己反向与父亲进行链接
    //步骤2:更新新增结点的祖先结点平衡因子的值
    while (parent)
    {
        //根据插入结点是父结点的左孩子还是右孩子,更新父结点的平衡因子
        if (cur == parent->_right)
        {
            parent->_bf++;
        }
        else
        {
            parent->_bf--;
        }
        //如果父结点的平衡因子为1或-1,说明子树仍然平衡,继续向上更新祖先结点的平衡因子
        if (parent->_bf == 1 || parent->_bf == -1)
        {
            //继续更新
            parent = parent->_parent;
            cur = cur->_parent;
        }
        //如果父结点的平衡因子为0,说明子树已经平衡,不需要再向上更新
        else if (parent->_bf == 0)
        {
            break;
        }
        //如果父结点的平衡因子为2或-2,说明子树失去平衡,需要进行旋转操作来恢复平衡
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            //需要旋转处理 -- 1、让这颗子树平衡 2、降低这颗子树的高度
            //这里省略了旋转操作的具体实现,实际需要根据情况调用相应的旋转函数
            break;
        }
        else
        {
            assert(false);
        }
    }
    return true;
}

步骤 2:插入新增结点后,更新祖先结点的平衡因子

一、思路分析

1.衡量树平衡状态的方法

影响一个结点平衡因子的因素是该结点左右子树的高度差,通常平衡因子的计算方式为右子树高度减去左子树高度。新增结点的平衡因子初始值为 0,因为其左右子树在插入时都是空树。

在新增结点后,通过更新平衡因子来判断树的平衡是否受到影响。若更新后平衡因子的绝对值小于等于 1,说明树的平衡未受影响,无需额外处理;若平衡因子的绝对值大于 1,则表明树的平衡被破坏,需要通过旋转操作来恢复树的平衡结构。


2.新增结点对祖先结点的影响范围

在 AVL 树中,新增结点只会对从自身到根结点路径上的祖先结点的平衡因子产生影响。这是因为搜索二叉树的特性决定了新结点的插入会改变这些祖先结点所在子树的左右子树高度差。由于并非所有祖先结点都会受到影响,所以我们需要借助父结点指针parent沿着新增结点的路径往上更新祖先结点的平衡因子,以此来判断哪些祖先结点受到了影响。父结点指针在这个过程中起到了回溯查找新增结点祖先结点的关键作用。

3.插入新增结点后,更新祖先结点平衡因子的具体思路

(1)当新增结点cur插入到父亲parent的右边时,parent的右子树高度增加 1,根据平衡因子的计算方式(右子树高度 - 左子树高度),parent的平衡因子会增加 1;当新增结点cur插入到父亲parent的左边时,parent的左子树高度增加 1,parent的平衡因子则会减少 1 。 

(2)在更新完新增结点父结点的平衡因子后,我们需要判断是否继续往上更新其余祖先结点的平衡因子,判断的依据是父结点所在子树高度是否发生变化:

①判断依据:若新增结点的父结点所在子树高度不变,这意味着该子树的高度变化没有传递到更高层,所以无需继续往上更新祖先平衡因子;若父结点所在子树高度发生变化,由于父结点所在子树是爷爷所在子树的一部分,这种高度变化会进而影响爷爷所在子树的高度,所以一定要继续往上更新祖先平衡因子。

②插入新增结点后,更新parent父结点平衡因子出现的三种情况

  • 情况 1:父结点平衡因子更新后等于 1 或 - 1 。这表明在新增结点插入前,父结点的平衡因子为 0(因为在 AVL 树中,结点的平衡因子只能是 - 1、0、1 )。插入后,父结点左右子树出现了高度差,即父结点所在子树高度发生了变化,而父结点所在子树高度的变化会影响到爷爷所在子树的高度,所以需要继续往上更新祖先平衡因子。例如,若父结点平衡因子变为 1,说明右子树相对左子树变高了,这种高度变化会沿着树的层次向上传递。

  • 情况 2:父结点平衡因子更新后等于 2 或 - 2 。此时父结点所在子树高度一定发生了变化,并且子树已经不平衡。我们需要通过旋转操作来让父结点所在子树恢复平衡结构。旋转操作的本质是降低子树高度,通过合理调整结点之间的连接关系,使树的结构重新达到平衡。而且旋转后,父结点所在子树的高度会降低并恢复到新增结点插入之前的高度,同时不会影响爷爷所在子树的高度。完成旋转后,插入结点的操作就结束了。比如,当父结点平衡因子为 2 且右子树的平衡因子为 1 时,可能需要进行左旋操作来恢复平衡。
  • 情况 3:父结点平衡因子更新后等于 0 。这说明在插入前,父结点的平衡因子是 1 或 - 1 ,即插入前父结点所在子树左右子树高度是不一致的。新增结点插入到较矮的子树一侧,使得父结点左右子树高度一致,父结点所在子树变得更加平衡,并且子树高度不变。由于父结点所在子树是爷爷所在子树的一部分,爷爷所在子树高度也不会发生变化,所以不需要继续往上更新祖先平衡因子,插入操作到此结束。

③总结:我们通过衡量新增结点祖先结点的平衡因子是否受到影响,来判断每个祖先结点所在子树的平衡是否出现问题。若祖先结点平衡因子更新后为 1 或 - 1 ,我们需要考虑上一层祖先结点平衡因子是否变化(通过回溯判断);若更新后为 2 或 - 2 ,说明子树的平衡出现了问题,此时需要通过旋转操作来降低子树高度并恢复平衡。判断是否继续往上更新祖先结点平衡因子,关键在于新增结点的爷爷所在子树高度是否发生变化。

二、代码实现

bool Insert(const pair<K, V>& kv)
{
    //步骤 1:完成搜索二叉树新增结点的插入
    //如果根结点为空,直接将新结点作为根结点插入,插入成功,返回 true
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    //定义指针 parent 用于记录当前结点 cur 的父结点,初始化为空
    //定义指针 cur 用于在树中查找插入位置,初始化为根结点 _root
    Node* parent = nullptr;
    Node* cur = _root;
    //循环查找插入位置,当 cur 不为空时继续循环
    while (cur)
    {
        //如果当前结点的键值小于要插入结点的键值
        if (cur->_kv.first < kv.first)
        {
            //更新 parent 为当前结点 cur
            //将 cur 移动到当前结点的右子结点,继续查找
            parent = cur;
            cur = cur->_right;
        }
        //如果当前结点的键值大于要插入结点的键值
        else if (cur->_kv.first > kv.first)
        {
            //更新 parent 为当前结点 cur
            //将 cur 移动到当前结点的左子结点,继续查找
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            //如果找到相同键值的结点,说明树中已存在该键值,插入失败,返回 false
            return false;
        }
    }

    //创建新结点 cur,存储要插入的键值对 kv
    cur = new Node(kv);
    //根据要插入结点的键值与父结点键值的大小关系,确定新结点插入到父结点的左子树还是右子树
    if (parent->_kv.first > kv.first)
    {
        //要插入结点的键值小于父结点键值,插入到父结点的左子树
        parent->_left = cur;
    }
    else
    {
        //要插入结点的键值大于父结点键值,插入到父结点的右子树
        parent->_right = cur;
    }
    //设置新结点的父结点为 parent
    cur->_parent = parent;

    //步骤 2:更新平衡因子 - 往上更新插入结点的祖先平衡因子
    //从插入结点的父结点开始,向上遍历更新祖先结点的平衡因子,直到没有父结点(parent 为空)
    while (parent)
    {
        //如果新结点插入到父结点的右子树
        if (cur == parent->_right)
        {
            //父结点的右子树高度增加 1,根据平衡因子计算方式(右子树高度 - 左子树高度),父结点的平衡因子增加 1
            parent->_bf++;
        }
        else
        {
            //如果新结点插入到父结点的左子树
            //父结点的左子树高度增加 1,根据平衡因子计算方式,父结点的平衡因子减少 1
            parent->_bf--;
        }

        //更新完父结点的平衡因子后,判断父结点所在子树的平衡状态,以决定后续操作
        //如果父结点的平衡因子更新后为 1 或 -1
        if (parent->_bf == 1 || parent->_bf == -1)
        {
            //说明在插入新结点前,父结点的平衡因子为 0 。插入后,父结点左右子树出现了高度差,
            //即父结点所在子树高度发生了变化,而父结点所在子树高度的变化会影响到爷爷所在子树的高度,
            //所以需要继续往上更新祖先平衡因子。
            //将 parent 指针指向父结点的父结点,继续向上遍历
            //将 cur 指针指向当前结点的父结点,保持相对位置关系
            parent = parent->_parent;
            cur = cur->_parent;
        }
        //如果父结点的平衡因子更新后为 0
        else if (parent->_bf == 0)
        {
            //说明在插入前,父结点的平衡因子是 1 或 -1 ,即插入前父结点所在子树左右子树高度是不一致的。
            //新增结点插入到较矮的子树一侧,使得父结点左右子树高度一致,父结点所在子树变得更加平衡,
            //并且子树高度不变。由于父结点所在子树是爷爷所在子树的一部分,爷爷所在子树高度也不会发生变化,
            //所以不需要继续往上更新祖先平衡因子,插入操作到此结束,跳出循环。
            break;
        }
        //如果父结点的平衡因子更新后为 2 或 -2
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            //此时父结点所在子树高度一定发生了变化,并且子树已经不平衡。
            //需要通过旋转操作来让父结点所在子树恢复平衡结构。旋转操作的本质是降低子树高度,
            //通过合理调整结点之间的连接关系,使树的结构重新达到平衡。而且旋转后,父结点所在子树的高度会降低
            //并恢复到新增结点插入之前的高度,同时不会影响爷爷所在子树的高度。
            //根据不同的不平衡情况,选择相应的旋转操作(左旋、右旋、先左旋后右旋、先右旋后左旋)
            if (parent->_bf == 2 && cur->_bf == 1)
            {
                //父结点平衡因子为 2(右子树高)且当前结点平衡因子为 1(右子树的右子树高),进行左旋
                RotateL(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == -1)
            {
                //父结点平衡因子为 -2(左子树高)且当前结点平衡因子为 -1(左子树的左子树高),进行右旋
                RotateR(parent);
            }
            else if (parent->_bf == -2 && cur->_bf == 1)
            {
                //父结点平衡因子为 -2(左子树高)且当前结点平衡因子为 1(左子树的右子树高),进行先左旋后右旋
                RotateLR(parent);
            }
            else if (parent->_bf == 2 && cur->_bf == -1)
            {
                //父结点平衡因子为 2(右子树高)且当前结点平衡因子为 -1(右子树的左子树高),进行先右旋后左旋
                RotateRL(parent);
            }
            else
            {
                //出现异常情况,说明平衡因子的状态不符合预期,使用断言报错,提示程序可能存在错误
                assert(false);
            }

            //完成旋转操作后,子树已恢复平衡,插入操作结束,跳出循环
            break;
        }
        else
        {
            //如果父结点的平衡因子不是上述的 1、-1、0、2、-2 这些情况,
            //说明在插入新结点之前,树中某个结点的平衡因子就已经不满足 AVL 树的特性(平衡因子绝对值不超过 1)。
            //使用断言 assert(false) 直接报错,帮助开发者及时发现树在插入前就存在的问题,
            //确保程序在处理的是符合 AVL 树定义的结构。
            assert(false);
        }
    }

    //插入操作成功,返回 true
    return true;
}

在代码实现中,我们首先完成了搜索二叉树新增结点的插入操作。然后,通过一个循环来更新插入结点的祖先结点的平衡因子。在循环中,根据新增结点与父结点的位置关系,准确地更新父结点的平衡因子。接着,依据父结点平衡因子的更新结果,进行不同的处理:

  • 当父结点平衡因子为 1 或 - 1 时,表明父结点所在子树高度发生变化,需要继续往上更新祖先平衡因子,通过移动parentcur指针来实现向上遍历。
  • 当父结点平衡因子为 0 时,说明父结点所在子树高度未变,插入操作结束,直接跳出循环。
  • 当父结点平衡因子为 2 或 - 2 时,说明父结点所在子树不平衡,需要进行旋转操作来恢复平衡,旋转完成后插入操作结束,同样跳出循环。
  • 而最后的else语句配合assert(false)断言,是为了检测在插入新增结点之前树是否已经存在不符合 AVL 树特性的情况。如果在插入操作中,祖先结点平衡因子的更新结果不属于预期的三种情况(绝对值为 1、2、0 ),那么就说明树在插入前就存在问题,此时断言会报错,帮助我们及时发现并解决问题。

通过上述思路分析和代码实现,我们能够在 AVL 树插入新结点后,有效地更新祖先结点的平衡因子,并根据不同情况进行合理处理,确保 AVL 树始终保持平衡状态,维持其高效的查找性能。

步骤 3:通过旋转解决 (parent) 祖先所在子树平衡被破坏问题

一、AVL 树旋转的基本概念

AVL 树是在搜索二叉树的基础上,通过引入平衡因子来确保每个结点的左右子树高度差不超过 1,以此维护树的平衡。由于直接控制高度存在实时性和难度方面的问题,所以通常采用实时旋转的策略来维持平衡。当插入新结点后,在自下而上更新祖先结点平衡因子的过程中,一旦某个祖先结点的平衡因子变为 2 或 - 2 ,就表明该祖先结点所在子树出现了平衡问题,此时需要对该子树进行旋转操作。

旋转操作具有两个核心目标:其一,确保旋转后树依然保持搜索二叉树的特性,即左子树的所有结点值小于根结点值,右子树的所有结点值大于根结点值;其二,降低平衡因子为 2 或 - 2 的祖先结点所在的不平衡子树的高度,使其恢复平衡状态。需要明确的是,出现平衡问题的祖先结点不一定是整棵树的根结点,因此旋转操作可能作用于整棵树,也可能仅针对整棵树中的某一棵子树。

二、说明(重点)

(1)AVL 树旋转的基本分类:AVL 树的旋转主要分为四种情况,左单旋、右单旋、左右双旋(先左单旋后右单旋)、右左双旋(先右单旋后左单旋)。

(2)AVL 树发生失衡的判断以及旋转触发的条件:在 AVL 树中插入新增结点后,我们会自下而上更新祖先结点的平衡因子。一旦某个祖先结点的平衡因子变为 2 或 - 2 ,这个祖先结点就成为失衡祖先结点,以它为根结点的子树就是失衡子树。此时,我们要对这棵失衡子树进行旋转操作,来解决平衡问题。

(3)每种失衡子树形状对应自己的旋转操作

在 AVL 树的子树中插入新增结点后,子树的平衡状态被打破,成为失衡子树,其结构也随之改变,出现右右、左左、左右、右左这 4 种失衡形状,且每种形状对应不同的旋转操作以此来恢复子树的平衡状态:

  • 若呈现右右(右边高的右边高)形状,即失衡祖先结点的右子树比左子树高,且其右子树根结点的右子树也比左子树高,需进行左单旋。
  • 若呈现左左(左边高的左边高)形状,即失衡祖先结点的左子树比右子树高,且其左子树根结点的左子树也比右子树高,需进行右单旋。
  • 若呈现左右(左边高的右边高)形状,即失衡祖先结点的左子树比右子树高,但左子树根结点的右子树比左子树高,需进行左右旋转,也就是先左单旋,再右单旋。
  • 若呈现右左(右边高的左边高)形状,即失衡祖先结点的右子树比左子树高,但右子树根结点的左子树比右子树高,需进行右左旋转,即先右单旋,再左单旋。

  • 根据图片可知双旋与单旋的关系:对于双旋模型的子树,可以先通过(左 / 右)单旋操作将其转化为单旋模型,再利用单旋模型的(左 / 右)单旋操作解决失衡问题。
  • AVL 树旋转操作演示模型的构建与说明:
    为直观且全面地演示 AVL 树插入新增结点后触发的左单旋、右单旋、左右双旋和右左双旋四种操作,我们构建包含 a、b、c、d 子树的 AVL 树作为演示模型。其中,a、b、c、d 子树均为 AVL 树,各自保持平衡状态。

    由于 a、b、c、d 子树的高度 h 取值范围从 0 到无穷大,存在无数种情况,若对所有情况逐一分析,不仅工作量巨大,且缺乏实际研究价值。

    因此,我们将研究范围聚焦于 h 为 0、1、2 这三种具有代表性的情况。通过在这三种不同高度的子树中插入新增结点,能够覆盖常见的失衡场景,完整呈现左单旋、右单旋、左右双旋、右左双旋四种旋转操作的触发逻辑与恢复过程。
  • 在研究 AVL 树插入新增结点引发的旋转操作时,将范围聚焦于 a、b、c、d 子树高度 h 为 0、1、2 这三种情况的原因:随着子树高度增加,插入结点引发的失衡情况本质上是 h 为 0、1、2 时基础形态的延伸和重复。例如,当子树高度为 3 或更高时,插入结点导致的失衡和旋转操作,其原理与高度为 2 时类似,只是结构层级增加,处理过程在逻辑上是一致的。聚焦于 h 为 0、1、2 的情况,能够用最少的典型场景,说明所有高度下插入结点引发失衡的处理方法,避免对相似情况的重复研究。
  • 旋转操作的范围与前提
    分析前提
    :这里对 AVL 树 4 种旋转的分析,都是基于插入新增结点后一定会使某个祖先结点平衡因子更新为 2 或 - 2 ,导致该祖先结点所在子树失衡这一前提分析的。

    AVL 树中插入新增结点致使平衡子树必然失衡的场景分析:在 AVL 树中,当某个祖先结点在插入新增结点前的平衡因子为 1 或 - 1 时(即左右子树高度差为 1,存在右子树高于左子树或左子树高于右子树两种情况),插入操作必然导致该结点失衡。新增结点后,其平衡因子会从 1 或 - 1 更新为 2 或 - 2 ,破坏所在子树的平衡结构,进而形成右右、左左、左右、右左这 4 种典型的失衡子树模型。


    操作范围:失衡的祖先结点可能是整棵树的根结点,也可能是某棵子树的根结点,因此旋转操作的对象可能是整棵树,也可能是树中的某棵子树。

三、单旋情况分析

注意事项:下面说的AVL树可能是整棵AVL树,或者是整棵AVL树中的某棵子AVL树。

情况1:新增结点插入到AVL树较高右子树的右侧,即在c树插入 — 右右:左单旋
1.左单旋介绍

在 AVL 树中,若新增结点插入到祖先结点的较高右子树的右侧,且该祖先结点平衡因子绝对值超过 1(违反规则),需进行左单旋。如图片所示,插入新结点前,AVL 树处于平衡状态,30 结点平衡因子为 0 ,其左子树 a 和右子树 b 高度均为 h,右子树 b 的右子树 c 高度也为 h 。插入新结点后,新结点插入到 60 的右子树(并非右孩子 )中,使得 60 的右子树增加了一层,高度变为 h + 1,此时 30 结点的平衡因子更新为 2 ,导致以 30 为根的子树失去平衡。

左单旋的操作过程如下:为使 30 结点所在子树恢复平衡,需要减少 30 右子树的高度一层,同时增加左子树一层高度,所以将右子树往上提升。于是,30 结点向左旋转下移,由于 30 小于 60,按照 AVL 树作为二叉搜索树的特性(左子树结点值小于根结点值 ),30 只能放置在 60 的左子树位置。若 60 存在左子树,根据二叉搜索树结点值的大小关系,其左子树根的值一定小于 60 且大于 30 ,所以该左子树需放置在 30 的右子树位置。旋转操作完成后,再更新各相关结点的平衡因子,即可使子树重新达到平衡状态。

特殊情况及处理

  • 60 结点左子树的存在性:60 结点左子树可能存在或不存在。若其左子树存在,在左单旋时,需将该左子树连接到 30 结点的右子树位置;若不存在,左单旋后 30 结点的右子树为空。
  • 30 结点的地位
    • 30 结点为整棵树根结点:若 30 结点是整棵树的根结点,左单旋结束后,整棵树的根结点需更新为 60 结点,后续对树的查找、插入、删除等所有操作,都将从新的根结点 60 开始。
    • 若 30 结点为某子树根结点:

      当 30 是其父结点左子树的根结点:
      左单旋后,30 结点下移成为 60 结点的左子结点,此时 30 结点原父结点的左子树指针需重新指向新的根结点 60,以维持树结构的连贯性。

      当 30 是其父结点右子树的根结点:左单旋后,30 结点下移成为 60 结点的左子结点,30 结点原父结点的右子树指针需重新指向新的根结点 60,保证树结构符合二叉搜索树性质。
2.新增结点插入 AVL 树较高右子树的右侧(即在 c 树 插入)时,c树不同高度(h = 0、1、2)触发左单旋的机制分析

3.代码实现
3.1.代码实现中的问题及解决

(1)问题1 - 指针维护不完整:在实现左单旋的代码RotateL中,常见的错误是没有正确维护parentsubRsubRL等指针指向结点的父指针_parent。例如,在一些错误代码中,通过Node* subR = parent->_right;subRL = subR->_left;获取了相关子树指针,并进行了parent->_right = subRL;以及subR->_left = parent;等操作,但在这些操作过程中,没有更新这些parentsubRsubRL等指针指向结点的_parent指针,使其指向正确的父结点。这可能导致在后续对树进行遍历、查找等操作时出现错误,破坏树结构的完整性和正确性。正确的做法是在操作中明确地更新相关结点的父指针,如在代码中添加subRL->_parent = parentparent->_parent = subR等语句,以确保指针的正确性。

(2)问题2 - 未考虑空树情况:原代码没有考虑subRL指向的树(即图中的 c 树 )是个空树的情况。在当前代码逻辑中,当subRL为空指针时,执行subR->_left = parent;语句会存在对空指针进行解引用的风险,因为此时subRLsubR->_left的值,若subRL为空,直接对subR->_left赋值就会引发错误。改进后的代码通过增加if (subRL)判断,即if (subRL) subRL->_parent = parent;,避免了空指针解引用问题,确保代码在各种情况下都能正确执行。

(3)问题3 - 旋转对象的差异处理:旋转的对象可能是整棵树,也可能是整棵树中的某棵子树。
当旋转的是整棵 AVL 树时,意味着根结点发生了变化此时需要更新记录整棵树根结点指针_root使其指向旋转后新的根结点。在代码中,通过判断ppnode == nullptrppnode为原根结点的父结点指针)来确定是否旋转整棵树,若ppnode为空,则说明旋转的是整棵树,需要更新_root指针,如_root = subR; _root->_parent = nullptr;
若旋转的是整棵树中的某棵子树,旋转操作会改变该子树的结构和根结点,在旋转完成后,需要调整该子树父结点的左右指针指向。如果该子树原本是父结点的左子树,那么旋转后要将父结点的左子树指针指向旋转后新的子树根结点;若原本是父结点的右子树,旋转后则要将父结点的右子树指针指向新的子树根结点。代码中通过if (ppnode->_left == parent)else语句块来实现这一调整,如if (ppnode->_left == parent) ppnode->_left = subR; else ppnode->_right = subR; subR->_parent = ppnode;,以保证整棵树结构的正确性和连贯性。

(4)问题4 - 平衡因子更新:在 AVL 树的旋转操作(如左单旋或右单旋)完成后,必须更新旋转涉及的关键结点的平衡因子,以保证树的平衡性。在左单旋操作中,parentsubR所指向的结点在树中的位置和高度关系发生了改变,而平衡因子的定义为结点的右子树高度减去左子树高度。如果不更新parentsubR指向结点的平衡因子,可能会使树的平衡状态判断出现偏差,进而导致后续的插入、删除或查找操作无法正常进行。在理想的左单旋操作后,parentsubR这两个结点的左右子树高度差得到修正,使得它们的左右子树高度相等。根据平衡因子的定义,此时计算得出的平衡因子为 0。所以在代码中通过parent->_bf = subR->_bf = 0;语句,将这两个结点的平衡因子更新为 0 ,以准确反映此时它们的子树高度关系,维持 AVL 树平衡状态的判断依据,保证后续插入、删除等操作能基于正确的平衡状态进行。

3.2.代码实现

//左单旋函数,用于处理AVL树中因插入新结点导致的不平衡情况。
//当一个祖先结点的平衡因子更新为2,且新增结点插入到该祖先结点较高的右子树的右侧时,需要进行左单旋操作。
//parent参数指向当前平衡因子更新为2的祖先结点。
//这里采用传值传参的方式传递结点地址,因为我们只需要修改结点的成员(如左右子树指针、平衡因子等),而不是改变指针本身指向的结点。
void RotateL(Node* parent)
{
    //步骤1: 记录关键结点,为后续旋转操作做准备
    //subR指向parent(祖先结点)右子树的根结点。
    //在左单旋操作中,我们要将parent的右子树提升为新的根结点,所以先记录这个关键结点。
    Node* subR = parent->_right;
    //subRL指向parent(祖先结点)右子树的左子树根结点。
    //这个结点在旋转过程中位置会发生改变,提前记录下来方便后续调整树的结构。
    Node* subRL = subR->_left;
    //步骤2: 调整parent结点的右子树指针,并更新subRL的父指针
    //将parent的右子树指向subRL,这是左单旋操作的一部分,符合二叉搜索树和AVL树的结构调整逻辑。
    parent->_right = subRL;
    //检查subRL是否为空。如果不为空,说明subRL指向的子树存在。
    //此时需要更新subRL的父指针为parent,确保树结构中每个结点的父指针指向正确,维持树结构的连贯性。
    if (subRL)
    {
        subRL->_parent = parent;
    }
    //步骤3: 保存parent的父结点,为后续判断旋转类型做准备
    //提前保存parent的父结点ppnode,因为后续操作会改变parent的父指针指向。
    //这样做是为了后续能正确判断旋转的是整棵树还是树中的某棵子树,进而进行相应的树结构调整。
    Node* ppnode = parent->_parent;
    //步骤4: 进行左单旋的核心操作,调整subR和parent的位置关系
    //将subR的左子树指向parent,这是左单旋的核心操作之一,把parent结点下移成为subR的左子结点。
    subR->_left = parent;
    //更新parent的父指针为subR,完成旋转操作中parent结点的指针调整,确保parent在新的树结构中有正确的父指针指向。
    parent->_parent = subR;
    //步骤5: 判断旋转对象,根据不同情况调整树的根结点或子树指针
    //情况1: 旋转对象为整棵树
    //若ppnode为空,说明parent是整棵树的根结点,即此次旋转操作针对的是整棵AVL树。
    //当旋转整棵树时,根结点会发生变化,需要更新记录整棵树根结点的指针 _root。
    if (ppnode == nullptr)
    {
        //将整棵树的根结点更新为subR。
        _root = subR;
        //因为整棵树的根结点没有父结点,所以设置新根结点subR的父指针为空。
        _root->_parent = nullptr;
    }
    //情况2: 旋转对象为某棵子树
    //若ppnode不为空,说明旋转的是整棵树中的某棵子树。
    //旋转后需要调整该子树父结点的左右指针指向,确保子树在旋转后正确连接到父结点。
    else
    {
        //如果旋转的子树原本是其父结点的左子树,将父结点的左子树指针指向旋转后的新根结点subR。
        if (ppnode->_left == parent)
        {
            ppnode->_left = subR;
        }
        //如果旋转的子树原本是其父结点的右子树,将父结点的右子树指针指向旋转后的新根结点subR。
        else
        {
            ppnode->_right = subR;
        }
        //更新subR的父指针为ppnode,确保子树与父结点的正确连接,使新的子树根结点在树结构中有正确的父指针指向。
        subR->_parent = ppnode;
    }
    //步骤6: 更新旋转涉及结点的平衡因子,维持AVL树的平衡状态
    //在旋转操作后,parent与subR所指向的结点在树中的位置和高度关系发生了变化。
    //根据平衡因子的定义(右子树高度 - 左子树高度),在理想的左单旋操作后,parent和subR结点的左右子树高度相等,平衡因子均为0。
    //更新这两个结点的平衡因子是为了维持AVL树的平衡状态判断准确性,让后续的插入、删除等操作能基于正确的平衡状态进行。
    parent->_bf = subR->_bf = 0;
}

情况2:新增结点插入到AVL树较高左子树的左侧,即在a树插入 — 左左:右单旋
1.右单旋介绍

在 AVL 树中,若新增结点插入到祖先结点的较高左子树的左侧,且该祖先结点平衡因子绝对值超过 1(违反规则),需进行右单旋。如图片所示,插入新结点前,AVL 树处于平衡状态,60 结点平衡因子为 - 1,其左子树 30 的左右子树 a 和 b 高度均为 h,右子树 c 高度也为 h。插入新结点后,新结点插入到 30 的左子树(并非左孩子)中,使得 30 的左子树增加了一层,高度变为 h + 1,此时 60 结点的平衡因子更新为 - 2,导致以 60 为根的子树失去平衡。

右单旋的操作过程如下:为使 60 结点所在子树恢复平衡,需要减少 60 左子树的高度一层,同时增加右子树一层高度,所以将左子树往上提升。于是,60 结点向右旋转下移,由于 60 大于 30,按照 AVL 树作为二叉搜索树的特性(右子树结点值大于根结点值),60 只能放置在 30 的右子树位置。若 30 存在右子树,根据二叉搜索树结点值的大小关系,其右子树根的值一定大于 30 且小于 60,所以该右子树需放置在 60 的左子树位置。旋转操作完成后,再更新各相关结点的平衡因子,即可使子树重新达到平衡状态。

特殊情况及处理

  • 30 结点右子树的存在性:30 结点右子树可能存在或不存在。若其右子树存在,在右单旋时,需将该右子树连接到 60 结点的左子树位置;若不存在,右单旋后 60 结点的左子树为空。
  • 60 结点的地位
    • 若 60 结点为整棵树根结点:若 60 结点是整棵树的根结点,右单旋结束后,整棵树的根结点需更新为 30 结点,后续对树的查找、插入、删除等所有操作,都将从新的根结点 30 开始。
    • 若 60 结点为某子树根结点
      • 当 60 是其父结点左子树的根结点:右单旋后,60 结点下移成为 30 结点的右子结点,此时 60 结点原父结点的左子树指针需重新指向新的根结点 30,以维持树结构的连贯性。
      • 当 60 是其父结点右子树的根结点:右单旋后,60 结点下移成为 30 结点的右子结点,60 结点原父结点的右子树指针需重新指向新的根结点 30,保证树结构符合二叉搜索树性质。
2.新增结点插入 AVL 树较高左子树的左侧(即在 a 树 插入)时,a树不同高度(h = 0、1、2)触发右单旋的机制分析

3.代码实现

void RotateR(Node* parent)
{
	//步骤1: 记录关键结点,为后续旋转操作做准备
	//subL指向parent(祖先结点)左子树的根结点。
	//在右单旋操作中,我们要将parent的左子树提升为新的根结点,所以先记录这个关键结点。
	Node* subL = parent->_left;
	//subLR指向parent(祖先结点)左子树的右子树根结点。
	//这个结点在旋转过程中位置会发生改变,提前记录下来方便后续调整树的结构。
	Node* subLR = subL->_right;

	//步骤2: 调整parent结点的左子树指针,并更新subLR的父指针
	//将parent的左子树指向subLR,这是右单旋操作的一部分,符合二叉搜索树和AVL树的结构调整逻辑。
	parent->_left = subLR;
	//检查subLR是否为空。如果不为空,说明subLR指向的子树存在。
	//此时需要更新subLR的父指针为parent,确保树结构中每个结点的父指针指向正确,维持树结构的连贯性。
	if (subLR)
		subLR->_parent = parent;

	//步骤3: 保存parent的父结点,为后续判断旋转类型做准备
	//提前保存parent的父结点ppnode,因为后续操作会改变parent的父指针指向。
	//这样做是为了后续能正确判断旋转的是整棵树还是树中的某棵子树,进而进行相应的树结构调整。
	Node* ppnode = parent->_parent;

	//步骤4: 进行右单旋的核心操作,调整subL和parent的位置关系
	//将subL的右子树指向parent,这是右单旋的核心操作之一,把parent结点下移成为subL的右子结点。
	subL->_right = parent;
	//更新parent的父指针为subL,完成旋转操作中parent结点的指针调整,确保parent在新的树结构中有正确的父指针指向。
	parent->_parent = subL;

	//步骤5: 判断旋转对象,根据不同情况调整树的根结点或子树指针
	//情况1: 旋转对象为整棵树
	//若parent等于整棵树的根结点_root,说明此次旋转操作针对的是整棵AVL树。
	//当旋转整棵树时,根结点会发生变化,需要更新记录整棵树根结点的指针 _root。
	
	if (parent == _root)//判断祖先结点是否等于整棵树的根结点
	//或者写成 if (ppnode == nullptr) //判断祖先结点的父结点是否为空,若为空则祖先结点就是整棵树的根结点
	{
		//将整棵树的根结点更新为subL。
		_root = subL;
		//因为整棵树的根结点没有父结点,所以设置新根结点subL的父指针为空。
		_root->_parent = nullptr;
	}
	//情况2: 旋转对象为某棵子树
	//若parent不等于整棵树的根结点,说明旋转的是整棵树中的某棵子树。
	//旋转后需要调整该子树父结点的左右指针指向,确保子树在旋转后正确连接到父结点。
	else
	{
		//如果旋转的子树原本是其父结点的左子树,将父结点的左子树指针指向旋转后的新根结点subL。
		if (ppnode->_left == parent)
		{
			ppnode->_left = subL;
		}
		//如果旋转的子树原本是其父结点的右子树,将父结点的右子树指针指向旋转后的新根结点subL。
		else
		{
			ppnode->_right = subL;
		}
		//更新subL的父指针为ppnode,确保子树与父结点的正确连接,使新的子树根结点在树结构中有正确的父指针指向。
		subL->_parent = ppnode;
	}

	//步骤6: 更新旋转涉及结点的平衡因子,维持AVL树的平衡状态
	//在旋转操作后,parent与subL所指向的结点在树中的位置和高度关系发生了变化。
	//根据平衡因子的定义(右子树高度 - 左子树高度),在理想的右单旋操作后,parent和subL结点的左右子树高度相等,平衡因子均为0。
	//更新这两个结点的平衡因子是为了维持AVL树的平衡状态判断准确性,让后续的插入、删除等操作能基于正确的平衡状态进行。
	subL->_bf = parent->_bf = 0;
}

四、双旋情况分析

双旋操作由两个单旋组成。单旋处理的是失衡祖先结点左子树比右子树高(右单旋)或右子树比左子树高(左单旋)的情况。而双旋则针对更复杂的失衡场景。

情况3:新增结点插入到AVL树较高左子树的右侧,即在b 或 c树插入 — 左右:左右双旋(先左单旋再右单旋)
1.左右双旋介绍

左右双旋的触发场景:当新增结点插入到违反规则(平衡因子绝对值超过 1)的祖先结点的较高左子树的右侧(即在 b/c 树插入新增结点)时,需要进行左右双旋操作。 

左右双旋详细过程:

  • 插入新结点导致失衡:在 b 或 c 树中插入新结点后,例如插入到 b 树使 b 树高度变为 h,这会使以 90 为根的子树失衡,90 结点的平衡因子更新为 - 2,30 结点的平衡因子更新为 1。
  • 左单旋操作:对 30 进行左单旋,将 30 的右子结点 60 提升为新的根结点,30 变为 60 的左子结点。若 60 存在左子树(这里是高度变化后的 b 树),根据二叉搜索树性质,将其移至 30 的右子树位置。此时,30 结点的平衡因子变为 0,60 结点的平衡因子变为 1。
  • 右单旋操作:对 90 进行右单旋,将 90 的左子结点 60 提升为新的根结点,90 变为 60 的右子结点。若 60 存在右子树(这里是 c 树),根据二叉搜索树性质,将其移至 90 的左子树位置。经过这次旋转,整棵子树的结构进一步调整。
  • 平衡因子更新:旋转完成后,需重新计算各结点的平衡因子(右子树高度减去左子树高度),并更新相关结点的平衡因子,使子树重新达到平衡状态。

2.新增结点插入到AVL树较高左子树的右侧(即在b 或 c树插入)时,b 或 c 树不同高度(h = 0、1、2)触发左右双旋的机制分析

3.代码实现
(1)双旋操作后平衡因子的调节

双旋操作后平衡因子的调节受到双旋操作前新增结点插入位置的影响。具体而言,新增结点在 b/c 树插入会使左右旋转操作后 parent、subL、subLR 平衡因子的值不同。我们可以通过查看插入新增结点后 subLR 结点更新后的平衡因子的值来判断新增结点是插入到 b 树还是 c 树:

  • 当 b、c 树存在时,若 subLR 结点平衡因子更新为 1(右比左高),说明新增结点插入到 c 树。
  • 若 subLR 结点平衡因子更新为 -1(左比右高),说明新增结点插入到 b 树。
  • 当 b、c 树不存在(即都为空树)且 60 本身是新增结点时,subLR 结点平衡因子为 0。
(2)左右双旋代码实现

//左右双旋实现思路:复用左单旋、右单旋代码
//当新增结点插入到违反规则(平衡因子绝对值超过 1)的祖先结点的较高左子树的右侧时,
//会引发 AVL 树的失衡问题。此时需要通过左右双旋操作来重新调整树的结构,恢复树的平衡特性。
//左右双旋本质上是先执行一次左单旋,再执行一次右单旋,以此来修正树的高度差,保证 AVL 树的平衡。
void RotateLR(Node* parent)
{
    //步骤1: 记录关键结点和平衡因子
    //subL 指向 parent(祖先结点)的左子结点。
    //在后续的旋转操作中,该结点的位置和结构会发生变化,提前记录以便后续操作。
    Node* subL = parent->_left;
    //subLR 指向 parent 左子结点的右子结点。
    //这个结点是判断新增结点插入位置的关键,其平衡因子的变化能反映新增结点是插入到 b 树还是 c 树。
    Node* subLR = subL->_right;
    //插入新增结点后,记录 subLR 更新完的平衡因子的值。
    //后续会根据这个值来判断新增结点的插入位置,从而正确调整各结点的平衡因子。
    int bf = subLR->_bf;

    //步骤2: 进行左右双旋操作
    //左边高的右边高(形状是个折线,先往左折,再往右折) - 左右双旋的思路:先左单旋,再右单旋
    //对失衡的祖先结点的左子树进行左单旋
    //具体操作是将 subL 的右子结点提升为新的根结点,把 subL 变为其右子结点的左子结点。
    //若该右子结点存在左子树,会根据二叉搜索树的性质将其移至 subL 的右子树位置。
    //这一步操作可以初步调整子树的结构,为后续的右单旋做准备。
    RotateL(parent->_left); //先左单旋
    //左单旋完后,再对祖先结点所在整棵子树进行右单旋
    //此操作会将 parent 的左子结点提升为新的根结点,把 parent 变为其左子结点的右子结点。
    //若该左子结点存在右子树,会根据二叉搜索树的性质将其移至 parent 的左子树位置。
    //经过这次右单旋,整棵子树的结构会进一步调整,使树的高度差逐渐恢复平衡。
    RotateR(parent); //再右单旋

    //注意事项:复用单旋代码,单旋代码内部会把 subL、parent、subLR 结点的平衡因子都调节(更新)为 0
    //单旋代码的平衡因子更新逻辑是基于其自身的旋转操作,但左右双旋是更为复杂的情况。
    //单旋操作完成后,树的实际平衡状态还需要根据新增结点的插入位置来进一步确定。
    //所以使用左、右单旋代码后,必须手动根据实际情况对 subL、parent、subLR 结点的平衡因子进行调节(更新)

    //分析 AVL 树双旋触发条件,可观察未双旋时新增结点插入后 subLR 结点平衡因子的更新情况
    //以此判断新增结点是插入到 c 树、b 树还是其本身为新增结点。原因在于,subLR 是平衡因子更新的根源
    //其平衡因子变化传导至 subL 结点,再影响 parent 结点,当 parent 结点平衡因子变为 2 或 -2 时触发旋转
    //所以 subLR 平衡因子更新情况是判断双旋触发条件的关键

    //步骤3: 左右双旋操作后,更新平衡因子
    //注:不管 bf 等于 1 / -1 / 0,旋转后 subLR 都变成子树根结点,则 subLR 的平衡因子 subLR->_bf 都会是 0
    if (bf == 1) //旋转前 subLR 平衡因子 bf 等于 1 说明新增结点插入到 c 树
    {
        //新增结点插入到 c 树,经过左右双旋操作后,树的结构发生了变化。
        //parent 结点的左右子树高度达到平衡,根据平衡因子的定义(右子树高度 - 左子树高度),其平衡因子为 0。
        parent->_bf = 0;
        //subLR 成为子树根结点,左右子树高度平衡,所以其平衡因子为 0。
        subLR->_bf = 0;
        //subL 结点的左子树相对较高,导致其平衡因子为 -1。
        subL->_bf = -1;
    }
    else if (bf == -1) //旋转前 subLR 平衡因子 bf 等于 -1 说明新增结点插入到 b 树
    {
        //新增结点插入到 b 树,双旋操作后树的结构改变。
        //parent 结点的右子树相对较高,所以其平衡因子为 1。
        parent->_bf = 1;
        //subLR 成为子树根结点,左右子树高度平衡,平衡因子为 0。
        subLR->_bf = 0;
        //subL 结点的左右子树高度平衡,平衡因子为 0。
        subL->_bf = 0;
    }
    //旋转前 subLR 平衡因子 bf 等于 0 说明 b、c 树是个空树,即 b、c 树是个空树,则 subLR 本身就是新增结点
    //注:即使单旋后会把 parent、subLR、subL 的平衡因子更新为 0,但是不要双旋不要依赖于单旋对这种情况的处理
    //因为单旋可能出错导致没有更新
    else if (bf == 0)
    {
        //b、c 树为空,subLR 本身为新增结点,经过双旋操作后,整棵子树达到平衡状态。
        //此时 parent、subLR、subL 结点的左右子树高度均相等,根据平衡因子的定义,它们的平衡因子都为 0。
        parent->_bf = 0;
        subLR->_bf = 0;
        subL->_bf = 0;
    }
    else
    {
        //注:使用 assert(false); 来断言旋转后平衡因子的更新只有上面三种情况
        //若都不是上面三种情况,则平衡因子的更新一定会在上面三种情况之外,则此时一定有误
        //直接使用 assert(false) 断言报错帮助我们检查出问题
        assert(false);
    }
}
情况4:新增结点插入到AVL树较高右子树的左侧,即在b 或 c树插入 — 右左:右左双旋(先右单旋再左单旋)
1.右左双旋介绍

右左双旋的触发场景:当新增结点插入到违反规则(平衡因子绝对值超过 1)的祖先结点的较高右子树的左侧(即在 b/c 树插入新增结点)时,需要进行右左双旋操作。

右左双旋详细过程:

  • 插入新结点导致失衡:在 b 或 c 树中插入新结点后,例如插入到 c 树使 c 树高度变为 h,这会使以 30 为根的子树失衡,30 结点的平衡因子更新为 2,90 结点的平衡因子更新为 -1。
  • 右单旋操作:对 90 进行右单旋,将 90 的左子结点 60 提升为新的根结点,90 变为 60 的右子结点。若 60 存在左子树(这里是 b 树),根据二叉搜索树性质,将其移至 90 的左子树位置。此时,90 结点的平衡因子变为 0,60 结点的平衡因子变为 -1。
  • 左单旋操作:对 30 进行左单旋,将 30 的右子结点 60 提升为新的根结点,30 变为 60 的左子结点。若 60 存在右子树(这里是高度变化后的 c 树),根据二叉搜索树性质,将其移至 30 的右子树位置。经过这次旋转,整棵子树的结构进一步调整。
  • 平衡因子更新:旋转完成后,需重新计算各结点的平衡因子(右子树高度减去左子树高度),并更新相关结点的平衡因子,使子树重新达到平衡状态。

2.新增结点插入到AVL树较高右子树的左侧(即在b 或 c树插入)时,b 或 c 树不同高度(h = 0、1、2)触发右左双旋的机制分析

3.代码实现
(1)双旋操作后平衡因子的调节

双旋操作后平衡因子的调节受到双旋操作前新增结点插入位置的影响。具体而言,新增结点在 b/c 树插入会使左右旋转操作后 parent、subR、subRL 平衡因子的值不同。我们可以通过查看插入新增结点后 subRL 结点更新后的平衡因子的值来判断新增结点是插入到 b 树还是 c 树:

  • 当 b、c 树存在时,若 subRL 结点平衡因子更新为 1(右比左高),说明新增结点插入到 c 树。
  • 若 subRL 结点平衡因子更新为 -1(左比右高),说明新增结点插入到 b 树。
  • 当 b、c 树不存在(即都为空树)且 60 本身是新增结点时,subRL 结点平衡因子为 0。
(2)右左双旋代码实现

//右左双旋函数(RotateRL),专门用于处理AVL树在特定插入场景下的失衡问题
//触发条件:当新增结点插入到祖先结点较高右子树的左侧,且导致祖先结点的平衡因子绝对值大于1时
//解决策略:通过组合右单旋和左单旋操作(先执行右单旋,再执行左单旋),重新调整树结构,恢复AVL树的平衡特性
//该函数复用了已有的左单旋(RotateL)和右单旋(RotateR)函数,提升代码复用性
void RotateRL(Node* parent)
{
    //步骤1:标记关键结点并获取平衡因子信息
    //subR指向当前失衡的祖先结点parent的右子结点。
    //在后续的双旋操作中,subR是右单旋的核心操作对象,其结构和位置会发生变化,提前记录便于后续操作。
    Node* subR = parent->_right;
    //subRL指向subR的左子结点,该结点是判断新增结点插入位置的关键所在。
    //subRL的平衡因子状态,直接决定了新增结点是插入到b树(subR的左子树)还是c树(subRL的右子树),
    //进而影响后续各结点平衡因子的更新策略。
    Node* subRL = subR->_left;
    //bf用于记录subRL更新后的平衡因子值。
    //通过这个值,我们可以在后续逻辑中准确判断新增结点的插入位置,
    //从而对subR、parent、subRL等关键结点的平衡因子进行正确调整,确保AVL树的平衡性。
    int bf = subRL->_bf;

    //步骤2:执行右左双旋操作
    //首先,对parent的右子树执行右单旋操作(RotateR)。
    //具体操作过程如下:
    //将subR的左子结点提升为新的根结点,subR则变为该左子结点的右子结点;
    //若该左子结点存在右子树,根据二叉搜索树的性质(左子树所有结点值小于根结点值,右子树所有结点值大于根结点值),
    //将其移至subR的左子树位置。这一步操作初步调整了局部子树结构,为后续的左单旋做准备,使树结构更接近平衡状态。
    RotateR(parent->_right);
    //右单旋完成后,对以parent为根的整棵子树执行左单旋操作(RotateL)。
    //具体操作过程如下:
    //将parent的右子结点(经过右单旋后结构已改变)提升为新的根结点,parent变为该右子结点的左子结点;
    //若该右子结点存在左子树,同样依据二叉搜索树的性质,将其移至parent的右子树位置。
    //经过这次左单旋,整棵子树的结构得到进一步调整,最终使树的高度差恢复平衡,AVL树重新达到平衡状态。
    RotateL(parent);

    //重要提示:虽然左单旋和右单旋函数内部会将涉及的subR、parent、subRL等结点的平衡因子默认设置为0,
    //但这是基于单旋操作自身逻辑的简单处理。在右左双旋这种复杂场景下,单旋后的树结构平衡状态
    //还需要结合新增结点的实际插入位置才能准确确定。因此,在调用完单旋函数后,
    //必须手动根据实际情况对subR、parent、subRL等结点的平衡因子进行重新计算和调整,
    //以保证AVL树在后续的插入、删除、查找等操作中,能够基于正确的平衡状态进行判断和处理。

    //平衡因子更新核心逻辑解析:
    //subRL结点是整个右左双旋操作中判断的核心。新增结点插入后引发的平衡因子变化,
    //会从subRL开始,逐级传导至subR结点,进而影响到parent结点。当parent结点的平衡因子
    //超出[-1, 1]的范围时,就会触发右左双旋操作。所以,观察在未进行双旋操作时subRL结点平衡因子的更新情况,
    //是判断新增结点插入位置(c树、b树,或者subRL本身就是新增结点)以及确定双旋后各结点平衡因子
    //正确更新方式的关键依据。

    //步骤3:右左双旋操作后,精确更新各结点的平衡因子
    //无论初始情况下bf的值是1、-1还是0,经过右左双旋操作后,subRL都会成为子树的根结点。
    //根据AVL树平衡因子的定义(右子树高度 - 左子树高度),作为根结点且平衡的subRL,其平衡因子必然为0。
    if (bf == 1) 
    {
        //如果旋转前subRL的平衡因子bf等于1,这表明新增结点插入到了c树(subRL的右子树)。
        //经过右左双旋操作后,树的结构发生了变化:
        //subR结点的左右子树高度达到平衡,根据平衡因子的计算方式,其平衡因子更新为0;
        //parent结点的左子树相对较高,因此其平衡因子更新为 -1;
        //subRL作为新的子树根结点,左右子树高度平衡,其平衡因子为0。
        subR->_bf = 0;
        parent->_bf = -1;
        subRL->_bf = 0;
    }
    else if (bf == -1) 
    {
        //如果旋转前subRL的平衡因子bf等于 -1,这表明新增结点插入到了b树(subR的左子树)。
        //经过右左双旋操作后,树的结构改变如下:
        //subR结点的左子树相对较高,所以其平衡因子更新为1;
        //parent结点的左右子树高度达到平衡,其平衡因子更新为0;
        //subRL作为新的子树根结点,左右子树高度平衡,其平衡因子为0。
        subR->_bf = 1;
        parent->_bf = 0;
        subRL->_bf = 0;
    }
    //如果旋转前subRL的平衡因子bf等于0,这说明b树和c树都是空树,即subRL本身就是新增结点。
    //尽管单旋操作可能会将parent、subRL、subR的平衡因子更新为0,但不能完全依赖单旋操作来处理这种情况,
    //因为单旋操作存在出错而未正确更新的风险。所以在此处,我们手动将subR、parent、subRL的平衡因子设为0,
    //以确保整棵子树的平衡状态能够被准确表示。
    else if (bf == 0) 
    {
        subR->_bf = 0;
        parent->_bf = 0;
        subRL->_bf = 0;
    }
    else 
    {
        //使用assert(false)进行断言,目的是确保旋转后平衡因子的更新只存在上述三种情况。
        //如果出现了非上述三种情况,说明在平衡因子更新的逻辑中存在错误。
        //通过这个断言,程序会在出现异常时立即报错,帮助开发者快速定位和解决问题,
        //保证AVL树平衡维护逻辑的正确性和稳定性。
        assert(false);
    }
}

五、AVL树四种旋转类型总结

1.不同形状的失衡子树对应不同类型的旋转操作。

AVL 树的旋转操作旨在应对插入新结点导致的失衡问题,共涵盖四种类型。分析时以新增结点的失衡祖先结点所在子树为对象,该子树包含 a、b、c、d AVL 子树,因这些子树高度情况繁多,仅探讨高度 h 为 0、1、2 时,插入新增结点引发的旋转操作。

当在 AVL 树子树中插入新增结点后,若某个祖先结点插入前平衡因子为 1 或 - 1(即左右子树高度差为 1 ),插入后变为 2 或 - 2 ,该祖先结点所在子树就会失衡,形成以下 4 种失衡模型(形状):

  • 右右(右边高的右边高):失衡祖先结点的右子树比左子树高,且失衡祖先结点右子树根结点的右子树比左子树高,采用左单旋操作。
  • 左左(左边高的左边高):失衡祖先结点的左子树比右子树高,且失衡祖先结点左子树根结点的左子树比右子树高,采用右单旋操作。
  • 左右(左边高的右边高):失衡祖先结点的左子树比右子树高,且失衡祖先结点左子树根结点的右子树比左子树高,采用左右旋转,即先左单旋,再右单旋。
  • 右左(右边高的左边高):失衡祖先结点的右子树比左子树高,且失衡祖先结点右子树根结点的左子树比右子树高,采用右左旋转,即先右单旋,再左单旋。

注意事项:通过对双旋模型的某棵子树进行左 / 右单旋操作,可将双旋模型转化为单旋模型,进而利用单旋操作解决整个双旋模型的失衡问题。 

2.AVL树四种旋转类型介绍

(1)情况 1:新增结点插入到 AVL 树较高右子树的右侧,即在 c 树插入 — 右右:左单旋

新结点插入 60 右子树(非右孩子)致 30 为根二叉树失衡。为恢复平衡,将 30 右子树往上提,30 左旋转至 60 左子树 。若 60 有左子树,按二叉搜索树性质放于 30 右子树,最后更新节点平衡因子。

(2)情况 2:新增结点插入到 AVL 树较高左子树的左侧,即在 a 树插入 — 左左:右单旋

新结点插入 30 左子树(非左孩子)使 60 为根二叉树失衡。为恢复平衡,将 60 左子树往上提,60 右旋转至 30 右子树。若 30 有右子树,按二叉搜索树性质放于 60 左子树,最后更新节点平衡因子。

(3)情况 3:新增结点插入到 AVL 树较高左子树的右侧,即在 b 或 c 树插入 — 左右:左右双旋(先左单旋再右单旋)

将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再
考虑平衡因子的更新。

(4)情况 4:新增结点插入到 AVL 树较高右子树的左侧,即在 b 或 c 树插入 — 右左:右左双旋(先右单旋再左单旋)

将双旋变成单旋后再旋转,即:先对90进行右单旋,然后再对30进行左单旋,旋转完成后再
考虑平衡因子的更新。

六、AVL 树失衡时四种旋转操作的调用代码

(1)AVL 树失衡时四种旋转操作的调用逻辑与判断依据

若以 parent 为根的子树不平衡,即 parent 的平衡因子为 2 或者 -2,可按以下情况处理:

  • 当 parent 的平衡因子为 2 时,表明 parent 的右子树高。设 parent 的右子树的根为 subRsubR 的左子树的根为 subRL
    • 当 subR 的平衡因子为 1 时,执行左单旋操作。
    • 当 subR 的平衡因子为 -1 时,执行右左双旋操作(即先右单旋,再左单旋 )。
  • 当 parent 的平衡因子为 -2 时,表明 parent 的左子树高。设 parent 的左子树的根为 subLsubL 的右子树的根为 subLR
    • 当 subL 的平衡因子为 -1 时,执行右单旋操作。
    • 当 subL 的平衡因子为 1 时,执行左右双旋操作(即先左单旋,再右单旋 )。

旋转完成后,以原 parent 为根的子树高度降低,恢复平衡状态,此时无需再向上更新平衡因子及进行旋转操作。

(2)四种旋转操作的调用代码 — Insert的实现
//插入操作,用于向 AVL 树中插入一个键值对
//参数 kv 是要插入的键值对,类型为 std::pair<K, V>,其中 K 代表键的类型,V 代表值的类型
//返回值:若插入成功,返回 true;若键已存在于树中,插入失败,返回 false
bool Insert(const pair<K, V>& kv)
{
    //步骤 1: 处理树为空的情况
    //检查树是否为空,通过判断根结点指针 _root 是否为 nullptr 来确定
    //若根结点为空,表明树目前为空,此时直接创建一个新的结点作为根结点
    //新结点将存储传入的键值对 kv,使用 new 操作符动态分配内存
    //插入操作成功,返回 true
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    //步骤 2: 寻找插入位置
    //初始化两个指针,用于遍历树以找到合适的插入位置
    //parent 指针用于记录当前遍历到的结点的父结点,初始化为 nullptr
    //cur 指针用于从根结点开始遍历树,初始化为根结点 _root
    Node* parent = nullptr;
    Node* cur = _root;

    //使用 while 循环遍历树,只要当前结点 cur 不为空,就继续查找
    //当 cur 为 nullptr 时,表示找到了合适的插入位置
    while (cur)
    {
        //比较当前结点的键(cur->_kv.first)和要插入的键(kv.first)
        //如果当前结点的键小于要插入的键,说明要插入的结点应该在当前结点的右子树中
        if (cur->_kv.first < kv.first)
        {
            //更新 parent 指针为当前结点,记录当前结点为父结点
            parent = cur;
            //将 cur 指针移动到当前结点的右子结点,继续在右子树中查找
            cur = cur->_right;
        }
        //如果当前结点的键大于要插入的键,说明要插入的结点应该在当前结点的左子树中
        else if (cur->_kv.first > kv.first)
        {
            //更新 parent 指针为当前结点,记录当前结点为父结点
            parent = cur;
            //将 cur 指针移动到当前结点的左子结点,继续在左子树中查找
            cur = cur->_left;
        }
        //如果当前结点的键等于要插入的键,说明键已经存在于树中
        //插入操作失败,返回 false
        else
        {
            return false;
        }
    }

    //步骤 3: 插入新结点
    //当找到合适的插入位置(cur 为 nullptr)后,创建一个新的结点
    //新结点存储传入的键值对 kv,使用 new 操作符动态分配内存
    cur = new Node(kv);

    //根据父结点的键和要插入的键的大小关系,将新结点插入到父结点的左子树或右子树中
    //如果父结点的键大于要插入的键,将新结点插入到父结点的左子树
    if (parent->_kv.first > kv.first)
    {
        parent->_left = cur;
    }
    //否则,将新结点插入到父结点的右子树
    else
    {
        parent->_right = cur;
    }
    //设置新结点的父结点指针,使其指向父结点,完成新结点的插入,建立双向连接关系
    cur->_parent = parent;

    //步骤 4: 更新平衡因子
    //从新插入结点的父结点开始,向上更新平衡因子,直到根结点或平衡因子不需要再更新
    //平衡因子用于衡量结点左右子树的高度差,确保树的平衡,其值为右子树高度减去左子树高度
    while (parent)
    {
        //根据新结点的位置更新父结点的平衡因子
        //如果新结点是父结点的右子结点,说明父结点的右子树高度增加,平衡因子加 1
        if (cur == parent->_right)
        {
            parent->_bf++;
        }
        //如果新结点是父结点的左子结点,说明父结点的左子树高度增加,平衡因子减 1
        else
        {
            parent->_bf--;
        }

        //判断当前父结点平衡因子的情况
        //若当前父结点的平衡因子为 1 或 -1,意味着以该父结点为根的子树高度发生了变化,但尚未失衡
        //不过,该子树高度的变化可能会对其父结点(即当前父结点的父结点)的平衡产生影响
        //所以需要继续向上更新平衡因子
        if (parent->_bf == 1 || parent->_bf == -1)
        {
            //保存当前父结点的父结点指针,后续更新 cur 指针会用到
            Node* grandParent = parent->_parent;
            //更新 cur 指针为当前父结点,因为后续要以当前父结点为基础继续向上更新
            cur = parent;
            //更新 parent 指针为当前父结点的父结点,继续向上更新平衡因子
            parent = grandParent;
        }
        //若当前父结点的平衡因子为 0,表明插入新结点后,以该父结点为根的子树高度未影响其平衡状态
        //也就是该父结点左右子树的高度差没有改变,无需再向上更新平衡因子,退出循环
        else if (parent->_bf == 0)
        {
            break;
        }
        //如果当前父结点的平衡因子为 2 或 -2,说明以该父结点为根的子树已经失衡,需要进行旋转操作来恢复平衡
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            //右右(RR)情况:当前父结点的平衡因子为 2,右子结点的平衡因子为 1
            //这种情况表示右子树的右子树较高,需要进行左单旋操作
            if (parent->_bf == 2 && cur->_bf == 1) 
            {
                //调用左单旋函数,传入当前父结点作为参数,通过调整结点连接关系使树恢复平衡
                //左单旋会将当前父结点的右子结点提升为新的根结点
                //当前父结点变为新根结点的左子结点
                //新根结点原来的左子结点变为当前父结点的右子结点
                RotateL(parent);
            }
            //左左(LL)情况:当前父结点的平衡因子为 -2,左子结点的平衡因子为 -1
            //这种情况表示左子树的左子树较高,需要进行右单旋操作
            else if (parent->_bf == -2 && cur->_bf == -1) 
            {
                //调用右单旋函数,传入当前父结点作为参数,通过调整结点连接关系使树恢复平衡
                //右单旋会将当前父结点的左子结点提升为新的根结点
                //当前父结点变为新根结点的右子结点
                //新根结点原来的右子结点变为当前父结点的左子结点
                RotateR(parent);
            }
            //左右(LR)情况:当前父结点的平衡因子为 -2,左子结点的平衡因子为 1
            //这种情况表示左子树的右子树较高,需要进行左右双旋操作(先左单旋,再右单旋)
            else if (parent->_bf == -2 && cur->_bf == 1) 
            {
                //调用左右双旋函数,传入当前父结点作为参数,分两步调整树结构以恢复平衡
                //先对当前父结点的左子树进行左单旋,将左子树的右子结点提升为左子树的根结点
                //再对当前父结点进行右单旋,将提升后的左子树的根结点作为新的根结点
                RotateLR(parent);
            }
            //右左(RL)情况:当前父结点的平衡因子为 2,右子结点的平衡因子为 -1
            //这种情况表示右子树的左子树较高,需要进行右左双旋操作(先右单旋,再左单旋)
            else if (parent->_bf == 2 && cur->_bf == -1) 
            {
                //调用右左双旋函数,传入当前父结点作为参数,分两步调整树结构以恢复平衡
                //先对当前父结点的右子树进行右单旋,将右子树的左子结点提升为右子树的根结点
                //再对当前父结点进行左单旋,将提升后的右子树的根结点作为新的根结点
                RotateRL(parent);
            }
            //如果不满足以上四种情况,说明出现了异常情况
            //可能是代码逻辑错误,或者树的状态不符合 AVL 树性质
            //使用 assert 函数进行断言,若条件为 false 程序会终止并提示错误
            else
            {
                assert(false);
            }

            //旋转操作完成后,以当前父结点为根的子树已经恢复平衡,不需要再向上更新平衡因子
            //退出循环
            break;
        }
        //如果当前父结点的平衡因子不在 -2 到 2 的范围内,属于异常情况
        //正常情况下 AVL 树结点的平衡因子只可能是 -2、-1、0、1、2
        //使用 assert 函数进行断言,若条件为 false 程序会终止并提示错误
        else
        {
            assert(false);
        }
    }

    //插入操作成功,返回 true
    return true;
}

AVL树Insert接口测试

一、AVL树中序遍历

//定义一个模板类 AVLTree,用于实现 AVL 树数据结构
//模板参数 K 表示键的类型,V 表示值的类型
template<class K, class V>
class AVLTree
{
    //定义一个类型别名 Node,用于简化对 AVLTreeNode<K, V> 类型的引用
    typedef AVLTreeNode<K, V> Node;
public:
    //公有成员函数 InOrder,用于对 AVL 树进行中序遍历
    //中序遍历是一种二叉树遍历方式,按照左子结点->根结点->右子结点的顺序访问结点
    //此函数调用私有成员函数 _InOrder 进行实际的遍历操作,并在遍历结束后输出换行符
    void InOrder()//主函数
    {
        //调用私有递归函数 _InOrder,从根结点开始进行中序遍历
        _InOrder(_root);
        //输出换行符,使输出结果更清晰
        cout << endl;
    }

private:
    //私有成员函数 _InOrder,是一个递归函数,用于实现 AVL 树的中序遍历
    //参数 root 是当前要遍历的子树的根结点
    void _InOrder(Node* root)//子函数
    {
        //如果当前根结点为空指针,说明已经到达空结点
        //此时直接返回,结束本次递归调用
        if (root == nullptr)
        {
            return;
        }

        //递归调用 _InOrder 函数,对当前根结点的左子树进行中序遍历
        //这会先访问左子树中的所有结点,直到最左侧的叶子结点
        _InOrder(root->_left);

        //输出当前根结点的键值(即键值对中的键),并在后面添加一个空格
        //这是中序遍历中访问根结点的步骤
        cout << root->_kv.first << " ";

        //递归调用 _InOrder 函数,对当前根结点的右子树进行中序遍历
        //完成对整个子树的中序遍历
        _InOrder(root->_right);
    }
private:
    //私有成员变量 _root,指向 AVL 树的根结点
    //初始化为空指针,表示创建 AVL 树对象时树为空
    Node* _root = nullptr;
};

二、AVL树的验证 IsBalance - 验证一棵二叉树是否是AVL树

1.AVL 树定义及与平衡树关系

AVL 树作为一种自平衡二叉搜索树,具有独特的性质:每个结点的左右子树高度差的绝对值不超过 1,且每个结点平衡因子的绝对值也不超过 1 。需要明确的是,AVL 树必然是平衡树,但平衡树却不一定是 AVL 树。这是因为平衡树只要求每个结点的左右子树高度差在一定范围内,而 AVL 树在此基础上,还对平衡因子有严格限制。

2.判断二叉树是否是 AVL 树的常见错误写法及问题剖析

(1)问题 1:基于平衡因子遍历判断的误区

有一种错误的思路是,通过遍历整棵二叉树,检查每个结点平衡因子绝对值不超过 1 来判定二叉树是 AVL 树。在创建 AVL 树的过程中,结点平衡因子是手动更新的。一旦更新逻辑出现错误,就可能出现某个结点平衡因子绝对值超过 1 的情况。然而,此时树的底层结构可能依然符合 AVL 树的特征。例如,在插入新节点时,由于代码中平衡因子更新部分存在逻辑缺陷,导致某个节点的平衡因子计算错误,但树的整体结构仍然近似 AVL 树。所以,仅依据平衡因子判断,会将这种树误判为非 AVL 树。正确的判断思路应是检查二叉树每个结点左右子树的高度差不超过 1。不过,满足此条件只能说明二叉树可能是 AVL 树,因为平衡树不一定就是 AVL 树,AVL 树对平衡因子还有额外要求。

(2)问题 2:平衡树判断的不完整性

仅判断当前树的左右子树高度差不超过 1,不能直接认定该树是平衡树。因为即使整棵树的左右子树高度差满足条件,树的左、右子树内部仍可能存在不是平衡树的情况。比如,一棵二叉树的整体左右子树高度差符合要求,但左子树中某个子树的左右子树高度差过大,这种情况下整棵树就不是平衡树,更不可能是 AVL 树。所以,要判断二叉树是平衡树,必须递归检查每个结点的左右子树高度差都不超过 1 ,从根节点开始,对每个子树都进行同样的检查,确保树的每个部分都满足平衡树的条件。

错误代码如下:

(3)问题 3:平衡树与 AVL 树判断的混淆

仅通过判断二叉树每个结点的左右子树高度差的绝对值不超过 1,只能判定该二叉树是平衡树,无法确定其是否为 AVL 树。AVL 树有两个规定:一是每个结点的左右子树高度差的绝对值不超过 1;二是每个结点平衡因子的绝对值不超过 1。一棵二叉树只有同时满足这两个规定才是 AVL 树,仅满足前者只能是平衡树。例如,有些二叉树虽然左右子树高度差控制得很好,但在插入或删除节点时,由于平衡因子更新不准确,导致某些节点的平衡因子绝对值超过 1,这样的树就只是平衡树而非 AVL 树。

错误代码:图片中的代码缺少对二叉树每个结点平衡因子绝对值是否不超过 1 的判断 。仅通过代码中的逻辑,只能判断二叉树每个结点的左右子树高度差的绝对值不超过 1,即只能判定该二叉树是平衡树,但无法确定其是否为 AVL 树,因为没有检查平衡因子相关条件。

(4)问题 4:结点平衡因子更新异常问题

在使用相关函数判断二叉树是否为 AVL 树时,必须关注二叉树中结点平衡因子的更新情况。

例如在插入结点后,可能会引发某些结点平衡因子异常。以插入节点 14 为例,可能会导致 6 结点平衡因子出现异常。为了排查这种问题,可以通过打条件断点的方式,找出插入特定结点时触发的旋转类型,进而排查更新平衡因子出错的问题。通过这种方式,可以更精确地定位代码中平衡因子更新逻辑的错误,确保树的平衡性和 AVL 树性质。

3.AVL树验证 IsBalance 的实现

(1)代码思路分析
判断一棵二叉树是否是 AVL 树分两个步骤:首先判断该二叉树是平衡树;在确定是平衡树后,再判断平衡树的每个结点平衡因子值不超过 1。对于后者,采用反证法判断,即只要有 1 个结点的平衡因子不等于该结点左右子树高度差值,则该二叉树不是 AVL 树;反之,每个结点都满足此条件时,该二叉树才有可能是 AVL 树。这两个步骤缺一不可,只有先确定树是平衡树,再验证平衡因子的准确性,才能确定树是否为 AVL 树。

(2)代码实现思路

  • 判断二叉树是平衡树:依据二叉树的每个结点的左右子树高度差的绝对值不超过 1 这一标准。从根结点开始,递归计算每个结点的左右子树高度差,若所有结点都满足高度差条件,则树是平衡树。
  • 判断平衡树的每个结点平衡因子值不超过 1:运用反证法,只要存在 1 个结点的平衡因子不等于该结点左右子树高度差值,该二叉树就不是 AVL 树;若每个结点都符合条件,该二叉树才可能是 AVL 树。在代码中,通过比较计算出的高度差与结点记录的平衡因子来进行判断。

(3)代码实现


//定义一个模板类 AVLTree,K 代表键的类型,V 代表值的类型
//该类用于实现 AVL 树(一种自平衡二叉搜索树)的数据结构
template<class K, class V>
class AVLTree
{
	//为 AVLTreeNode<K, V> 定义一个别名 Node,方便后续代码编写和阅读
	using Node = AVLTreeNode<K, V>;
public:
	//对外提供的判断树是否为 AVL 树的接口,调用私有递归函数 _IsBalance 从根结点开始检查树的平衡性以及平衡因子的正确性。
	//如果返回 true,表示树 是 AVL 树;如果返回 false,则不是 AVL 树
	bool IsBalance()
	{
		return _IsBalance(_root);
	}

	//计算树高度的接口,调用私有递归函数 _Height 从根结点开始计算
	int Height()
	{
		return _Height(_root);
	}

private:
	//递归计算以 root 为根的树高度,若 root 为空,高度为 0;否则为左右子树高度最大值加 1
	int _Height(Node* root)
	{
		//若当前结点为空,说明遇到空树,而空树高度为 0
		if (root == nullptr)
			return 0;

		//递归调用 _Height 函数计算左子树的高度
		int leftH = _Height(root->_left);

		//递归调用 _Height 函数计算右子树的高度
		int rightH = _Height(root->_right);

		//当前树的高度为左右子树高度的最大值加 1
		return leftH > rightH ? leftH + 1 : rightH + 1;
	}

	//递归判断以 root 为根的树是否为 AVL 树,先检查平衡因子,再递归检查左右子树平衡性
	bool _IsBalance(Node* root)
	{
		//若当前结点为空,说明遇到空树,空树是平衡树,也是 AVL 树,直接返回 true。
		if (root == nullptr)
			return true;

		//这里必须使用变量接收返回值,以便后续多次使用
		int leftH = _Height(root->_left); // 求当前根的左子树高度

		int rightH = _Height(root->_right); // 求当前根的右子树高度

		//AVL 树在插入或删除结点时会更新平衡因子,若平衡因子更新出错会导致树的平衡判断失误,
		//这里通过比较计算出的高度差(即理论平衡因子 rightH - leftH )与结点记录的平衡因子 _bf 是否相等,
		//若不相等,说明平衡因子更新错误,该二叉树不是 AVL 树,输出错误信息并返回 false 说明该二叉树不是 AVL 树。
		if (rightH - leftH != root->_bf)
		{
			//输出平衡因子更新异常的结点的键信息,方便调试定位问题
			cout << root->_kv.first << "结点平衡因子异常" << endl;

			//平衡因子不匹配,该树不是 AVL 树
			return false;
		}

		return abs(leftH - rightH) < 2 //判断当前树(根结点)的左右子树高度差是否不超过 1。
			&& _IsBalance(root->_left)//递归判断左子树是否为 AVL 树(平衡树)
			&& _IsBalance(root->_right); //递归判断右子树是否为 AVL 树(平衡树)
	}

private:
	//指向 AVL 树的根结点,初始化为空指针
	Node* _root = nullptr;
};



二、应用:判断二叉树是否是平衡树

LCR 176. 判断是否为平衡二叉树 - 力扣(LeetCode)

110. 平衡二叉树 - 力扣(LeetCode)

class Solution 
{
public:
	int Height(TreeNode* root)
	{
		if (root == NULL)
			return 0;

		int leftH = Height(root->left);
		int rightH = Height(root->right);

		return leftH > rightH ? leftH + 1 : rightH + 1;
	}

    //判断二叉树是否是平衡树的思路:若二叉树的每个子树(结点)的
    //左右子树高度差不超过1,则该二叉树就是平衡树。
	bool isBalanced(TreeNode* root)
	{
		if (root == NULL)
		{
			return true;
		}

		int leftH = Height(root->left);
		int rightH = Height(root->right);

		return abs(leftH - rightH) < 2
			&& isBalanced(root->left)
			&& isBalanced(root->right);
	}
};

在这些题目中,通过递归计算每个结点的左右子树高度差,并进行比较判断。如果在递归过程中,任何一个结点的左右子树高度差超过 1,则说明该二叉树不是平衡树;只有当所有结点都满足高度差条件时,二叉树才是平衡树。这种解题思路与判断 AVL 树中判断平衡树的步骤是一致的,都是基于树的平衡性原理,通过对每个结点的细致检查来确定整棵树的性质。

三、AVL 树 Insert 接口测试

AVL 树是一种自平衡二叉搜索树,在对其 Insert 接口进行测试时,主要关注功能正确性和性能表现,特别是插入操作对树平衡性的影响以及平衡因子的更新情况。

1.Insert 功能测试

(1)测试目的:找出 4 种旋转类型(左单旋、右单旋、左右双旋、右左双旋 )中,哪种旋转会导致某些结点的平衡因子更新出错。

(2)测试数据

常规场景1:{16, 3, 7, 11, 9, 26, 18, 14, 15}

特殊场景2:{4, 2, 6, 1, 3, 5, 15, 7, 16, 14}

(3)测试原理

在 AVL 树中插入结点时,为维持树的平衡,可能会触发左单旋、右单旋、左右双旋、右左双旋这 4 种类型的旋转操作。插入操作完成后,通过调用IsBalance函数判断树是否平衡。若树出现不平衡的情况,就需要确定是哪个结点插入导致的问题。具体做法是,在插入每个结点后都调用IsBalance函数检查树的平衡性并输出结果,通过观察输出结果,能够快速定位到引发平衡问题的插入结点。

(4)测试代码

确定问题结点后,有两种方式(如代码中所示)来触发该结点插入时引发的旋转类型,进而检查对应旋转代码中平衡因子更新的逻辑是否存在问题。

void Test_AVLTree1()
{
	//测试数据数组,这里选择{ 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 }作为测试数据,用于对AVLTree的Insert接口进行功能测试
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };

	AVLTree<int, int> t1;
	for (auto e : a)
	{
		//当我们调用IsBalance函数判断树的平衡性后,若发现有结点插入导致相关结点平衡因子更新错误,
		//此时就需要重新插入该结点来触发导致平衡因子更新错误的旋转类型,以便检查对应旋转代码中平衡因子更新的逻辑。
		//当确定问题结点后,有两种方式可触发相关旋转类型:

		//方法1:若确定是14结点插入导致平衡因子更新错误,可编写if (e == 14)语句。
		//在该语句内部设置断点后,按Fn5(不同编译器可能按键不同,一般是调试进入函数的快捷键 ),
		//程序会跳转到该if语句内,此时e = 14,接着执行t1.Insert(make_pair(e, e))就会插入14结点,
		//从而触发可能导致平衡因子更新错误的旋转类型。
		//这里int x = 0;并非完全无作用,在一些编译器中,若if语句块为空且设置断点,
		//按调试快捷键时可能无法正常进入该语句块,所以此变量起到占位作用,使调试能正常进行。
		//if (e == 14)
		//{
		//    int x = 0; 
		//}

		//方法2:在Insert函数之前设置条件断点,断点设置的条件为e == 14。当插入元素为14时,程序会暂停,
		//这样能方便观察插入14后触发的旋转操作类型,进而检查对应旋转代码中平衡因子更新逻辑是否有误。
		t1.Insert(make_pair(e, e));
		//每插入一个结点,调用IsBalance函数检查树是否平衡,并输出结果。
		//通过观察输出结果,可快速定位是哪个结点的插入引发了平衡问题。
		cout << e << "插入:" << t1.IsBalance() << endl;
	}

	t1.InOrder();
	// 再次检查树在插入所有结点后的平衡性,并输出结果,验证最终状态下树是否平衡。
	cout << t1.IsBalance() << endl;
}

示例问题分析

以插入节点 14 为例,插入该节点后可能导致 6 结点平衡因子异常。比如在右左双旋(RotateRL )操作中,可能由于代码逻辑问题,对相关结点(如subRparentsubRL )的平衡因子更新错误。在正确的右左双旋操作后,某些结点的平衡因子不应被默认更新为 0,而应根据实际旋转情况和结点位置来准确确定。

此时,可以通过上述设置条件断点的方式,在插入 14 时暂停程序,查看触发的旋转操作及相关变量值,从而排查平衡因子更新逻辑的错误。具体来说,当使用条件断点(条件为e == 14)时,程序在插入 14 结点时会暂停,此时可以详细观察旋转操作的具体过程,以及各个结点平衡因子的更新情况,进而找出平衡因子更新逻辑中存在的问题。

2.Insert 性能测试

测试目的:通过插入大量随机数,判断 Insert 接口在进行插入操作后能否保持树的平衡,评估其维持树平衡的性能。

测试原理:向 AVL 树中插入大量随机数,模拟实际应用中频繁插入数据的场景。插入完成后,通过调用IsBalance函数判断树是否平衡,同时输出树的高度。若树能保持平衡且树高在合理范围内,说明 Insert 接口在维持树平衡方面性能较好;若出现不平衡情况,则说明接口存在问题,需要进一步排查插入过程中旋转操作及平衡因子更新等逻辑是否正确。

测试代码

void Test_AVLTree2()
{
    //设置随机数种子,以当前时间为种子,保证每次运行产生不同随机序列
    srand(time(0)); 
    const size_t N = 5000000;

    AVLTree<int, int> t;
    for (size_t i = 0; i < N; ++i)
    {
        //生成随机数并插入到AVL树中
        size_t x = rand() + i; 
        t.Insert(make_pair(x, x));

        //在每次插入后检查树是否平衡,实时监控平衡情况
        //cout << t.IsBalance() << endl; 
    }

    //中序遍历输出树中结点,查看树的结构情况
    //t.Inorder(); 

    //插入完成后,检查树是否平衡
    cout << t.IsBalance() << endl; 

    //输出树的高度,辅助评估树的结构和性能
    cout << t.Height() << endl; 
}

AVL树 查找Find

在 AVL 树中,查找操作与普通二叉搜索树的查找操作思路是一样的,都是基于二叉搜索树的性质(左子树的键值小于根结点的键值,右子树的键值大于根结点的键值)来进行查找。


template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//Find查找函数:用于在AVL树中查找键值为key的结点
	//该函数的设计基于AVL树作为二叉搜索树的特性,即左子树的所有结点键值小于根结点键值,
	//右子树的所有结点键值大于根结点键值,以此来逐步缩小查找范围。
	
	//参数 - key:要查找的键值,使用const K&类型,(传引用传参)既可以避免对传入的键值进行不必要的拷贝,
	//提高效率,又能保证在函数内部不会意外修改传入的键值。
	
	//返回值:如果在AVL树中成功找到键值为key的结点,则返回指向该结点的指针,通过该指针可以进一步访问结点的其他信息,
	//如结点的键值对、左右子结点指针、父结点指针等;如果遍历完整个AVL树都未找到键值为key的结点,则返回nullptr,
	//表示未找到目标结点。
	Node* Find(const K& key)
	{
		//将根结点的指针赋值给cur,从根结点开始进行查找操作。
		//根结点是AVL树的起始点,后续的查找过程将基于根结点逐步深入到子树中。
		Node* cur = _root;

		//只要cur不为空,就继续进行查找。
		//因为当cur为空时,说明已经遍历到了树的末端(遇到空结点),
		//且在遍历过程中没有找到目标结点,此时查找结束。
		while (cur)
		{
			//如果当前结点cur的键值小于要查找的键值key,
			//根据AVL树作为二叉搜索树的性质,目标结点的键值更大,
			//所以目标结点应该在当前结点cur的右子树中。
			//因此,将cur指针更新为当前结点的右子结点,继续在右子树中查找。
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			//如果当前结点cur的键值大于要查找的键值key,
			//同样根据AVL树作为二叉搜索树的性质,目标结点的键值更小,
			//所以目标结点应该在当前结点cur的左子树中。
			//于是,将cur指针更新为当前结点的左子结点,继续在左子树中查找。
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			//如果当前结点cur的键值等于要查找的键值key,
			//说明已经成功找到了目标结点,直接返回当前结点的指针cur,
			//以便调用者可以通过该指针获取目标结点的相关信息。
			else
			{
				return cur;
			}
		}

		//如果循环结束后,cur变为nullptr,这意味着在整个AVL树的遍历过程中,
		//没有找到键值为key的结点,此时返回nullptr,向调用者表示未找到目标结点。
		return nullptr;
	}

private:
	Node* _root = nullptr;
};

AVL树删除操作Erase — 非递归(循环)版本

一、AVL 树删除操作核心概念

AVL 树是一种自平衡的二叉搜索树,其删除操作在遵循二叉搜索树删除逻辑的基础上,还需额外处理平衡因子更新与旋转操作,以确保删除结点后树依然保持平衡状态。最坏情况下,平衡因子更新需从删除结点的父结点一直回溯至根节点。

二、二叉搜索树删除的基本情况

二叉树进阶 - 二叉搜索树_csdn gettoken 二叉树-CSDN博客

注:上面博客有关于二叉搜索树删除Erase详细代码和分析过程。

在进行 AVL 树删除操作前,需先了解二叉搜索树删除的两种基本情况,这是 AVL 树删除操作的基础:

1.删除叶子结点或只有一个孩子的结点(托孤法)

  • 类型 1:当删除结点的左孩子为空(仅有右孩子),且该结点是父结点的左孩子时,将父结点的左指针指向删除结点的右孩子,随后释放要删除的结点。
  • 类型 2:若删除结点的右孩子为空(仅有左孩子),且该结点是父结点的右孩子,需将父结点的右指针指向删除结点的左孩子,再释放该结点。

2.删除有两个孩子的结点(替换法 / 请保姆法)

  • 选定保姆结点:选择删除结点右子树的最小结点(最左结点) 或 左子树的最大结点(最右结点)作为替换结点(保姆结点)。
  • 替换值:将删除结点的值替换为保姆结点的值。
  • 删除保姆结点:因二叉搜索树不允许数据冗余,替换后需删除保姆结点。由于保姆结点必然是叶子结点或仅有一个孩子的结点,删除时可参照上述托孤方法,并在删除前妥善调整其与父结点的指针指向关系,从而间接完成对原删除结点的删除。

三、AVL 树删除操作的实现思路

1.思路概述
AVL 树删除操作主要分为三个步骤:定位并删除目标结点、回溯更新平衡因子、判断并执行旋转操作。通过这三个步骤,在删除目标结点的同时,确保 AVL 树的平衡特性得以维持。

2.具体步骤

(1)步骤 1:定位并删除目标结点
采用二叉搜索树的查找逻辑定位目标结点。根据待删除结点的子结点数量,沿用二叉搜索树的删除逻辑,采用托孤法(删除叶子结点或单孩子结点)或替换法(删除双孩子结点)移除目标结点。删除后记录其父结点作为后续回溯更新平衡因子的起点。

(2)步骤 2:回溯更新平衡因子
从删除结点的父结点开始向上逐层回溯。依据删除结点在其父结点的位置(左子树或右子树)更新祖先结点的平衡因子:若从左子树删除,祖先结点的平衡因子 bf++;若从右子树删除,平衡因子 bf--。在更新过程中,根据平衡因子的变化动态调整回溯策略。

(3)步骤 3:判断并执行旋转操作

  • 平衡状态(±1):若更新后平衡因子变为 1 或 -1,说明原平衡因子为 0,删除操作未改变子树高度,不会影响上层子树平衡,此时直接终止回溯,无需进一步调整。
  • 临界状态(0):若更新后平衡因子变为 0,表明删除操作导致子树高度变化,且该变化会向上层传递,需继续向上回溯,重复步骤 2 更新祖先结点的平衡因子。
  • 失衡状态(±2):若更新后平衡因子变为 2 或 -2,则当前子树已失衡,需立即执行相应的旋转操作(左旋、右旋、先左旋后右旋、先右旋后左旋)恢复平衡。旋转会调整子树结构并更新结点平衡因子,因删除操作的旋转会降低子树高度,可能改变原树整体高度。旋转后,需比较上层子树左右子树高度差与平衡因子值,不等则继续回溯更新上层平衡因子,相等则停止回溯 。

四、AVL 树删除操作代码实现过程中的问题及解决方法

1.问题一:除结点后父结点及祖先结点平衡因子更新问题

(1)问题描述:

在 AVL 树删除操作中,无论待删除结点的子结点数量为 0、1 还是 2,在实际删除前都需要将其孩子结点重新连接至父结点(即 “托孤” 操作) 。这一操作会改变树的局部连接关系,导致删除结点后,无法直接通过传统方式(依据删除结点在父结点中的位置更新平衡因子)判断删除结点是父结点的左孩子还是右孩子,进而无法正确更新父结点的平衡因子。

传统更新平衡因子的方式是:若从父结点的左子树删除结点,则父结点的平衡因子 bf++;若从右子树删除,则 bf--。为解决上述问题,一种直观的思路是在删除结点前记录其孩子结点 delechild,删除后通过判断父结点 parent 的左 / 右孩子是否为 delechild,来确定父结点哪一侧子树高度减少,从而更新平衡因子。但该方法存在局限性:当删除结点没有孩子(即叶子结点)时, delechild 为空指针。若父结点仅有该叶子结点一个孩子,删除后父结点的左右子树指针必有一个为空,此时使用 parent->left == delechild 或 parent->right == delechild 判断并更新平衡因子,会因空指针冲突导致判断失效。这意味着该方法仅适用于删除结点有一个孩子的情况,无法处理叶子结点的删除场景 。

错误写法: dechild指向删除结点的孩子。(注:无论是托孤 或是 请保姆,最终要删除结点至多只有一个孩子。对于请保姆法来说,最终要删除的结点是保姆结点,而保姆结点至多只有一个孩子)

(2)解决方式:为了准确且通用地更新父结点的平衡因子,采用计算父结点左右子树高度差的方式。具体实现代码如下。

int plh = (parent->_left) ? _Height(parent->_left) : 0;  //计算父结点左子树高度,若左子树为空则高度为0
int prh = (parent->_right) ? _Height(parent->_right) : 0;  //计算父结点右子树高度,若右子树为空则高度为0
int pbf = prh - plh;  //计算高度差作为父结点平衡因子
parent->_bf = pbf;  //更新父结点平衡因子

通过这种方式,无论删除结点是否有孩子,都能根据父结点实际的左右子树高度差准确更新平衡因子,避免了因结点连接关系变化和空指针冲突导致的更新错误,为后续判断树是否失衡及执行旋转操作提供了可靠依据 。

2.问题二:AVL 树删除操作中特殊单旋触发情况详解

(1)AVL 树删除导致失衡的原理

在 AVL 树的删除操作里,通常会选择删除较矮子树的结点。这是因为删除较矮子树的结点会使原本左右子树高度差为 1 的树,其高度差进一步扩大超过 1,从而引发树的失衡。这里的失衡可能出现在整棵 AVL 树上,也可能仅影响其中的某棵子树。

(2)原树较高子树的状态及对应树状结构

原树较高子树存在两种状态,这两种状态会导致在删除较矮子树结点后,原树出现不同的失衡树状结构:

较高子树左右高度差为 1

  • 右右树状 - 左单旋:当 AVL 树较矮子树的结点被删除后,树发生失衡,此时失衡树的右子树比左子树高,并且失衡树右孩子的右子树也比其左子树高。
  • 左左树状 - 右单旋:若删除较矮子树结点使树失衡,失衡树的左子树会比右子树高,同时失衡树左孩子的左子树比其右子树高。

较高子树左右高度差为 0

  • 右平树状 - 左单旋:删除较矮子树结点致使树失衡后,失衡树的右子树比左子树高,不过失衡树的右子树本身是左右子树高度差为 0 的完全平衡树
  • 左平树状 - 右单旋:同样在删除较矮子树结点导致树失衡时,失衡树的左子树比右子树高,而失衡树的左子树是左右子树高度差为 0 的完全平衡树

(3)不同树状结构对应的旋转操作及代码实现

以下是根据不同树状结构判断并执行相应旋转操作的代码:

else if (parent->_bf == 2 || parent->_bf == -2)
{
    Node* pparent = parent->_parent;
    Node* subL = parent->_left;
    Node* subR = parent->_right;

    // 右右树状 - 左单旋
    if (parent->_bf == 2 && subR->_bf == 1)
    {
        RotateL(parent);
    }
    // 左左树状 - 右单旋
    else if (parent->_bf == -2 && subL->_bf == -1)
    {
        RotateR(parent);
    }
    // 左右树状 - 左右双旋
    else if (parent->_bf == -2 && subL->_bf == 1)
    {
        RotateLR(parent);
    }
    // 右左树状 - 右左双旋
    else if (parent->_bf == 2 && subR->_bf == -1)
    {
        RotateRL(parent);
    }
    // 右平树状 - 左单旋
    else if (parent->_bf == 2 && subR->_bf == 0)
    {
        RotateL(parent);
    }
    // 左平树状 - 右单旋
    else if (parent->_bf == -2 && subL->_bf == 0)
    {
        RotateR(parent);
    }
    else
    {
        assert(false);
    }
}

代码解释

  • parent->_bf 代表当前失衡子树的根结点的平衡因子。当 parent->_bf 为 2 时,意味着右子树较高;为 -2 时,则表示左子树较高。
  • subL->_bf 和 subR->_bf 分别是失衡子树根结点的左子结点和右子结点的平衡因子,用于进一步判断子树的具体形态。
  • 根据不同的平衡因子组合,程序能够准确判断树的形状,并执行对应的旋转操作,包括左单旋 RotateL、右单旋 RotateR、左右双旋 RotateLR 以及右左双旋 RotateRL。如果出现不符合上述任何一种情况的平衡因子组合,代码会触发 assert(false) 来表明出现异常。

3.问题三:AVL 树删除操作中旋转对树高度的影响及判断机制

(1)插入 Insert 与 删除 Erase 操作引发失衡及旋转处理的差异

①在失衡 AVL 树中,旋转操作的核心目的是通过调整失衡 AVL 树的子树高度,使失衡 AVL 树 恢复平衡。插入操作和删除操作在引发 AVL 树失衡以及旋转处理方面存在显著不同。

②插入操作是向 AVL 树中添加新结点,通常新结点会被插入到 AVL 树的较高子树中,这会使该子树高度进一步增加,从而导致 AVL 树失衡。不过,插入操作引发的 AVL 树失衡可以通过旋转操作降低高度,最终让 AVL 树恢复到插入前的高度。也就是说,旋转使得插入前后,AVL 树整体高度没发生变化 (注:该 AVL 树可能是整棵 AVL 树,或者是整棵 AVL 树中的某棵子 AVL 树)。

③与之不同,删除操作是从 AVL 树中移除结点。通常情况下,删除的是 AVL 树中较矮子树的结点,这会使较矮子树与较高子树的高度差超过 1,进而引发 AVL 树失衡。为使失衡的 AVL 树(可能是整棵树,也可能是树中的某棵子树)恢复平衡,需要通过旋转操作降低较高子树的高度,使其与较矮子树达到高度均衡。

由于这种旋转会降低子树高度,进而影响失衡 AVL 树的整体高度,很可能导致原树(即失衡前的 AVL 树)高度降低。若原树是整棵 AVL 树中的某棵子 AVL 树,旋转导致的原树高度降低会影响到上一层子树。此时,需要回溯更新上一层子树根结点的平衡因子,以确保整棵 AVL 树的平衡状态得以维持。

(2)旋转后判断对上层子树高度影响的必要性

在 AVL 树的删除操作中,由于旋转操作会降低子树高度,可能导致原树(失衡前的 AVL 树)高度降低。当原树为整棵 AVL 树中的某棵子 AVL 树时,其高度变化会直接影响上一层子树。若不判断旋转后子树对上层子树高度的影响,未及时更新上层子树的平衡因子,会导致整棵 AVL 树的平衡状态被破坏,使得查找、插入等操作的时间复杂度无法维持在 O (log n) ,严重影响 AVL 树的性能和数据操作的正确性。因此,在完成旋转操作后,必须判断当前旋转子树是否会对其上一层子树的高度产生影响,这是确保整个 AVL 树平衡状态得以正确维护的关键步骤。

(3)判断方法及代码实现

判断当前旋转子树是否影响上一层子树高度,需通过比较上一层子树的左右子树高度差与该子树的平衡因子值。具体逻辑如下:

  • 两者不相等:说明旋转操作改变了上一层子树的高度分布,影响了上一层子树的平衡状态,需要继续向上回溯,更新上层子树的平衡因子,确保整棵树的平衡。
  • 两者相等:表明旋转操作未对上层子树高度造成影响,树的平衡状态在该层未被破坏,此时可以停止回溯,无需进一步处理。

//注:pparent 是当前处理的结点 parent 的父结点,在 AVL 树删除操作里,
//用于判断旋转后是否影响上一层子树高度,决定是否向上回溯更新平衡因子。
//若 pparent 为空,说明当前子树是整棵树,无需更新上一层平衡因子。
if (pparent) 
{
    //注:pparent表示当前parent的父亲结点
    
    //计算上一层子树左子树高度,若左子树存在则获取其高度,否则为0
    int pplh = (pparent->_left) ? _Height(pparent->_left) : 0;
    //计算上一层子树右子树高度,若右子树存在则获取其高度,否则为0
    int pprh = (pparent->_right) ? _Height(pparent->_right) : 0;

    //计算上一层子树左右子树高度差
    int ppbf = pprh - pplh;

    //若计算所得高度差与上一层子树根结点记录的平衡因子值不相等
    if (ppbf != pparent->_bf) 
    {
        //将当前处理结点更新为上一层子树根结点,继续向上处理
        parent = pparent;
    }
    else 
    {
        //旋转未影响上一层子树高度,停止回溯
        break;
    }
}
else 
{
    //不存在上一层子树(已达根结点或无父结点),停止回溯
    break;
}

代码中,首先判断父结点pparent是否存在。若存在,分别计算其左右子树高度并得出高度差ppbf,与pparent的平衡因子进行比较,根据比较结果决定是否继续向上处理;若pparent不存在,说明已处理至根结点或无需再向上回溯,直接停止操作。

五、AVL树删除操作Erase 代码实现

 

bool Erase(const K& key)
{
    //用于记录待删除结点的父结点,初始化为空指针
    Node* parent = nullptr;
    //用于遍历树,定位待删除结点,初始化为根结点
    Node* cur = _root;

    //遍历AVL树,查找键值为key的待删除结点
    while (cur)
    {
        if (cur->_kv.first < key)
        {
            //更新parent为当前结点cur,记录当前结点的父结点
            parent = cur;
            //若当前结点的键值小于key,继续在右子树中查找
            cur = cur->_right;
        }
        else if (cur->_kv.first > key)
        {
            //更新parent为当前结点cur,记录当前结点的父结点
            parent = cur;
            //若当前结点的键值大于key,继续在左子树中查找
            cur = cur->_left;
        }
        else
        {
            //情况1:待删除结点左孩子为空 - 删除方法:托孤发
            if (cur->_left == nullptr)
            {
                //若待删除结点为根结点
                if (cur == _root)
                {
                    //将根结点更新为原根结点的右孩子
                    _root = cur->_right;
                    //若新根结点存在,将其_parent父指针置空,确保其为根结点
                    if (_root)
                        _root->_parent = nullptr;
                }
                else
                {
                    //根据parent与cur的指针关系,调整parent的子树指针
                    if (parent->_left == cur)
                    {
                        //若cur删除结点是parent其父结点的左孩子,将parent父结点的左指针指向cur删除结点的右孩子
                        parent->_left = cur->_right;
                    }
                    else
                    {
                        //若cur删除结点是parent父结点的右孩子,将parent父结点的右指针指向cur删除结点的右孩子
                        parent->_right = cur->_right;
                    }
                    //若cur删除结点的右孩子存在,更新其_parent父指针指向其新的父亲parent
                    if (cur->_right)
                        cur->_right->_parent = parent;
                }
                //释放待删除结点的内存,完成删除操作
                delete cur;
            }
            //情况2:待删除结点右孩子为空 - 删除方法:托孤发
            else if (cur->_right == nullptr)
            {
                //若待删除结点为根结点
                if (cur == _root)
                {
                    //将根结点更新为原根结点的左孩子
                    _root = cur->_left;
                    //若新根结点存在,将其parent指针置空,确保其为根结点
                    if (_root)
                        _root->_parent = nullptr;
                }
                else
                {
                    //根据parent与cur的指针关系,调整parent的子树指针
                    if (parent->_left == cur)
                    {
                        //若cur删除结点是parent其父结点的左孩子,将parent父结点的左指针指向cur删除结点的左孩子
                        parent->_left = cur->_left;
                    }
                    else
                    {
                        //若cur删除结点是parent父结点的右孩子,将parent父结点的右指针指向cur删除结点的左孩子
                        parent->_right = cur->_left;
                    }
                    //若cur删除结点的左孩子存在,更新其_parent父指针指向新的父亲parent
                    if (cur->_left)
                        cur->_left->_parent = parent;
                }
                //释放待删除结点的内存,完成删除操作
                delete cur;
            }
            //情况3:待删除结点左右孩子都存在 - 删除方法:请保姆
            else
            {
                //找到待删除结点右子树中的最小结点(最左结点)作为保姆结点minRight,并找保姆结点的父结点pminRight
                Node* pminRight = cur;
                Node* minRight = cur->_right;

                //循环查找右子树中的最小结点(找保姆结点)
                while (minRight->_left)
                {
                    pminRight = minRight;
                    minRight = minRight->_left;
                }
                //用最小结点的(保姆结点)值替换待删除结点的值,实现逻辑删除
                cur->_kv = minRight->_kv;

                //根据minRight与pminRight的指针关系,调整pminRight的子树指针
                if (pminRight->_left == minRight)
                {
                    //若minRight保姆结点是pminRight父结点的左孩子,将pminRight父结点的左指针指向minRight保姆结点的右孩子
                    pminRight->_left = minRight->_right;
                }
                else
                {
                    //若minRight保姆结点是pminRight父结点的右孩子,将pminRight父结点的右指针指向minRight保姆结点的右孩子
                    pminRight->_right = minRight->_right;
                }

                //若minRight保姆结点的右孩子存在,更新其_parent父指针指向新的父亲pminRight
                if (minRight->_right)
                    minRight->_right->_parent = pminRight;

                //更新cur和parent指针,指向实际待删除的minRight保姆结点及其父结点
                cur = minRight;
                parent = cur->_parent;

                //释放minRight保姆结点的内存,完成删除操作
                delete minRight;
            }

            //从删除结点的父结点开始向上回溯,更新平衡因子,以维护AVL树的平衡
            while (parent)
            {
                //计算parent左子树的高度,若左子树为空则高度为0
                int plh = (parent->_left) ? _Height(parent->_left) : 0;
                //计算parent右子树的高度,若右子树为空则高度为0
                int prh = (parent->_right) ? _Height(parent->_right) : 0;
                //计算parent的平衡因子,即右子树高度减去左子树高度
                int pbf = prh - plh;
                //更新parent的平衡因子
                parent->_bf = pbf;

                //平衡状态:parent父结点的平衡因子更新为 1 或 -1,说明原平衡因子为 0,
                //删除操作未改变子树高度,不会影响上层子树平衡,此时直接终止回溯,无需进一步调整。
                if (parent->_bf == 1 || parent->_bf == -1)
                {
                    break;
                }
                //临界状态:parent父结点的平衡因子更新为 0,表明删除操作导致子树高度变化,
                //且该变化会向上层传递,需继续向上回溯,重复步骤 2 更新祖先结点的平衡因子。
                else if (parent->_bf == 0)
                {
                    parent = parent->_parent;
                }
                //失衡状态:parent父结点的平衡因子更新为 2 或 -2,则当前子树已失衡,需立即执行相应
                //的旋转操作(左旋、右旋、先左旋后右旋、先右旋后左旋)恢复平衡。旋转会调整子树结构并
                //更新结点平衡因子,因删除操作的旋转会降低子树高度,可能改变原树整体高度。旋转后,
                //需比较上层子树左右子树高度差与平衡因子值,不等则继续回溯更新上层平衡因子,相等则停止回溯 。
                else if (parent->_bf == 2 || parent->_bf == -2)
                {
                    //记录parent的父结点pparent,用于判断旋转操作是否影响上一层子树高度
                    Node* pparent = parent->_parent;

                    //记录parent的左子结点
                    Node* subL = parent->_left;
                    //记录parent的右子结点
                    Node* subR = parent->_right;

                    //右右树状 - 左单旋:右子树高且右子树的右子树更高,执行左单旋操作来恢复平衡
                    if (parent->_bf == 2 && subR->_bf == 1)
                    {
                        RotateL(parent);
                    }
                    //左左树状 - 右单旋:左子树高且左子树的左子树更高,执行右单旋操作来恢复平衡
                    else if (parent->_bf == -2 && subL->_bf == -1)
                    {
                        RotateR(parent);
                    }
                    //左右树状 - 左右双旋:左子树高且左子树的右子树更高,执行先左后右双旋操作来恢复平衡
                    else if (parent->_bf == -2 && subL->_bf == 1)
                    {
                        RotateLR(parent);
                    }
                    //右左树状 - 右左双旋:右子树高且右子树的左子树更高,执行先右后左双旋操作来恢复平衡
                    else if (parent->_bf == 2 && subR->_bf == -1)
                    {
                        RotateRL(parent);
                    }
                    //右平树状 - 左单旋:右子树高且右子树为完全平衡树,执行左单旋操作来恢复平衡
                    else if (parent->_bf == 2 && subR->_bf == 0)
                    {
                        RotateL(parent);
                    }
                    //左平树状 - 右单旋:左子树高且左子树为完全平衡树,执行右单旋操作来恢复平衡
                    else if (parent->_bf == -2 && subL->_bf == 0)
                    {
                        RotateR(parent);
                    }
                    else
                    {
                        //若出现不符合上述任何一种情况的平衡因子组合,触发断言错误
                        assert(false);
                    }

                    //判断旋转操作是否影响上一层子树高度
                    if (pparent)
                    {
                        //计算pparent左子树的高度,若左子树为空则高度为0
                        int pplh = (pparent->_left) ? _Height(pparent->_left) : 0;
                        //计算pparent右子树的高度,若右子树为空则高度为0
                        int pprh = (pparent->_right) ? _Height(pparent->_right) : 0;

                        //计算pparent的平衡因子,即右子树高度减去左子树高度
                        int ppbf = pprh - pplh;

                        //若pparent的实际平衡因子与计算值不等,说明旋转影响了上一层子树,继续向上回溯
                        if (ppbf != pparent->_bf)
                        {
                            parent = pparent;
                        }
                        else
                        {
                            //否则说明旋转未影响上一层子树,停止回溯
                            break;
                        }
                    }
                    else
                    {
                        //若pparent为空,说明当前子树为整棵树,停止回溯
                        break;
                    }
                }
                else
                {
                    //若出现其他异常的平衡因子值,触发断言错误
                    assert(false);
                }
            }

            //成功删除结点,返回true
            return true;
        }
    }

    //未找到待删除结点,返回false
    return false;
}

AVL树性能

1.AVL 树的结构特性

AVL 树是一种高度平衡的二叉搜索树,其核心特性在于每个结点的左右子树高度差的绝对值不超过 1。从树的形态上看,AVL 树近似于完全二叉树,但与之存在本质区别:完全二叉树要求最后一层结点从左至右连续排列,而 AVL 树仅保证高度平衡,允许最后一两层存在结点缺失 。这种平衡特性使得 AVL 树在结构上较为紧凑,为其性能表现奠定基础。

2.AVL 树的查询性能

由于 AVL 树的高度平衡特性,其高度始终保持在log​N级别(N 为树中结点个数)。基于此,AVL 树在查询操作上具备高效性,时间复杂度稳定为O(logN)。这意味着,随着数据规模的增长,查询所需的操作次数以对数级别缓慢增加,能快速定位目标结点,适用于频繁查询的场景。

3.AVL 树的结构修改性能

在插入和删除等结构修改操作方面,AVL 树的性能表现相对较差。插入新结点时,为维持树的高度平衡,可能需要进行多次旋转操作来调整树的结构;删除结点时,影响范围可能向上传递,极端情况下甚至需要旋转操作持续至根结点,导致大量的结构调整开销。因此,在数据频繁变动的场景下,AVL 树因结构维护成本较高,并不适用。

4.AVL 树的适用场景总结

  • 适合场景:若应用场景中数据静态性强(数据量基本固定,仅以查询操作为主),且对查询效率要求极高,AVL 树凭借其稳定的O(logN)查询复杂度,是理想选择。
  • 不适合场景:当数据结构需要频繁进行插入、删除等修改操作时,由于 AVL 树维护平衡的成本较高,会导致整体性能下降,此时应考虑其他更适合动态数据的结构 。

AVL树模拟实现的整个工程代码

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

template<class K, class V>
struct AVLTreeNode
{
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    AVLTreeNode<K, V>* _parent;
    pair<K, V> _kv;
    int _bf;

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

template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    Node* Find(const K& key)
    {
        Node* cur = _root;

        while (cur)
        {
            if (cur->_kv.first < key)
            {
                cur = cur->_right;
            }
            else if (cur->_kv.first > key)
            {
                cur = cur->_left;
            }
            else
            {
                return cur;
            }
        }

        return nullptr;
    }

    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->_left = cur;
        }
        else
        {
            parent->_right = cur;
        }
        cur->_parent = parent;

        while (parent)
        {
            if (cur == parent->_right)
            {
                parent->_bf++;
            }
            else
            {
                parent->_bf--;
            }

            if (parent->_bf == 1 || parent->_bf == -1)
            {
                parent = parent->_parent;
                cur = cur->_parent;
            }
            else if (parent->_bf == 0)
            {
                break;
            }
            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;
    }

    bool Erase(const K& key)
    {
        Node* parent = nullptr;
        Node* cur = _root;

        while (cur)
        {
            if (cur->_kv.first < key)
            {
                parent = cur;
                cur = cur->_right;
            }
            else if (cur->_kv.first > key)
            {
                parent = cur;
                cur = cur->_left;
            }
            else
            {
                if (cur->_left == nullptr)
                {
                    if (cur == _root)
                    {
                        _root = cur->_right;

                        if (_root)
                            _root->_parent = nullptr;
                    }
                    else
                    {
                        if (parent->_left == cur)
                        {
                            parent->_left = cur->_right;
                        }
                        else
                        {
                            parent->_right = cur->_right;
                        }

                        if (cur->_right)
                            cur->_right->_parent = parent;
                    }

                    delete cur;
                }
                else if (cur->_right == nullptr)
                {
                    if (cur == _root)
                    {
                        _root = cur->_left;

                        if (_root)
                            _root->_parent = nullptr;
                    }
                    else
                    {
                        if (parent->_left == cur)
                        {
                            parent->_left = cur->_left;
                        }
                        else
                        {
                            parent->_right = cur->_left;
                        }

                        if (cur->_left)
                            cur->_left->_parent = parent;
                    }

                    delete cur;
                }
                else
                {
                    Node* pminRight = cur;
                    Node* minRight = cur->_right;

                    while (minRight->_left)
                    {
                        pminRight = minRight;
                        minRight = minRight->_left;
                    }

                    cur->_kv = minRight->_kv;

                    if (pminRight->_left == minRight)
                    {
                        pminRight->_left = minRight->_right;
                    }
                    else
                    {
                        pminRight->_right = minRight->_right;
                    }

                    if (minRight->_right)
                        minRight->_right->_parent = pminRight;

                    cur = minRight;
                    parent = cur->_parent;

                    delete minRight;
                }

                while (parent)
                {
                    int plh = (parent->_left) ? _Height(parent->_left) : 0;
                    int prh = (parent->_right) ? _Height(parent->_right) : 0;
                    int pbf = prh - plh;

                    parent->_bf = pbf;

                    if (parent->_bf == 1 || parent->_bf == -1)
                    {
                        break;
                    }
                    else if (parent->_bf == 0)
                    {
                        parent = parent->_parent;
                    }
                    else if (parent->_bf == 2 || parent->_bf == -2)
                    {
                        Node* pparent = parent->_parent;
                        Node* subL = parent->_left;
                        Node* subR = parent->_right;

                        if (parent->_bf == 2 && subR->_bf == 1)
                        {
                            RotateL(parent);
                        }
                        else if (parent->_bf == -2 && subL->_bf == -1)
                        {
                            RotateR(parent);
                        }
                        else if (parent->_bf == -2 && subL->_bf == 1)
                        {
                            RotateLR(parent);
                        }
                        else if (parent->_bf == 2 && subR->_bf == -1)
                        {
                            RotateRL(parent);
                        }
                        else if (parent->_bf == 2 && subR->_bf == 0)
                        {
                            RotateL(parent);
                        }
                        else if (parent->_bf == -2 && subL->_bf == 0)
                        {
                            RotateR(parent);
                        }
                        else
                        {
                            assert(false);
                        }

                        if (pparent)
                        {
                            int pplh = (pparent->_left) ? _Height(pparent->_left) : 0;
                            int pprh = (pparent->_right) ? _Height(pparent->_right) : 0;
                            int ppbf = pprh - pplh;
                            if (ppbf != pparent->_bf)
                            {
                                parent = pparent;
                            }
                            else
                            {
                                break;
                            }
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        assert(false);
                    }
                }

                return true;
            }
        }

        return false;
    }

    void PreOrder()
    {
        _PreOrder(_root);
        cout << endl;
    }

    void InOrder()
    {
        _InOrder(_root);
        cout << endl;
    }

    bool IsBalance()
    {
        return _IsBalance(_root);
    }

    int Height()
    {
        return _Height(_root);
    }

private:
    int _Height(Node* root)
    {
        if (root == nullptr)
            return 0;

        int leftH = _Height(root->_left);
        int rightH = _Height(root->_right);

        return leftH > rightH ? leftH + 1 : rightH + 1;
    }

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

        int leftH = _Height(root->_left);
        int rightH = _Height(root->_right);

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

        return abs(leftH - rightH) < 2
            && _IsBalance(root->_left)
            && _IsBalance(root->_right);
    }

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

        parent->_right = subRL;
        if (subRL)
            subRL->_parent = parent;

        Node* ppnode = parent->_parent;

        subR->_left = parent;
        parent->_parent = subR;

        if (ppnode == nullptr)
        {
            _root = subR;
            _root->_parent = nullptr;
        }
        else
        {
            if (ppnode->_left == parent)
            {
                ppnode->_left = subR;
            }
            else
            {
                ppnode->_right = subR;
            }

            subR->_parent = ppnode;
        }

        if (subR->_bf == 1)
        {
            parent->_bf = subR->_bf = 0;
        }
        else if (subR->_bf == 0)
        {
            parent->_bf = 1;
            subR->_bf = -1;
        }
    }

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

        parent->_left = subLR;
        if (subLR)
            subLR->_parent = parent;

        Node* ppnode = parent->_parent;

        subL->_right = parent;
        parent->_parent = subL;

        if (parent == _root)
        {
            _root = subL;
            _root->_parent = nullptr;
        }
        else
        {
            if (ppnode->_left == parent)
            {
                ppnode->_left = subL;
            }
            else
            {
                ppnode->_right = subL;
            }
            subL->_parent = ppnode;
        }

        if (subL->_bf == -1)
        {
            subL->_bf = parent->_bf = 0;
        }
        else if (subL->_bf == 0)
        {
            parent->_bf = -1;
            subL->_bf = 1;
        }
    }

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

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

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

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

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

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

    void _PreOrder(Node* root)
    {
        if (root == nullptr)
            return;
        cout << root->_kv.first << " ";

        _PreOrder(root->_left);
        _PreOrder(root->_right);
    }

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

        _InOrder(root->_left);
        cout << root->_kv.first << " ";
        _InOrder(root->_right);
    }
private:
    Node* _root = nullptr;
};    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值