【C++ STL篇(十一)】深入浅出红黑树:从原理到实现,一篇搞定

C++ STL篇(十一) ------ 红黑树详解

上篇文章中我们从零开始,深入学习了 AVL树的底层逻辑,本篇将由浅入深,带你循序渐进地掌握红黑树,全程干货,坐稳发车~ ദ്ദി˶ー̀֊ー́ )✧

文章目录

  • [C++ STL篇(十一) ------ 红黑树详解](#C++ STL篇(十一) —— 红黑树详解)

一、红黑树的概念

首先,红黑树是一棵二叉搜索树 (BST),它拥有二叉搜索树的全部特性:左子树所有节点的值 < 根节点的值 < 右子树所有节点的值。

但它比普通二叉搜索树多出来的一样东西是:每个节点多了一个存储位,用来表示该节点的"颜色"------非红即黑。正是通过对红黑颜色的严格约束,红黑树才保证了树的形状大致平衡。

1.1 红黑树的四条铁律

红黑树必须同时满足以下 4 条规则,缺一不可:

  1. 节点颜色非红即黑:每个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色
  3. 红色节点的孩子必须是黑色 :也就是说,从根到叶子的任何一条简单路径上,不允许出现两个相邻的红色节点
  4. 每条路径上的黑色节点数量相等:从任意一个节点出发,到其所有后代空节点(NULL / NIL)的简单路径上,包含的黑色节点数目必须相同。

这里看几个红黑树的图帮助理解:

补充:关于"叶子节点"

有些教材(比如《算法导论》)还会额外加一条规则:"每个叶子节点(NIL)都是黑色的"。这里的"叶子"并不是我们平时说的没有孩子的那个节点,而是指空节点 ,也就是 NULL 指针所在的位置,有时也叫"外部节点"。

引入这个 NIL 黑色叶子可以让"路径"的定义更加统一(所有路径都终结于一个黑色 NIL),方便理论分析。《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道一下这个概念即可。

这里我们还要注意一个点 ,你觉得下面这个红黑树中一共有几条路径?

很多人可能会把路径数当成叶子节点的个数,认为有4条路径,这样就大错特错了!

正确答案是9条路径,因为路径是从根节点走到空节点,并不是到叶子节点。

1.2 为什么最长路径不会超过最短路径的两倍?

这可能是很多人最好奇的问题:不用记录高度,只靠颜色规则,怎么就能保证树不"退化"呢?

我们做两个极端假设,推理一下:

  • 规则 4 告诉我们:每条从根到 NULL 的路径上,黑色节点的数量是相等的。我们把这个数量记作 bh(black height,黑高)。
  • 那么,一条路径上最短的情况是什么呢?全是黑色节点 ------因为规则不禁止黑色连续出现,而且这样节点数最少。此时最短路径长度就是 bh
  • 最长的情况呢?在保持黑节点数为 bh 的前提下,我们尽量塞入红色节点。规则 3 规定红节点不能相邻,所以只能在一对黑节点之间"插空"放一个红节点,形成黑-红-黑-红 交替的样子。这样一来,路径上的红色节点最多也只能是 bh 个(每个黑节点后面跟一个红节点,或者相反)。因此最长路径的长度就是 bh + bh = 2*bh

比较一下:最短路径长度为 bh,最长路径长度最多为 2*bh。所以,最长路径永远不会超过最短路径的 2 倍

注意,这只是理论上的极限情况。实际的红黑树很少长成全黑或完美红黑交替,大多数时候路径长度介于 bh2*bh 之间。但有了这个上界保证,树的高度就被限制在了 (O(log N)) 级别。

1.3 效率怎么样?

设节点总数为 (N),树的高度为 (h)。根据二叉树的节点数下限:

所以高度 (h) 最坏也是 (O(log N))。那么插入、删除、查找时顺着路径走到头,时间复杂度自然就是 (O(log N)) 了。

和 AVL 树相比,两者效率在同一量级。但红黑树对平衡的要求更"宽松"(AVL 要求高度差不超过 1),所以在插入、删除时红黑树需要的旋转次数往往更少,综合性能更优。


二、红黑树的节点结构

先看看节点是如何定义的:

cpp 复制代码
enum Colour
{
    RED,
    BLACK
};

template<class K, class V>
struct RBTreeNode
{
    pair<K, V> _kv;           // 键值对
    RBTreeNode* _left;        // 左孩子
    RBTreeNode* _right;       // 右孩子
    RBTreeNode* _parent;      // 父节点 ------ 为旋转和回溯做准备
    Colour _col;              // 颜色

    RBTreeNode(const pair<K,V>& kv)
        :_kv(kv)
        ,_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_col(RED)            // 新节点默认红色,原因马上揭晓
    { }
};

这里有几个关键点:

  • _parent 指针的三叉链结构:比起普通二叉搜索树只存左右孩子,这里多了一个指向父节点的指针。这是为了方便在插入后向上回溯检查规则,以及在进行旋转操作时能快速调整父节点的指向,不用单独维护栈来记录路径。
  • 颜色枚举 Colour :只有 REDBLACK 两种,规则1在代码层面直接由类型保证。
  • 新节点的默认颜色是 RED :这是红黑树插入策略中非常重要的一环。为什么不是黑色?
    • 如果新插入一个黑色 节点,它会立刻增加这条路径上的黑色节点数量,导致它所在路径的黑高比其他路径多 1,直接违反规则4。规则4是全局性的,破坏后需要大范围调整,非常难修复。
    • 如果新插入一个红色 节点,最坏情况是它的父节点也是红色,此时违反规则3(连续红色)。规则3是局部性的,只涉及祖孙三代,调整起来相对容易。两害相权取其轻,所以新节点一律标红。

三、红黑树插入的总体流程

插入一个键值对 kv 的过程分为两步:

  1. 按二叉搜索树的规则找到位置,插入新节点。
  2. 如果新节点的父节点是黑色,则插入结束,无需调整。
  3. 如果新节点的父节点是红色,就触发了"红红冲突",需要根据叔叔节点(父节点的兄弟)的情况进行分类修正,直到整棵树重新满足红黑树规则。

在接下来的讨论中,我们统一用以下代号称呼相关节点:

  • cur(或 c):当前关注的节点。一开始就是刚插入的红色新节点,调整过程中可能会向上移动。
  • parent(或 p):cur 的父节点。
  • grandfather(或 g):parent 的父节点,cur 的祖父。
  • uncle(或 u):parent 的兄弟节点,也就是 grandfather 的另一个孩子。

因为红黑树的规则对称,我们这里重点讨论 parentgrandfather 左孩子的情况,另一侧(parent 是右孩子)镜像对称即可。

curparent 都是红色时,grandfather 必定是黑色 (不然插入前就不满足规则3了)。现在 cur 红、parent 红、grandfather 黑这三个颜色已经定死,问题的突破口就在 uncle 身上。根据 uncle 的颜色和是否存在,可以分为以下三种情况。


四、插入修正的三种情况(图文拆解)

4.1 情况1:叔叔为红色 ------ 变色 + 向上回溯

条件: cur 为红,parent 为红,grandfather 为黑,并且 uncle 存在且为红色

4.1.1 cur 是新增节点

情形示意(parent 是左孩子):

cp 的左右孩子均可,情况1的处理不关心方向)

