【C++STL】平衡二叉树(AVL树)

一.为什么要有平衡二叉树

二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如图。

依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)。

在此二叉搜索树中查找元素 5需要查找 5 次。

二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 A,将其改为下图的方式存储,查找元素 5 时只需比较 3 次,查找效率提升一倍。

可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。

这种左右子树的高度相差不超过 1 的树为平衡二叉树。

二. 什么是AVL树

首先,AVL树是一棵二叉搜索树。

为了保证二叉搜索树的性能,规定在插入和删除节点时,要保证任意节点的左、右子树高度差的绝对值不超过1,这样的二叉搜索树称为平衡二叉树(简称AVL树)。

也就是说:AVL树是在二叉搜索树的基础上加入了平衡性的限制。

其中节点左子树与右子树的高度差 定义为该节点的平衡因子 (一般是左子树的高度减去右子树的高度,当然反过来也是可以的,下面我举的例子全都是左子树的高度减去右子树的高度

由此可知,平衡二叉树中,每一个节点的平衡因子只可能是-1、0或1。

如下图所示:节点上方的数字表示平衡因子。

左图是一棵平衡二叉树,右图不是一棵平衡二叉树

下面的平衡因子是右子树的高度减去左子树的高度

2.1.平衡因子

AVL 树的每个节点都有一个平衡因子(Balance Factor),其定义为:

  • 平衡因子 = 左子树高度 - 右子树高度
  • 或者
  • 平衡因子 = 右子树高度 - 左子树高度

两种情况哪一种都是可以的。

AVL 树要求每个节点的平衡因子的绝对值不超过 1,也就是平衡因子只能取 -1、0 或 1。

下面3个例子的平衡因子 = 左子树高度 - 右子树高度。

示例 1:平衡因子都为 0 的 AVL 树

cpp 复制代码
         5(0)
       /     \
    3(0)      7(0)
   /   \     /   \
2(0)   4(0) 6(0)  8(0)

各节点的平衡因子计算:

  • 节点 2:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
  • 节点 4:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
  • 节点 3:左子树(节点 2)高度为 1,右子树(节点 4)高度为 1,平衡因子 = 1 - 1 = 0。
  • 节点 6:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
  • 节点 8:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
  • 节点 7:左子树(节点 6)高度为 1,右子树(节点 8)高度为 1,平衡因子 = 1 - 1 = 0。
  • 节点 5:左子树(以节点 3 为根)高度为 2,右子树(以节点 7 为根)高度为 2,平衡因子 = 2 - 2 = 0。

示例 2:包含平衡因子为 -1 和 1 的 AVL 树

cpp 复制代码
         8(0)
       /     \
    4(1)      12(-1)
   /             \
2(0)             14(0)

各节点的平衡因子计算:

    • 节点 2:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
    • 节点 4:左子树(节点 2)高度为 1,右子树高度为 0,平衡因子 = 1 - 0 = 1。
    • 节点 14:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
    • 节点 12:左子树高度为 0,右子树(节点 14)高度为 1,平衡因子 = 0 - 1 = -1。
    • 节点 8:左子树(以节点 4 为根)高度为 2,右子树(以节点 12 为根)高度为 2,平衡因子 = 2 - 2 = 0。

示例 3:平衡因子为 -2 的 BST 树,不是 AVL 树

cpp 复制代码
   10(-2)
  /    \
5(0)    20(0)
        /  \
      15(0) 30(0)

各节点的平衡因子计算:

    • 节点 5:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
    • 节点 15:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
    • 节点 30:左子树高度为 0,右子树高度为 0,平衡因子 = 0 - 0 = 0。
    • 节点 20:左子树(节点 15)高度为 1,右子树(节点 30)高度为 1,平衡因子 = 1 - 1 = 0。
    • 节点 10:左子树(节点 5)高度为 1,右子树(以节点 15 为根的子树和节点 20 及其子树共同构成右子树部分)高度为 3,平衡因子 = 1 - 3 = -2。

2.2.最小不平衡子树

在⼆叉搜索树中插⼊新结点之后,插⼊路径的点中,可能存在很多平衡因⼦的绝对值⼤于 的,此时 找到距离插⼊结点最近的不平衡的点以这个点为根的⼦树就是最⼩不平衡⼦树。如下图:

右图的70就是距离插入的节点67的最近的 不平衡结点。

三.实现AVL树

首先我们需要先将这个结点类型给定义出来

这个其实也没什么好说的,就很简单

cpp 复制代码
// AVL树节点结构体模板
template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;                // 键值对,用pair存储
	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)
	{}
};

然后,我们就可以定义出这么一个根结点出来了!!!

cpp 复制代码
// AVL树类模板
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
......

private:
	Node* _root = nullptr;   // 根节点指针,初始化为空
};

四.插入操作

接下来我将讲解插入操作的核心要素

它从新插入节点的父节点开始,沿着祖先链向上检查,一旦发现某个节点失衡(平衡因子绝对值达到2),就通过旋转调整该子树,使整棵树重新满足AVL规则。

4.1.寻找新插入节点的插入位置,并插入节点

首先我们需要先寻找我们需要插入的这个节点的位置在哪里?然后我们再去进行插入,再更新节点之间的关系,这个过程其实还是很简单的,和我们的二叉搜索树是基本相同的。

cpp 复制代码
// 插入键值对,返回是否插入成功(若键已存在则插入失败)
	bool Insert(const pair<K, V> &kv)
	{
		if (_root == nullptr) // 树为空,直接创建根节点
		{
			_root = new Node(kv);
			return true;
		}

		Node *parent = nullptr; // 用于记录当前节点的父节点
		Node *cur = _root;		// 从根节点 开始搜索 插入位置

		// 查找插入位置
		while (cur) // 只要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 变为空时,循环结束,此时我们就找到了一个空位置,
		// 而 parent 指针正好指向这个空位置的父节点。
		// 接下来就可以创建新节点,并将其链接到 parent 的左或右孩子处。
		// 创建新节点
		cur = new Node(kv);
		// 将新节点链接到父节点
		// 因为我们不知道找到的这个空位置是这个空位置的父节点的左孩子还是右孩子,我们只能通过键来判断。
		//根据二叉搜索树的特性:左子树的值<根节点<右子树
		if (parent->_kv.first < kv.first) // 需要插入的键的值 > 这个空位置的父节点的值
		{
			parent->_right = cur;
		}
		else // 需要插入的键的值 < 这个空位置的父节点的值
		{
			parent->_left = cur;
		}
		cur->_parent = parent; // 设置新节点的父指针
......
}

4.2. 通过循环来更新并检查平衡因子

这里开始就是和二叉搜索树完全不同的地方了,AVL树引入了平衡因子这个东西。

那么我们需要明白,我们插入一个新的节点,那么树中哪些节点的平衡因子会受到影响?

这个很清楚了,也就是只有插入节点的祖先的平衡因子才会受到影响。

那是不是所有祖先的平衡因子都会受影响呢?

其实也不是。

接下来我们就使用一个循环来一直往上更新我们的平衡因子。

while (parent):从新节点的父节点开始,逐层向上处理,直到根节点或提前停止。

在整个循环里面,我们只干了2件事情

  1. 更新parent的平衡因子
  2. 检查更新后的parent的平衡因子有没有问题,看看有没有必要往上对新插入节点的祖先的平衡因子进行更新

结束循环的方式有下面3种:

  1. 当 parent 变为空指针时,表示已回溯至根节点之上,更新过程自然结束。
  2. 当 parent 的平衡因子变为 0 时,表明插入后该子树高度未变,不会影响祖先的平衡因子,因此提前终止循环。
  3. 当 parent 的平衡因子为 ±2 时,说明该节点失衡,需通过旋转恢复平衡,旋转后子树高度复原,直接跳出循环。

4.2.1.更新平衡因子

更新规则:根据当前节点 cur 是其父节点 parent 的左孩子还是右孩子,来修改 parent 的平衡因子:

  • 若 cur 是parent的左孩子 → 左子树变高,parent->_bf 减1。
  • 若 cur 是parent的右孩子 → 右子树变高,parent->_bf 加1。

注意:cur是parent的孩子。

4.2.2.检查平衡因子

我们需要根据更新后的平衡因子决定后续操作

更新完 parent 的平衡因子后,检查其值,分三种情况:

  • 情况1:parent->_bf == 0
  • 情况2:parent->_bf == 1 或 -1
  • 情况3:parent->_bf == 2 或 -2
  • 情况4:其他情况

我们需要仔细考虑一下


4.2.2.1.情况1:parent->_bf == 0
  • 含义:说明插入前 parent 的平衡因子是 ±1,插入后左右子树较矮的一侧被填平,子树高度未发生变化。
  • 操作:break 跳出循环。因为高度不变,不会影响更高层的祖先,调整结束。

我们这里思考一下,为什么不会影响到更高层的祖先?

因为父节点的平衡因子只依赖于它的左右子树的高度。具体来说:

  • 假设当前节点是 parent,它的父节点是 ppnode。
  • ppnode 的平衡因子 = 右子树高度 - 左子树高度。
  • 如果 parent 是 ppnode 的左孩子,那么 ppnode 的左子树高度就是 parent 树的高度;如果 parent 是右孩子,那么 ppnode 的右子树高度就是 parent 树的高度。

而插入后parent->_bf == 0,说明插入前,parent 的平衡因子为 ±1,意味着左右子树高度差1。设较高侧高度为 h+1,较矮侧为 h,则 parent 树的总高度为 h+2。

新节点插入在较矮侧,使较矮侧高度变为 h+1,此时左右子树等高(均为 h+1),parent 树的新高度仍为 h+2(因为最大高度 +1)。高度未变。

如果 parent 树的高度没有变化,而parent 树其实是作为ppnode的左/右子树的,换句话说ppnode的左右子树的高度都没有发生变化,因此 ppnode 的平衡因子保持不变,自然不需要继续向上更新。


4.2.2.2.情况2:parent->_bf == 1 或 -1
  • 含义:插入前 parent 的平衡因子是 0,插入后子树高度增加了1。

  • 操作:继续向上更新,即:

    cpp 复制代码
    cur = parent;
    parent = parent->_parent;
  • 将 parent 作为新的 cur,检查它的父节点,因为高度变化可能向上传递。

我们这里思考一下,为什么会影响到更高层的祖先?

因为父节点的平衡因子只依赖于它的左右子树的高度。具体来说:

  • 假设当前节点是 parent,它的父节点是 ppnode。
  • ppnode 的平衡因子 = 右子树高度 - 左子树高度。
  • 如果 parent 是 ppnode 的左孩子,那么 ppnode 的左子树高度就是 parent 树的高度;如果 parent 是右孩子,那么 ppnode 的右子树高度就是 parent 树的高度。

插入新节点后parent->_bf == 1 或 -1,说明插入前,parent 的平衡因子为0,左右子树等高,设高度均为 h,则 parent 树总高度为 h+1。

新节点插入在某一侧,该侧高度变为 h+1,另一侧仍为 h,此时 parent 树高度变为 h+2,parent这棵树高度增加了1。

parent树的平衡因子还是在我们的预期范围之内的。我们现在就还是没有必要进行处理。

parent树的高度增加了1,而parent 树其实是作为ppnode的左/右子树的,换句话说ppnode的某一侧子树的高度发生变化了,因此 ppnode 的平衡因子一定会发生变化,那么我们就需要对这个 ppnode的平衡因子进行进一步的处理。我们需要接着往上更新。


4.2.2.3.情况3:parent->_bf == 2 或 -2
  • 含义:插入前 parent 的平衡因子已经是 ±1,插入后导致该节点失衡(高度差达到2)。
  • 操作:根据 parent 和它的孩子 cur 的平衡因子判断旋转类型,并调用相应的旋转函数(左单旋、右单旋、双旋)。旋转后子树高度恢复为插入前的高度,因此 break 跳出循环,不再向上更新。

我们发现现在parent树就是最小不平衡二叉树!!

那么这个时候我们有必要往上更新吗??完全没有必要,因为这棵子树就已经出现问题了,祖先节点肯定也是不平衡的,所以我们需要在先进行旋转处理,再去进行更新平衡因子。

我们需要对这个进行分情况处理,那么具体的分情况,我们还需要去进一步判断旋转类型来作出对应的旋转处理。

旋转方式就有4种

  1. 左旋
  2. 右旋
  3. 左右双旋
  4. 右左双旋

那么我们怎么知道我们到底需要使用哪一种旋转类型呢??

其实,旋转的具体类型(单旋或双旋)由两个关键信息决定:

  • 失衡节点 parent 的平衡因子:它告诉我们哪一侧子树过高(正2表示右子树过高,负2表示左子树过高)。
  • 较高子树的根 cur 的平衡因子:cur 是 parent 的较高子树的孩子(即导致失衡的那个孩子)。cur 的平衡因子进一步告诉我们插入节点发生在 cur 的哪一侧,从而明确整体的"形状"。
  • 注意:parent 是 cur 的父节点,平衡因子 = 右子树高度 - 左子树高度

之所以能通过这两个平衡因子唯一确定旋转类型,是因为它们组合起来恰好对应了四种可能的子树形态。下面分别解释每种组合背后的几何意义。

  • 情况1. parent->_bf == 2 且 cur->_bf == 1(左旋)
  • 情况2. parent->_bf == -2 且 cur->_bf == -1(右旋)
  • 情况3. parent->_bf == 2 且 cur->_bf == -1(右左双旋)
  • 情况4. parent->_bf == -2 且 cur->_bf == 1(左右双旋)

现在我们就来好好的看看具体情况。

情况1. parent->_bf == 2 且 cur->_bf == 1

当某个节点 parent 的平衡因子为 +2 时,表示它的右子树比左子树高2层,即右子树过重。

此时需要考察它的右孩子cur 的平衡因子,以确定更具体的失衡形态。

如果 cur 的平衡因子为 +1,说明 cur 的右子树比左子树高1层,即 cur 自身也是"右重"的。

那么从 parent 到 cur 再到 cur 的右孩子,形成了一条向右的直线(称为"右右"型)。

这种形态下,过重的部分全部集中在右侧,最适合通过一次左单旋来调整。

那么问题其实来了:为什么cur是parent的右孩子而不是左孩子??

在AVL树的旋转判断中,我们通过parent的平衡因子符号就能确定cur是它的左孩子还是右孩子:

  • 如果parent->_bf == 2,说明右子树更高,而我们是插入元素,插入元素的那一侧子树的高度就会增加,那么导致失衡的较高子树的根一定是parent的右孩子,即cur必然等于parent->_right。
  • 如果parent->_bf == -2,说明左子树更高,那么cur必然是parent的左孩子。

情况2. parent->_bf == -2 且 cur->_bf == -1