分析:

  • pu 是红色,g 是黑色。
  • 现在 cp 连续红,破坏了规则3。
  • 同时我们也不想破坏规则4(黑高不变)。

解决策略:变色

  • pu 由红变黑。
  • g 由黑变红。

为什么这样可行?

  • g 往下看,左右两条路径原本各有 1 个黑色节点(g 自己是黑,子树内可能还有更多黑节点,但这里只看与 p/u 直接相关的部分)。现在 pu 变黑,它们的路径上多了一个黑色节点;g 变红,相当于 g 把自己的那个黑色分给了两个儿子。这样一来,左右路径上的黑色节点总数保持不变,规则4依然满足。
  • 同时,cp 的红红冲突也解除了,因为 p 变成了黑色。
  • 但是,g 被染成了红色,如果 g 的父节点(即 c 的曾祖父)恰好也是红色,那么就会在更高层引发新的红红冲突。这就是为什么需要将 cur 指针指向 gg 当作新的 cur 继续向上调整
  • 如果 g 就是整棵树的根节点,最后我们会强制把它恢复为黑色,满足规则2,调整结束。
  • 如果 g 的父节点是黑色,则直接结束,万事大吉。
4.1.2 cur 不是新增节点

上面我们讨论了cur是新增节点时的一种情况,,但是实际中需要这样处理的有很多种情况。