当某个节点 parent 的平衡因子为 -2 时,表示它的右子树比左子树低2层,即左子树过重。

此时需要考察它的左孩子cur 的平衡因子,以确定更具体的失衡形态。

如果 cur 的平衡因子为 -1,说明 cur 的右子树比左子树低1层,即 cur 自身也是"左重"的。

那么从 parent 到 cur 再到 cur 的左孩子,形成了一条向左的直线(称为"左左"型)。

这种形态下,过重的部分全部集中在左侧,最适合通过一次右单旋来调整。

那么问题其实来了:为什么cur是parent的左孩子而不是右孩子??

在AVL树的旋转判断中,我们通过parent的平衡因子符号就能确定cur是它的左孩子还是右孩子:

  • 如果parent->_bf == 2,说明右子树更高,而我们是插入元素,插入元素的那一侧子树的高度就会增加,那么导致失衡的较高子树的根一定是parent的右孩子,即cur必然等于parent->_right。
  • 如果parent->_bf == -2,说明左子树更高,那么cur必然是parent的左孩子。

情况3. parent->_bf == 2 且 cur->_bf == -1

当某个节点 parent 的平衡因子为 2 时,表示它的右子树比左子树高2层,即右子树过重。

此时需要考察它的右孩子cur 的平衡因子,以确定更具体的失衡形态。

如果 cur 的平衡因子为 -1,说明 cur 的左子树比右子树高1层,即 cur 自身是"左重"的。

那么从 parent 到 cur 再到 cur 的左孩子,形成了一条先右后左的折线(称为 "右左"型)。

这种形态下,过重的部分分布在两侧,无法通过单次旋转(如左单旋或右单旋)直接恢复平衡,因为单旋会导致新的不平衡。

因此,需要采用 右左双旋:先对 cur 进行右单旋,将折线捋直为"右右"型,再对 parent 进行左单旋,最终使树恢复平衡。

那么问题其实来了:为什么cur是parent的右孩子而不是左孩子??

在AVL树的旋转判断中,我们通过parent的平衡因子符号就能确定cur是它的左孩子还是右孩子:

  • 如果parent->_bf == 2,说明右子树更高,而我们是插入元素,插入元素的那一侧子树的高度就会增加,那么导致失衡的较高子树的根一定是parent的右孩子,即cur必然等于parent->_right。
  • 如果parent->_bf == -2,说明左子树更高,那么cur必然是parent的左孩子。

情况4. parent->_bf == -2 且 cur->_bf == 1

  • 含义:parent 的左子树过高,但 cur 的右子树过高(即 cur 的平衡因子为正)。
  • 形状:呈"左右"折线:parent 的左孩子是 cur,而 cur 的右孩子是插入发生的地方。
  • 插入位置:新节点插入在 cur 的右子树中。
  • 解决方案:先对 cur 进行左单旋,再对 parent 进行右单旋。同样,旋转后根据原 cur 右孩子的平衡因子更新各节点。

当某个节点 parent 的平衡因子为 -2 时,表示它的右子树比左子树低2层,即左子树过重。

此时需要考察它的左孩子cur 的平衡因子,以确定更具体的失衡形态。

如果 cur 的平衡因子为 1,说明 cur 的右子树比左子树高1层,即 cur 自身是"右重"的。

那么从 parent 到 cur 再到 cur 的左孩子,形成了一条先左后右的折线(称为 "左右"型)。

这种形态下,过重的部分分布在两侧,无法通过单次旋转(如左单旋或右单旋)直接恢复平衡,因为单旋会导致新的不平衡。

因此,需要采用 左右双旋:先对 cur 进行左单旋,将折线捋直为"左左"型,再对 parent 进行右单旋,最终使树恢复平衡。

那么问题其实来了:为什么cur是parent的左孩子而不是右孩子??

在AVL树的旋转判断中,我们通过parent的平衡因子符号就能确定cur是它的左孩子还是右孩子:

  • 如果parent->_bf == 2,说明右子树更高,而我们是插入元素,插入元素的那一侧子树的高度就会增加,那么导致失衡的较高子树的根一定是parent的右孩子,即cur必然等于parent->_right。
  • 如果parent->_bf == -2,说明左子树更高,那么cur必然是parent的左孩子。

4.2.2.4.情况4:其他情况(如 3 或 -3):
  • 这种情况理论上不应出现,如果出现了就说明树之前就不平衡,用 assert(false) 终止程序。

4.3.插入操作的完整代码

按照上面的思路,我们现在就很快得到下面这个代码:大家捋一下

cpp 复制代码
// 插入键值对,返回是否插入成功(若键已存在则插入失败)
	bool Insert(const pair<K, V> &kv)
	{
		if (_root == nullptr) // 树为空,直接创建根节点
		{
			_root = new Node(kv);
			return true;
		}

		Node *parent = nullptr; // 用于记录当前节点的父节点
		Node *cur = _root;		// 从根节点 开始搜索 插入位置

		// 查找插入位置
		while (cur) // 只要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 变为空时,循环结束,此时我们就找到了一个空位置,
		// 而 parent 指针正好指向这个空位置的父节点。
		// 接下来就可以创建新节点,并将其链接到 parent 的左或右孩子处。

		// 创建新节点
		cur = new Node(kv);
		// 将新节点链接到父节点
		// 因为我们不知道找到的这个空位置是这个空位置的父节点的左孩子还是右孩子,我们只能通过键来判断。
		if (parent->_kv.first < kv.first) // 需要插入的键的值 > 这个空位置的父节点的值
		{
			parent->_right = cur;
		}
		else // 需要插入的键的值 < 这个空位置的父节点的值
		{
			parent->_left = cur;
		}
		cur->_parent = parent; // 设置新节点的父指针

		// --- 控制平衡:更新平衡因子并根据需要旋转 ---
		// 从新节点的父节点开始,逐层向上处理,直到根节点或提前停止。
		// parent不为空,就说明parent不是根节点,只有cur是根节点的时候parent才为空
		while (parent) // 从插入节点的父节点开始向上更新平衡因子
		{
			// 根据 cur 是 parent 的左孩子还是右孩子更新平衡因子
			// 注意我们这里的平衡因子 = 右子树的高度 - 左子树的高度
			// 若 cur 是左孩子 → 左子树变高,parent->_bf 减1。
			// 若 cur 是右孩子 → 右子树变高,parent->_bf 加1。
			if (cur == parent->_left)
			{
				parent->_bf--; // 左子树变高,平衡因子减1
			}
			else // cur == parent->_right
			{
				parent->_bf++; // 右子树变高,平衡因子加1
			}

			// 检查更新后的平衡因子
			if (parent->_bf == 0)
			{
				// 平衡因子为0,说明插入前 parent 的平衡因子是 ±1,插入后左右子树较矮的一侧被填平,子树高度未发生变化。
				// 操作:break 跳出循环。因为插入这个节点后子树的高度不变,不会影响更高层的祖先,调整结束。
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{

				// 平衡因子为±1,说明插入前 parent 的平衡因子是 0,插入后子树高度增加了1,可能影响祖先,继续向上更新
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 平衡因子为±2,说明插入前 parent 的平衡因子已经是 ±1,插入后导致该节点失衡(高度差达到2)子树失去平衡,需要旋转
				// 根据 parent 和 cur 的平衡因子判断旋转类型(四种情况)
				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) // 右左:右左双旋
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1) // 左右:左右双旋
				{
					RotateLR(parent);
				}
				// 旋转后子树高度恢复,平衡,停止更新
				break;
			}
			else
			{
				// 如果平衡因子出现其他值(如3或-3),说明之前就存在不平衡,直接断言错误,中断程序
				assert(false);
			}
		}
		/*
		结束循环的方式有下面3种:
		1.当 parent 变为空指针时,表示已回溯至根节点之上,更新过程自然结束。
		2.当 parent 的平衡因子变为 0 时,表明插入后该子树高度未变,不会影响祖先的平衡因子,因此提前终止循环。
		3.当 parent 的平衡因子为 ±2 时,说明该节点失衡,需通过旋转恢复平衡,旋转后子树高度复原,直接跳出循环。*/

		return true; // 插入成功
	}

五.各种旋转方式的实现

旋转的时候需要注意的问题:

  • 1.保持这颗子树是二叉搜索树
  • 2.让它变成AVL树,且降低整颗子树的高度

我们上面在parent->_bf == 2 或 -2需要进行旋转处理。那么针对4种不同旋转类型,我们作出的旋转操作也不一样。

  • 情况1. parent->_bf == 2 且 cur->_bf == 1 ------采用左单旋
  • 情况2. parent->_bf == -2 且 cur->_bf == -1 ------ 采用右单旋
  • 情况3. parent->_bf == 2 且 cur->_bf == -1 ------采用右左双旋
  • 情况4. parent->_bf == -2 且 cur->_bf == 1 ------采用左右双旋

那么这4种旋转的情况,我们都还没有进行详细实现,那么在我们这里一节里面,我们就对这4种旋转情况进行详细分析。

5.1. 左单旋

什么时候需要左单旋?也就是RR情况(右右情况)

**RR情况:新结点由于插⼊在T结点的右孩⼦(R)的右⼦树(RR)中,从⽽导致T结点失衡。**如下图所⽰:

此时需要⼀次向左的旋转操作,将R左旋:

  • 将结点R向左上旋转代替结点T作为根结点;
  • 将节点T向左下旋转作为结点R的左⼦树的根结点;
  • 结点R的原左⼦树(RL)则作为结点T的右⼦树。

如下图:

案例:下列AVL树中插⼊ 64。

最⼩不平衡⼦树是以49为根的⼦树,引起不平衡的原因是49的右孩⼦的右⼦树上插⼊⼀个新的结 点,因此需要左旋⼀次。左旋的结点为59:


我们边看动图,边看代码

事实上,左单旋的情况其实是有无数种情况的。我们不能画出像上面那样有具体层数的图。我们只能使用一种概型的图去描述这个左旋的过程。

在下图:a,b,c都是符合AVL平衡规则的子树。

这个h可不是固定的一个数值,它可以取任意整数。

而且h仅仅只是代表了这棵子树的高度,至于树的形状还可以分成很多情况,我们这样子进行排列组合一下,发现符合这个符合条件的a,b,c子树的形状啥的就是无穷无尽的。

但是我们左旋的操作是都适用于所有满足条件的树的。

至于左旋,右旋,左右双旋,右左双旋都是一样的。

我们仔细观看一下这个动图

  • cur一直指向60这个节点
  • parent一直指向30这个节点

我们仔细观察一下这个动图就3步

  1. 让b成为30的右子树
  2. 让30成为60的左子树
  3. 更新平衡因子

但是,实际上在更新平衡因子之前还有一步,那就是我们这颗子树的根节点需要去替换。

  • 如果30本来就是一整颗树的根节点,那么现在根节点换成了60,我们需要修改一下
  • 如果30本来不是根节点,而是作为另外一棵树的子树的存在,那么我们就需要去更新原来的30的父节点(ppnode)的对应的那个子树指针的值。因为这个子树的根节点从30变成了60,我们需要将ppnode的对应子树指针的值从30这个节点的地址修改成60这个节点的地址。

做完了这一步,我们才能去更新我们的平衡因子。注意旋转之后,平衡因子一定为0.

cpp 复制代码
// 左单旋:处理右右情况(parent->_bf == 2 且 cur->_bf == 1)
	void RotateL(Node* parent)//注意传递进来的是parent,而不是cur
	{
		++_rotateCount;   // 记录旋转次数(可用于统计或调试)

		Node* cur = parent->_right;   // cur 为 parent 的右孩子
		Node* curleft = cur->_left;   // cur 的左孩子 , 本质也是 parent 的右孩子的左孩子,对应图上的b

		// 1. 将 curleft 作为 parent 的右孩子 ------ 本质就是将b作为30的右子树
		parent->_right = curleft;
		if (curleft)                     // 若 curleft 不为空,更新其父指针
		{
			curleft->_parent = parent;
		}

		// 2. 将 parent 作为 cur 的左孩子 ------ 对应动图上,本质就是让30成为60的左子树
		cur->_left = parent;

		// 3. 处理 parent 的父节点(ppnode)与 cur 的连接
		Node* ppnode = parent->_parent;   // 记录 parent 的原父节点
		parent->_parent = cur;            // 更新 parent 的父指针
		//注意这个时候的parent还是指向30那个节点,cur还是指向60那个节点

		if (parent == _root)              // 若 parent 是根节点
		{
			_root = cur;                  // 更新根节点为 cur
			cur->_parent = nullptr;       // cur 的父指针置空
		}
		else                               // 若 parent 不是根节点
		{
			// 判断 parent 原先是其父节点的左孩子还是右孩子
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;       // 将 ppnode 的左孩子改为 cur
			}
			else
			{
				ppnode->_right = cur;      // 将 ppnode 的右孩子改为 cur
			}
			cur->_parent = ppnode;         // 更新 cur 的父指针
		}

		// 4. 更新平衡因子:旋转后 parent 和 cur 的平衡因子均为0
		parent->_bf = cur->_bf = 0;
	}

在我们这个代码里面

  • cur一直指向60这个节点
  • parent一直指向30这个节点

为什么左旋之后parent和cur的平衡因子都设置为0了??

大家看这段代码的时候其实需要结合上面插入操作的代码来看

在插入操作时,我们在什么情况下采取了左旋??

也就是下面这个情况

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

我们看下面这个图就明白了

5.2. 右单旋

什么时候会用到右单旋呢?

也就是LL情况(左左情况)

LL情况:在某个节点A的左子树的左子树上插入新节点,导致该节点A的平衡因子的绝对值大于 1。

此时需要将L右旋:

  • 将结点L向右上旋转代替结点T作为根结点;
  • 将节点T向右下旋转作为结点L的右⼦树的根结点;
  • 结点L的原右⼦树(LR)则作为结点T的左⼦树。

案例:下列AVL树中插⼊1 。

最⼩不平衡⼦树是以13为根的⼦树,引起不平衡的原因是13的左孩⼦的左⼦树上插⼊⼀个新的结 点,因此需要右旋⼀次。右旋的结点为 10:


我们看一下动图,然后我们看一下代码

在AVL树的右单旋操作中,实际情况可能存在无数种不同的子树形态,因此不能仅通过绘制具有具体层数的示意图来涵盖所有情形。

我们需要采用一种抽象模型来描述右旋过程,如下图所示:其中a、b、c均为满足AVL平衡条件的子树,它们的高度h可以取任意非负整数。

h仅表示子树的高度,而子树的具体形状则因节点分布不同而千变万化,通过排列组合可知,符合条件的a、b、c子树的形状实际上是无穷无尽的。