为了能更直观的把握,我们使用抽象图来表示子树。抽象图虽然看起来简单,但它能覆盖成百上千种具体的节点形态------这正是红黑树调整的精妙之处:无论子树多复杂,只要结构关系符合,修正操作完全一致

图中长方形 abcdef 表示符合红黑树规则的子树,并且它们都带有一些黑色节点数(黑高)。

d/e/f代表每条路径拥有hb个黑色结点的子树,

a/b代表每条路径拥有hb-1个黑色结点的根为红色的子树

具体场景分析:

场景一: hb == 0

如果 hb == 0 ,此时a/b/c/d/e/f 都为空,这时的情况就是我们上面提到过的cur为新增节点时的情况。

场景二: hb == 1

此时子树可能的情况:

图解:

d/e/f 为hb==1 的红黑树:

  • c之前是黑色节点,ab中插入引发c变色为红色
  • d/e/f为x/y/z/m中任意一种,组合为 4x4x4
  • ab为红色节点,在ab的四个孩子的任意位置插入,都会让ab变成黑色,c变成红色,继续往上更新,插入位置有4个位置。
  • 所有情况组合起来合计:4x4x4x4 = 256

场景三 : hb == 2

此时子树可能的情况:


图解(这里a/b/c/d/e/f 内部可能是各种组合,我们只展示了其中一种):

d/e/f为hb == 2的红黑树,a和b是 hb == 1的根为红色的树:

  • d/e/f的组合为:(256+16)x(256+16)x(256+16) = 20123648
  • a和b为根节点为红色节点的hb==1的树,这里可以看到a和b插入组合也不少
  • a或者b插入至少要经历两次变色和向上处理才能得到这里的情况,这里的组合情况至少是百亿以上了。

hb == 2 的情况已经达到百亿以上了,hb>=3 的情况肯定超级的多,这里我们就不展开分析了。

小结一下:

情况1只进行了变色 ,没有旋转。无论 cp 的左孩子还是右孩子,pg 的左孩子还是右孩子,处理手段都完全一样:pu 变黑,g 变红,然后向上回溯。


4.2 情况2:叔叔为黑/不存在且呈直线 ------ 单旋 + 变色

(这里我们重点分析右单旋的情况,左单旋同理)

条件: cur 为红,parent 为红,grandfather 为黑,且 uncle 不存在 或者存在但为黑色

同时,curparentgrandfather 三者在一条直线上:即 curparent 的左孩子且 parentgrandfather 的左孩子(左左 ),或 curparent 的右孩子且 parentgrandfather 的右孩子(右右)。

4.2.1 cur 为新增节点 (此时uncle一定不存在)


分析:

  • cp 连续红,必须解决。
  • u 是不存在,单纯把 p 变黑并不能平衡,因为如果把 p 变黑,左路径黑高增加1,而右路径那边黑高不变,就违反了规则4。我们需要通过旋转来改变树的结构,同时配合变色来维持黑高。