尽管如此,右旋操作的步骤却具有通用性,适用于所有满足上述条件的树结构,这正是抽象模型的意义所在------它保证了旋转算法的普适性,无论子树的具体形态如何,都能通过统一的步骤完成平衡调整。

至于左右双旋,右左双旋都是一样的。

我们仔细观看一下这个动图

  • cur最初指向30
  • parent最初指向60

我们仔细观察一下这个动图就3步

  1. 让b成为60的左子树
  2. 让60成为30的右子树
  3. 更新平衡因子

但是,实际上在更新平衡因子之前还有一步,那就是我们这颗子树的根节点需要去替换。

  • 如果60本来就是一整颗树的根节点,那么现在根节点换成了30,我们需要修改一下
  • 如果60本来不是根节点,而是作为另外一棵树的子树的存在,那么我们就需要去更新原来的60的父节点(ppnode)的对应的那个子树指针的值。因为这个子树的根节点从60变成了30,我们需要将ppnode的对应子树指针的值从60这个节点的地址修改成30这个节点的地址。

做完了这一步,我们才能去更新我们的平衡因子。注意旋转之后,平衡因子一定为0.

cpp 复制代码
// 右单旋:处理左左情况(parent->_bf == -2 且 cur->_bf == -1)
	void RotateR(Node* parent)
	{
		++_rotateCount;   // 记录旋转次数

		Node* cur = parent->_left;        // cur 为 parent 的左孩子
		Node* curright = cur->_right;     // cur 的右孩子 , 本质也是 parent 的左孩子的右孩子,对应图上的b

		// 1. 将 curright 作为 parent 的左孩子 ------ 对应动图上也就是让b成为60的左孩子
		parent->_left = curright;
		if (curright)// 若 curright 不为空,更新其父指针
		{                       
			curright->_parent = parent;
		}

		// 2. 将 parent 作为 cur 的右孩子 ------ 对应动图上也就是让60成为30的右孩子
		cur->_right = parent;

		// 3. 处理 parent 的父节点(ppnode)与 cur 的连接
		Node* ppnode = parent->_parent;     // 记录 parent 的原父节点
		parent->_parent = cur;               // 更新 parent 的父指针
		//注意这个时候的parent还是指向60那个节点,cur还是指向30那个节点

		if (ppnode == nullptr)               // 若 parent 是根节点
		{
			_root = cur;                     // 更新根节点为 cur
			cur->_parent = nullptr;          // cur 的父指针置空
		}
		else                                 // 若 parent 不是根节点
		{
			// 判断 parent 原先是其父节点的左孩子还是右孩子
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;         // 将 ppnode 的左孩子改为 cur
			}
			else
			{
				ppnode->_right = cur;        // 将 ppnode 的右孩子改为 cur
			}
			cur->_parent = ppnode;           // 更新 cur 的父指针
		}

		// 4. 更新平衡因子:旋转后 parent 和 cur 的平衡因子均为0
		parent->_bf = cur->_bf = 0;
	}

注意:在我们这个代码里面

  • cur一直指向30这个节点
  • parent一直指向60这个节点

为什么右旋之后parent和cur的平衡因子都设置为0了??

大家看这段代码的时候其实需要结合上面插入操作的代码来看

在插入操作时,我们在什么情况下采取了左旋??

也就是下面这个情况

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

那么我们画图就能知道最后的平衡因子应该是啥了

5.3. 左右双旋

什么时候需要使用左右双旋呢?其实也就是LR情况(左右情况)

LR表⽰:新结点由于插⼊在T结点的左孩⼦(L)的右⼦树(LR)中,从⽽导致T结点失衡。

此时需要两次旋转操作,先将LR左旋,再将LR右旋。

将LR左旋:

  • 将结点LR向左上旋转代替结点L作为根结点;
  • 将节点L向左下旋转作为结点LR的左⼦树的根结点;
  • 结点LR的原左⼦树(LRL)则作为结点L的右⼦树。

将LR右旋:

  • 将结点LR向右上旋转代替结点T作为根结点;
  • 将节点T向右下旋转作为结点LR的右⼦树的根结点;
  • 结点LR的原右⼦树(LRR)则作为结点T的左⼦树。

如下图:

案例:下列AVL树中插⼊ 1。

最⼩不平衡⼦树是以49为根的⼦树,引起不平衡的原因是49的左孩⼦的右⼦树上插⼊⼀个新的结 点,因此需要左旋⼀次,然后右旋⼀次。

旋转的结点为45:


我们看看动图,然后看看代码

我们仔细观看一下这个动图

  • cur最初指向30
  • parent最初指向90

我们仔细观察一下这个动图就4步

  1. 让b成为30的右子树(左旋)
  2. 让30成为60的左子树(左旋)
  3. 让c成为90的左子树(右旋)
  4. 让90成为60的左子树(右旋)
  5. 更新平衡因子

我们把这4步拆分成2部分,一次左旋,一次右旋。

由于我们在之前已经写好了左旋,右旋的函数代码了,我们直接套用即可。

左旋右旋之后,我们需要对平衡因子受影响的那些节点作出平衡因子的更新。

cpp 复制代码
// 左右双旋:先左旋 cur,再右旋 parent(处理 parent->_bf == -2 且 cur->_bf == 1)
	void RotateLR(Node* parent)
	{
		Node* cur = parent->_left;          // cur 为 parent 的左孩子,对应图上的30
		Node* curright = cur->_right;       // cur 的右孩子(即旋转轴心),对应图上的60
		int bf = curright->_bf;               // 记录 curright 的平衡因子,用于后续调整,也就是60节点的平衡因子

		// 第一步:对 parent 的左子树(cur)进行左单旋
		RotateL(parent->_left);//对应图上的30
		// 第二步:对 parent 进行右单旋
		RotateR(parent);//对应图上的90

		//注意:只有30,60,90这3个节点的平衡因子才会改变
		// 旋转完成后,根据原 curright 的平衡因子更新相关节点的平衡因子
		if (bf == 0)          // curright 自身就是新插入节点(情况1)
		{
			parent->_bf = 0;//修改90节点的平衡因子
			cur->_bf = 0;//修改30节点的平衡因子
			curright->_bf = 0;//修改30节点的平衡因子
		}
		else if (bf == -1)    // 新节点插入在 curright 的左子树(情况2)
		{
			parent->_bf = 1;
			cur->_bf = 0;
			curright->_bf = 0;
		}
		else if (bf == 1)     // 新节点插入在 curright 的右子树(情况3)
		{
			parent->_bf = 0;
			cur->_bf = -1;
			curright->_bf = 0;
		}
	}

针对这个平衡因子的更新,我们需要详细讲解一下:

  • bf=0:curright就是新插入的元素
  • bf=-1:新插入的元素在curright的左子树上面
  • bf=1:新插入的元素在curright的右子树上面

为什么bf==0的时候就说明这个curright 自身就是新插入节点??

还记得我们在什么情况才会使用左右双旋吗?

左右双旋适用于parent->_bf == -2 且 cur->_bf == 1这种情况

新节点插入后,curright 的平衡因子 bf 有三种可能:0、-1、1。

bf == 0:这意味着插入后 curright 的左右子树高度相等。

  • 如果 curright 原来就有子树,那么插入一个节点后要使平衡因子变为0,通常需要原来左右子树高度差为±1,且插入在较矮的一侧,这样高度差被弥补,但此时 curright 的高度 不变(因为较矮的一侧变高后,总高度与原来较高的一侧相同)。
  • 但若 curright 高度不变,则 cur 的右子树高度也不变,**cur 的平衡因子就不会从原来的0变成+1,也就不会导致 parent 不平衡。**这与我们正在处理 parent 不平衡的事实矛盾。
  • 因此,唯一合理的解释是:**curright 本身就是新插入的节点,而且是插入到cur的右子树里面。**此时它没有左右子树,平衡因子为0,且高度为1(新节点)。这样,cur 的右子树高度从0增加到1,cur 的平衡因子从0变成+1,进而使 parent 的左子树高度增加,导致 parent 平衡因子从-1变成-2。所以 bf == 0 对应新节点就是 curright 本身。

为什么bf==-1的时候就说明新节点插入在 curright 的左子树?

首先我们需要明白,我们更新平衡因子,一定是从新插入节点的父节点开始往上更新新插入节点的祖先的平衡因子。

而我们这里的bf一定是更新完了的。那么我们就可以倒退之前的平衡因子是啥?

bf == -1:说明插入后 curright 的左子树比右子树高1层,这对应于新节点插入在 curright 的 左子树 中 ,且插入前 curright 的平衡因子为0(这样插入后变成-1,高度增加)。因为插入在左子树使得 curright 高度增加,进而引起 cur 的右子树高度增加,最终导致parent->_bf == -2 且 cur->_bf == 1。

为什么bf==-1的时候就说明新节点插入在 curright 的右子树?

bf == 1:插入后 curright 的右子树比左子树高1。这对应于新节点插入在 curright 的 右子树 中 ,同样插入前 curright 的平衡因子为0,插入后变成1,高度增加。因为插入在右子树使得 curright 高度增加,进而引起 cur 的右子树高度增加,最终导致parent->_bf == -2 且 cur->_bf == 1。

为什么bf的值不同,我们修改的平衡因子不同

我们不多说,直接看例子:还是很容易懂的

情况1:bf==0

情况2:bf==-1

情况3:bf==1

至于为什么平衡因子是那些数字?我们看上面这个图就明白了。

5.4. 右左双旋

什么时候需要使用右左双旋呢?其实也就是RL情况(右左情况)

RL表⽰:新结点由于插⼊在T结点的右孩⼦(R)的左⼦树(RL)中,从⽽导致T结点失衡。

如下图所⽰:

此时需要两次旋转操作,先将RL右旋,再将RL左旋。

将RL右旋:

  • 将结点RL向右上旋转代替结点R作为根结点;
  • 将节点R向右下旋转作为结点RL的右⼦树的根结点;
  • 结点RL的原右⼦树(RLR)则作为结点R的左⼦树。

将RL左旋:

  • 将结点RL向左上旋转代替结点T作为根结点;
  • 将节点T向左下旋转作为结点RL的左⼦树的根结点;
  • 结点RL的原左⼦树(RLL)则作为结点T的右⼦树。

如下图:

案例:下列AVL树中插⼊52。

最⼩不平衡⼦树是以49为根的⼦树,引起不平衡的原因是49的右孩⼦的左⼦树上插⼊⼀个新的结 点,因此需要右旋⼀次,然后再左旋⼀次。旋转的结点为55:


我们看动图,然后看代码

我们仔细观看一下这个动图

  • cur最初指向30
  • parent最初指向90

我们仔细观察一下这个动图就4步

  1. 让c成为90的左子树(右旋)
  2. 让90成为60的左子树(右旋)
  3. 让b成为30的右子树(左旋)
  4. 让30成为60的左子树(左旋)
  5. 更新平衡因子

我们把这4步拆分成2部分,一次右旋,一次左旋。

由于我们在之前已经写好了左旋,右旋的函数代码了,我们直接套用即可。

左旋右旋之后,我们需要对平衡因子受影响的那些节点作出平衡因子的更新。

我们的这个parent->_bf == 2 且 cur->_bf == -1

cpp 复制代码
// 右左双旋:先右旋 cur,再左旋 parent(处理 parent->_bf == 2 且 cur->_bf == -1)
	void RotateRL(Node* parent)
	{
		Node* cur = parent->_right;         // cur 为 parent 的右孩子,对应图上的90
		Node* curleft = cur->_left;         // cur 的左孩子(即旋转轴心),对应图上的60
		int bf = curleft->_bf;                // 记录 curleft 的平衡因子,用于后续调整,也就是60节点的平衡因子

		// 第一步:对 parent 的右子树(cur)进行右单旋
		RotateR(parent->_right);
		// 第二步:对 parent 进行左单旋
		RotateL(parent);

		//注意:只有30,60,90这3个节点的平衡因子才会改变
		// 旋转完成后,根据原 curleft 的平衡因子更新相关节点的平衡因子
		if (bf == 0)          // curleft 自身就是新插入节点(情况1)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)     // 新节点插入在 curleft 的右子树(情况2)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1)    // 新节点插入在 curleft 的左子树(情况3)
		{
			cur->_bf = 1;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);    // 不可能出现其他值
		}
	}

那么针对curleft->bf的三种不同情况,我们在下面就给出了答案

情况一:bf==0

情况二:bf==1

情况三:bf==-1

六.获取某棵子树的高度

获取一棵树的高度,其实很简单,我们可以采取递归的方式来

cpp 复制代码
// 计算整棵树的高度(对外接口)
	int Height()
	{
		return Height(_root);
	}

	// 递归计算以 root 为根的子树的高度
	int Height(Node *root)
	{
		if (root == nullptr)
			return 0;

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

		if (leftHeight > rightHeight)
		{
			return leftHeight + 1;
		}
		else
		{
			return rightHeight + 1;
		}
	}

当然,如果说你觉得这个递归版本的看不太懂,那么我们也可以使用层序遍历(BFS)记录层数

思路:使用队列进行层序遍历,每遍历完一层,层数加1。

cpp 复制代码
#include <queue>

int Height(Node* root) {
    if (root == nullptr) return 0;

    queue<Node*> q;
    q.push(root);
    int level = 0;

    while (!q.empty()) {
        int levelSize = q.size();  // 当前层的节点数
        for (int i = 0; i < levelSize; ++i) {
            Node* front = q.front();
            q.pop();
            if (front->_left) q.push(front->_left);
            if (front->_right) q.push(front->_right);
        }
        ++level;  // 一层遍历完,高度+1
    }
    return level;
}

说明:

  • 利用队列实现广度优先遍历,每处理完一层,计数器 level 加1。
  • 最终 level 就是树的高度(根节点在第1层,返回高度即为层数)。
  • 这种方法代码简单直观,且不易出错。

两种版本的功能是一样的。

我们采用同一种方法即可。

七.检查整棵树是否平衡

cpp 复制代码
// 检查整棵树是否平衡(对外接口)
	bool IsBalance()
	{
		return IsBalance(_root);
	}

	// 递归检查以 root 为根的子树是否平衡
	bool IsBalance(Node *root)
	{
		if (root == nullptr)
			return true;

		int leftHight = Height(root->_left);   // 左子树高度
		int rightHight = Height(root->_right); // 右子树高度

		// 检查平衡因子是否正确(右子树高度 - 左子树高度 应与存储的平衡因子相等)
		if (rightHight - leftHight != root->_bf)
		{
			cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;
			return false;
		}

		// 检查当前节点是否平衡(高度差绝对值小于2)并且左右子树也平衡
		return abs(rightHight - leftHight) < 2 && IsBalance(root->_left) && IsBalance(root->_right);
	}

这个其实就是检测一下平衡因子有没有出错的问题。

八.完整代码