操作:右单旋 + 变色

  1. grandfatherg)为支点进行右单旋
  2. 旋转后,parentp)成为新子树的根。
  3. p 的颜色改为黑色g 的颜色改为红色

新的根 p 是黑色,如果它上面还有父节点,无论父节点是红还是黑都不会再引发冲突(因为规则3只要求红节点孩子为黑,黑节点的孩子可以是任意颜色)。因此,调整到此结束,不需要再向上回溯 ,可以直接 break

为什么u不存在的时候,c一定是新增节点?

如果cur不是新增,那它在最初只能是黑色的,经过调整之后才变红的(在情况1的调整过程中,由黑色变成红色然后向上回溯上来的"老"节点),这样就不满足每条路径上的黑色节点相同这个条件了。

4.2.2 cur 不是新增节点(此时uncle一定存在且为黑色)

因为uncle为黑色,此时我们就不能使用情况一的调整规则,需要旋转+变色来进行调整。

为什么u存在且为黑的时候,c一定不是新增节点?

插入一个红色节点,如果叔叔是黑色,那说明在插入前,p 所在路径的黑高比 u 所在路径少,这不符合规则4!


4.3 情况3:叔叔为黑/不存在且呈折线 ------ 双旋 + 变色

(这里我们重点分析先左旋后右旋的情况,先右旋后左旋同理)

条件: 同样满足 cur 红、parent 红、grandfather 黑,uncle 不存在或为黑,但这次 curparentgrandfather 三者不在一条直线上,而是呈折线状:curparent 的右孩子,而 parentgrandfather 的左孩子(左右 ),或 curparent 的左孩子,而 parentgrandfather 的右孩子(右左)。

4.3.1 cur是新增节点

这种情况直接对 g 做右单旋是没有用的,旋转后 c 依然会是某个节点的红色孩子,仍然违反规则。正确的做法是进行两次旋转,把折线"掰直"。

操作:双旋 + 变色

  1. 先以 parentp)为支点进行左单旋 。此时结构变为 c 在上,p 在左下的直线形态。
  2. 再以 grandfatherg)为支点进行右单旋
  3. 最后,将旋转后处于子树根位置的 cur 染成黑色 ,将 grandfatherg)染成红色
  4. 新根是黑色,不再需要向上回溯,调整直接结束。
4.3.2 cur不是新增节点

这里同样需要进行两次旋转,第一次以p为旋转点进行左单旋,将结构拉直,第二次以g为旋转点进行右单旋,两次旋转结束后将处于子树根位置的 cur染成 黑色 ,将 grandfatherg)染成 红色。完成插入修正。


五、插入代码

有了上面的理论铺垫,我们再来读代码,就会觉得每一行都落在预料之中。下面是 Insert 函数的完整实现。