cpp 复制代码
#pragma once // 防止头文件重复包含

#include <iostream> // 输入输出流
#include <assert.h> // 断言,用于错误检查
#include <queue>
using namespace std;

// AVL树节点结构体模板
template <class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;				// 键值对,用pair存储
	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)
	{
	}
};

// AVL树类模板
template <class K, class V>
class 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) // 只要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 变为空时,循环结束,此时我们就找到了一个空位置,
		// 而 parent 指针正好指向这个空位置的父节点。
		// 接下来就可以创建新节点,并将其链接到 parent 的左或右孩子处。

		// 创建新节点
		cur = new Node(kv);
		// 将新节点链接到父节点
		// 因为我们不知道找到的这个空位置是这个空位置的父节点的左孩子还是右孩子,我们只能通过键来判断。
		if (parent->_kv.first < kv.first) // 需要插入的键的值 > 这个空位置的父节点的值
		{
			parent->_right = cur;
		}
		else // 需要插入的键的值 < 这个空位置的父节点的值
		{
			parent->_left = cur;
		}
		cur->_parent = parent; // 设置新节点的父指针

		// --- 控制平衡:更新平衡因子并根据需要旋转 ---
		// 从新节点的父节点开始,逐层向上处理,直到根节点或提前停止。
		while (parent) // 从插入节点的父节点开始向上更新平衡因子
		{
			// 根据 cur 是 parent 的左孩子还是右孩子更新平衡因子
			// 注意我们这里的平衡因子 = 右子树的高度 - 左子树的高度
			// 若 cur 是左孩子 → 左子树变高,parent->_bf 减1。
			// 若 cur 是右孩子 → 右子树变高,parent->_bf 加1。
			if (cur == parent->_left)
			{
				parent->_bf--; // 左子树变高,平衡因子减1
			}
			else // cur == parent->_right
			{
				parent->_bf++; // 右子树变高,平衡因子加1
			}

			// 检查更新后的平衡因子
			if (parent->_bf == 0)
			{
				// 平衡因子为0,说明插入前 parent 的平衡因子是 ±1,插入后左右子树较矮的一侧被填平,子树高度未发生变化。
				// 操作:break 跳出循环。因为插入这个节点后子树的高度不变,不会影响更高层的祖先,调整结束。
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{

				// 平衡因子为±1,说明插入前 parent 的平衡因子是 0,插入后子树高度增加了1,可能影响祖先,继续向上更新
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				// 平衡因子为±2,说明插入前 parent 的平衡因子已经是 ±1,插入后导致该节点失衡(高度差达到2)子树失去平衡,需要旋转
				// 根据 parent 和 cur 的平衡因子判断旋转类型(四种情况)
				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) // 右左:右左双旋
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1) // 左右:左右双旋
				{
					RotateLR(parent);
				}
				// 旋转后子树高度恢复,平衡,停止更新
				break;
			}
			else
			{
				// 如果平衡因子出现其他值(如3或-3),说明之前就存在不平衡,直接断言错误,中断程序
				assert(false);
			}
		}

		return true; // 插入成功
	}

	// 计算整棵树的高度(对外接口)
	int Height()
	{
		return Height(_root);
	}
	
	// 检查整棵树是否平衡(对外接口)
	bool IsBalance()
	{
		return IsBalance(_root);
	}

private:
	// 左单旋:处理右右情况(parent->_bf == 2 且 cur->_bf == 1)
	void RotateL(Node *parent) // 注意传递进来的是parent,而不是cur
	{
		++_rotateCount; // 记录旋转次数(可用于统计或调试)

		Node *cur = parent->_right; // cur 为 parent 的右孩子
		Node *curleft = cur->_left; // cur 的左孩子 , 本质也是 parent 的右孩子的左孩子,对应图上的b

		// 1. 将 curleft 作为 parent 的右孩子 ------ 本质就是将b作为30的右子树
		parent->_right = curleft;
		if (curleft) // 若 curleft 不为空,更新其父指针
		{
			curleft->_parent = parent;
		}

		// 2. 将 parent 作为 cur 的左孩子 ------ 对应动图上,本质就是让30成为60的左子树
		cur->_left = parent;

		// 3. 处理 parent 的父节点(ppnode)与 cur 的连接
		Node *ppnode = parent->_parent; // 记录 parent 的原父节点
		parent->_parent = cur;			// 更新 parent 的父指针
		// 注意这个时候的parent还是指向30那个节点,cur还是指向60那个节点

		if (parent == _root) // 若 parent 是根节点
		{
			_root = cur;			// 更新根节点为 cur
			cur->_parent = nullptr; // cur 的父指针置空
		}
		else // 若 parent 不是根节点
		{
			// 判断 parent 原先是其父节点的左孩子还是右孩子
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur; // 将 ppnode 的左孩子改为 cur
			}
			else
			{
				ppnode->_right = cur; // 将 ppnode 的右孩子改为 cur
			}
			cur->_parent = ppnode; // 更新 cur 的父指针
		}

		// 4. 更新平衡因子:旋转后 parent 和 cur 的平衡因子均为0
		parent->_bf = cur->_bf = 0;
	}

	// 右单旋:处理左左情况(parent->_bf == -2 且 cur->_bf == -1)
	void RotateR(Node *parent)
	{
		++_rotateCount; // 记录旋转次数

		Node *cur = parent->_left;	  // cur 为 parent 的左孩子
		Node *curright = cur->_right; // cur 的右孩子 , 本质也是 parent 的左孩子的右孩子,对应图上的b

		// 1. 将 curright 作为 parent 的左孩子 ------ 对应动图上也就是让b成为60的左孩子
		parent->_left = curright;
		if (curright) // 若 curright 不为空,更新其父指针
		{
			curright->_parent = parent;
		}

		// 2. 将 parent 作为 cur 的右孩子 ------ 对应动图上也就是让60成为30的右孩子
		cur->_right = parent;

		// 3. 处理 parent 的父节点(ppnode)与 cur 的连接
		Node *ppnode = parent->_parent; // 记录 parent 的原父节点
		parent->_parent = cur;			// 更新 parent 的父指针
		// 注意这个时候的parent还是指向60那个节点,cur还是指向30那个节点

		if (ppnode == nullptr) // 若 parent 是根节点
		{
			_root = cur;			// 更新根节点为 cur
			cur->_parent = nullptr; // cur 的父指针置空
		}
		else // 若 parent 不是根节点
		{
			// 判断 parent 原先是其父节点的左孩子还是右孩子
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur; // 将 ppnode 的左孩子改为 cur
			}
			else
			{
				ppnode->_right = cur; // 将 ppnode 的右孩子改为 cur
			}
			cur->_parent = ppnode; // 更新 cur 的父指针
		}

		// 4. 更新平衡因子:旋转后 parent 和 cur 的平衡因子均为0
		parent->_bf = cur->_bf = 0;
	}

	// 右左双旋:先右旋 cur,再左旋 parent(处理 parent->_bf == 2 且 cur->_bf == -1)
	void RotateRL(Node *parent)
	{
		Node *cur = parent->_right; // cur 为 parent 的右孩子,对应图上的90
		Node *curleft = cur->_left; // cur 的左孩子(即旋转轴心),对应图上的60
		int bf = curleft->_bf;		// 记录 curleft 的平衡因子,用于后续调整,也就是60节点的平衡因子

		// 第一步:对 parent 的右子树(cur)进行右单旋
		RotateR(parent->_right);
		// 第二步:对 parent 进行左单旋
		RotateL(parent);

		// 注意:只有30,60,90这3个节点的平衡因子才会改变
		//  旋转完成后,根据原 curleft 的平衡因子更新相关节点的平衡因子
		if (bf == 0) // curleft 自身就是新插入节点(情况1)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1) // 新节点插入在 curleft 的右子树(情况2)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == -1) // 新节点插入在 curleft 的左子树(情况3)
		{
			cur->_bf = 1;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false); // 不可能出现其他值
		}
	}

	// 左右双旋:先左旋 cur,再右旋 parent(处理 parent->_bf == -2 且 cur->_bf == 1)
	void RotateLR(Node *parent)
	{
		Node *cur = parent->_left;	  // cur 为 parent 的左孩子,对应图上的30
		Node *curright = cur->_right; // cur 的右孩子(即旋转轴心),对应图上的60
		int bf = curright->_bf;		  // 记录 curright 的平衡因子,用于后续调整,也就是60节点的平衡因子
		// 这个是旋转之前的平衡因子

		// 第一步:对 parent 的左子树(cur)进行左单旋
		RotateL(parent->_left); // 对应图上的30
		// 第二步:对 parent 进行右单旋
		RotateR(parent); // 对应图上的90

		// 注意:只有30,60,90这3个节点的平衡因子才会改变
		// parent指向的是90这个节点
		// cur指向的是30这个节点
		// curright指向的是60这个节点

		// 注意我们的左右双旋是为了调整parent->_bf == -2 && cur->_bf == 1这种情况的
		//  旋转完成后,根据原 curright 的平衡因子更新相关节点的平衡因子
		if (bf == 0) // curright 自身就是新插入节点(情况1)
		{
			// 目前是parent->_bf == -2 && cur->_bf == 1 && bf == 0
			// 左旋后:cur->buf=0 ,bf = -1,parent->_bf == -2
			// 右旋后: cur->buf=0 ,bf = 0,parent->_bf == 0
			parent->_bf = 0;
			cur->_bf = 0;
			curright->_bf = 0; // 因为curright本身就是新插入的节点,没有左右子树
		}
		else if (bf == -1) // 新节点插入在 curright 的左子树(情况2)
		{
			// 这个情况就和我们的动图是一模一样了
			parent->_bf = 1;
			cur->_bf = 0;
			curright->_bf = 0;
		}
		else if (bf == 1) // 新节点插入在 curright 的右子树(情况3)
		{
			// 目前是parent->_bf == -2 && cur->_bf == 1 && bf == 0
			// 左旋后:cur->buf=0 ,bf = -1,parent->_bf == -2
			// 右旋后: cur->buf=0 ,bf = 0,parent->_bf == 0
			parent->_bf = 0;
			cur->_bf = -1;
			curright->_bf = 0;
		}
	}

	// 递归计算以 root 为根的子树的高度
	int Height(Node *root)
	{
		if (root == nullptr)
			return 0;

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

		if (leftHeight > rightHeight)
		{
			return leftHeight + 1;
		}
		else
		{
			return rightHeight + 1;
		}
	}

	// 递归检查以 root 为根的子树是否平衡
	bool IsBalance(Node *root)
	{
		if (root == nullptr)
			return true;

		int leftHight = Height(root->_left);   // 左子树高度
		int rightHight = Height(root->_right); // 右子树高度

		// 检查平衡因子是否正确(右子树高度 - 左子树高度 应与存储的平衡因子相等)
		if (rightHight - leftHight != root->_bf)
		{
			cout << "平衡因子异常:" << root->_kv.first << "->" << root->_bf << endl;
			return false;
		}

		// 检查当前节点是否平衡(高度差绝对值小于2)并且左右子树也平衡
		return abs(rightHight - leftHight) < 2 && IsBalance(root->_left) && IsBalance(root->_right);
	}