cpp 复制代码
	bool Insert(const pair<K, V>& kv)
	{
		// 1. 空树处理
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;   // 根必须是黑色
			return true;
		}

		// 2. 按照二叉搜索树规则查找插入位置
		Node* cur = _root;
		Node* parent = nullptr;
		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;     // 不允许重复键值
			}
		}

		// 3. 创建新节点(默认红色)并链接到父节点
		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
			parent->_right = cur;
		else
			parent->_left = cur;
		cur->_parent = parent;

		// 4. 如果父节点是黑色,插入结束;如果父节点是红色,进入修正循环
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent; // 父红则祖父必然存在
			if (grandfather->_left == parent)    // 父是祖父的左孩子
			{
				Node* uncle = grandfather->_right;
				// 情况1:叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					// 把祖父当作新的cur,继续向上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 叔叔不存在或为黑
				{
					if (cur == parent->_left)//单旋+变色
					{
						RotateR(grandfather);

						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else // 父是祖父的右孩子(对称处理)
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED) // 情况1
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		// 5. 强制根为黑色,防止情况1把根染红
		_root->_col = BLACK;
		return true;
	}

六、旋转操作的实现

红黑树的旋转和 AVL 树的旋转完全一样。因为我们维护了父指针,旋转时需要小心地更新三个节点的父子关系。

6.1 右单旋(RotateR)

旋转点 parent 是要被拉下来的节点(图中的 g)。右旋后,它的左孩子 subL 会成为新的根。

cpp 复制代码
void RotateR(Node* parent)
{
    Node* subL = parent->_left;      // 左孩子
    Node* subLR = subL->_right;      // 左孩子的右子树
    Node* pParent = parent->_parent; // 原父节点

    // 1. 处理 subLR:把它挂到 parent 的左子
    parent->_left = subLR;
    if (subLR)
        subLR->_parent = parent;

    // 2. 处理 parent:把它挂到 subL 的右子
    subL->_right = parent;
    parent->_parent = subL;

    // 3. 处理 subL 与原父节点的关系
    if (parent == _root)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        if (pParent->_left == parent)
            pParent->_left = subL;
        else
            pParent->_right = subL;
        subL->_parent = pParent;
    }
}

6.2 左单旋(RotateL)

与右单旋完全对称。

cpp 复制代码
void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* pParent = parent->_parent;

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

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

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

七、红黑树的查找

查找操作完全就是标准二叉搜索树的查找,不用关心颜色。时间复杂度 (O(log N))。

cpp 复制代码
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;
}

八、如何验证一棵树是不是红黑树

写完代码后,我们必须有手段来检查树是否满足所有红黑树性质。有些人可能会想:先算一下最长路径和最短路径,看看最长是不是小于等于最短的2倍不就行了?

这是不够的! 满足"最长路径 <= 2×最短路径"的树不一定满足红黑树的颜色规则,它可能只是"歪打正着"的平衡。如果不按照颜色规则去维护,后续继续插入时迟早会崩。

因此,我们应该正面检查四条规则

  1. 规则1(颜色只有红或黑) :代码中用枚举类型 Colour 赋值,天然保证。
  2. 规则2(根节点是黑色) :直接检查 _root->_col == BLACK
  3. 规则3(没有连续红色节点):遍历过程中,如果当前节点是红色,检查其父节点是否为红色(注意根节点没有父节点,但它是黑色,不会进此判断)。
  4. 规则4(每条路径黑色节点数相等) :先计算出一条"最左路径"上的黑色节点数目作为参考值 refNum,然后递归遍历所有路径,对于每条路径,一边走一边统计黑节点数,走到空节点时与参考值比较是否相等。

下面是验证函数 IsBalanceTree 及其辅助递归函数 RBCheck 的实现:

cpp 复制代码
bool IsBalanceTree()
{
    if (_root == nullptr) 
    	return true;

    // 检查规则2:根是否为黑
    if (_root->_col == RED)
        return false;

    // 计算参考黑高:沿着最左路径走到底,统计黑色节点数
    int refNum = 0;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_col == BLACK)
            refNum++;
        cur = cur->_left;
    }

    // 递归检查规则3和规则4
    return RBCheck(_root, 0, refNum);
}

bool RBCheck(Node* root, int blackNum, int refNum)
{
    // 走到空节点:一条路径结束,检查黑高是否等于参考值
    if (root == nullptr)
    {
        if (blackNum != refNum)
        {
            cout << "存在黑色节点数量不相等的路径!" << endl;
            return false;
        }
        return true;
    }

    // 检查规则3:当前节点为红时,父节点不能为红
    if (root->_col == RED && root->_parent->_col == RED)
    {
        cout << "存在连续的红色节点!" << endl;
        return false;
    }

    // 累加黑色节点
    if (root->_col == BLACK)
        blackNum++;

    // 递归检查左右子树
    return RBCheck(root->_left, blackNum, refNum) 
        && RBCheck(root->_right, blackNum, refNum);
}