private:
	Node *_root = nullptr; // 根节点指针,初始化为空

public:
	int _rotateCount = 0; // 旋转计数器,公开以便外部查看(如测试时统计旋转次数)
};

九.小测试

那么我们现在就写一个完整的代码来对我们写好的这个AVL树的插入操作来进行测试

cpp 复制代码
#include "AVLTree.hpp"
#include<iostream>   // 输入输出流
using namespace std;

void TestAVLTree1() {
    // 测试递增序列(右右情况,应触发左旋)
    AVLTree<int, int> tree;
    cout << "测试递增序列: 插入 1,2,3,4,5,6,7,8,9" << endl;
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    for (auto e : a) {
        tree.Insert(make_pair(e, e));
        cout << "插入 " << e << " 后,树是否平衡: " << (tree.IsBalance() ? "是" : "否") 
             << ",高度: " << tree.Height() << ",旋转次数: " << tree._rotateCount << endl;
    }
    cout << endl;
}

void TestAVLTree2() {
    // 测试递减序列(左左情况,应触发右旋)
    AVLTree<int, int> tree;
    cout << "测试递减序列: 插入 9,8,7,6,5,4,3,2,1" << endl;
    int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
    for (auto e : a) {
        tree.Insert(make_pair(e, e));
        cout << "插入 " << e << " 后,树是否平衡: " << (tree.IsBalance() ? "是" : "否")
             << ",高度: " << tree.Height() << ",旋转次数: " << tree._rotateCount << endl;
    }
    cout << endl;
}

void TestAVLTree3() {
    // 测试复杂序列(包含左右和右左双旋)
    AVLTree<int, int> tree;
    cout << "测试复杂序列: 插入 16,3,7,11,9,26,18,14,15" << endl;
    int a[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
    for (auto e : a) {
        tree.Insert(make_pair(e, e));
        cout << "插入 " << e << " 后,树是否平衡: " << (tree.IsBalance() ? "是" : "否")
             << ",高度: " << tree.Height() << ",旋转次数: " << tree._rotateCount << endl;
    }
    cout << endl;
}

void TestAVLTree4() {
    // 测试重复键(应插入失败)
    AVLTree<int, int> tree;
    tree.Insert(make_pair(5, 5));
    cout << "插入 5 后,再次插入 5: " << (tree.Insert(make_pair(5, 5)) ? "成功" : "失败") << endl;
    cout << "树是否平衡: " << (tree.IsBalance() ? "是" : "否") << endl;
}

int main() {
    TestAVLTree1();
    TestAVLTree2();
    TestAVLTree3();
    TestAVLTree4();
    return 0;
}
相关推荐
叫我一声阿雷吧1 小时前
JS实现响应式导航栏(移动端汉堡菜单)|适配多端+无缝交互【附完整源码】
开发语言·javascript·交互
瓦特what?2 小时前
快 速 排 序
数据结构·算法·排序算法
前路不黑暗@2 小时前
Java项目:Java脚手架项目的文件服务(八)
java·开发语言·spring boot·学习·spring cloud·docker·maven
hetao17338372 小时前
2026-02-13~16 hetao1733837 的刷题记录
c++·算法
毅炼2 小时前
Java 集合常见问题总结(3)
java·开发语言·后端
沐知全栈开发2 小时前
ionic 对话框:深度解析与最佳实践
开发语言
浅念-3 小时前
C++ string类
开发语言·c++·经验分享·笔记·学习
百锦再3 小时前
Java多线程编程全面解析:从原理到实战
java·开发语言·python·spring·kafka·tomcat·maven
Cosmoshhhyyy3 小时前
《Effective Java》解读第38条:用接口模拟可扩展的枚举
java·开发语言