这里有个细节:我们取参考黑高时走的是最左路径,但实际上取任意一条路径都可以,因为只要树满足规则4,所有路径的黑高都相同。走最左路径只是图个方便。


九、完整代码

将上面的所有代码整合,加上一些辅助函数(中序遍历 Inorder、求节点数 Size、求高度 Height),就可以跑起来了。

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;

enum Colour
{
	RED,
	BLACK
};

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

	Colour _col;

	RBTreeNode(const pair<K,V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_col(RED)
	{ }
};

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:

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

		Node* cur = _root;
		Node* parent = nullptr;

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

		cur = new Node(kv);

		if (parent->_kv.first < kv.first)
			parent->_right = cur;
		else
			parent->_left = cur;

		cur->_parent = parent;

		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_left)//单旋+变色
					{
						RotateR(grandfather);

						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
						
		}
		_root->_col = BLACK;

		return true;
	}

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

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

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

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

			subL->_parent = pParent;
		}
	}

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

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

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

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

			subR->_parent = pParent;
		}
	}

	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;
	}

	int Size()
	{
		return _Size(_root);
	}

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

	void Inorder()
	{
		_Inorder(_root);
	}

	bool IsBalanceTree()
	{
		if (_root == nullptr)
		{
			return true;
		}

		if (_root->_col == RED)
		{
			return false;
		}

		int refNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				refNum++;

			cur = cur->_left;
		}

		return RBCheck(_root, 0, refNum);
	}


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

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

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

		return _Size(root->_left) + _Size(root->_right) + 1;
	}

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

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

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}
	
	bool RBCheck(Node* root, int blackNum, int refNum)
	{
		if (root == nullptr)
		{
			if (blackNum != refNum)
			{
				cout << "存在黑色节点数量不相等的路径!" << endl;
				return false;
			}
			return true;
		}

		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "存在连续的红色节点!" << endl;
			return false;
		}

		if (root->_col == BLACK)
		{
			blackNum++;
		}

		return RBCheck(root->_left, blackNum, refNum) 
			&& RBCheck(root->_right, blackNum, refNum);
	}

private:
	Node* _root = nullptr;
};

十、红黑树的删除(简要说明)

红黑树的删除比插入更复杂。基本流程是:

  1. 按二叉搜索树规则删除节点。
  2. 如果删除的是红色节点,不破坏任何规则,直接结束。
  3. 如果删除的是黑色节点,会导致某些路径的黑高减少,需要进行复杂的重新平衡操作(变色、旋转),甚至可能需要从兄弟节点那里"借"一个黑色节点过来。

删除的情况分析多达六、七种,这里不展开。有兴趣深入的同学可以阅读《算法导论》或《STL源码剖析》中的相关章节。对于大多数应用场景,理解插入修正已经能帮你掌握红黑树的核心思想了。


结语:

今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
fqbqrr10 小时前
2605C++,C++继承类实现调试器
开发语言·c++
阿里嘎多学长10 小时前
2026-05-21 GitHub 热点项目精选
开发语言·程序员·github·代码托管
wjs202410 小时前
PHP 面向对象编程(OOP)深入解析
开发语言
Deep-w10 小时前
【MATLAB】基于遗传算法的直流电机 PI 控制器参数优化研究
开发语言·算法·matlab
海清河晏11110 小时前
数据结构 | 循环队列
数据结构·c++·visual studio
wb0430720110 小时前
从 Java 1 到 Java 26 的HTTP Client发展历程
java·开发语言·http
fu159357456811 小时前
【使用python代码制作数学逻辑动画】 ——【教程】
开发语言·python
Shadow(⊙o⊙)11 小时前
Linux基础IO-1.0——open、close、read及write-深入手搓分析!
linux·运维·服务器·开发语言·c++·学习
我是一颗柠檬11 小时前
【JDK8新特性】Stream流API上Day4
java·开发语言·后端