当前位置: 首页 > news >正文

红黑树实现与原理剖析(上篇):核心规则与插入平衡逻辑

学习本博客之前建议看看我前面发布的AVL树代码,因为AVL树与红黑树有很多相似之处,并且本文对于旋转部分讲解将直接套用AVL旋转代码哦(>,<)

结尾有红黑树完整代码,可以结合来看

//下章将讲解红黑树平衡判断

在二叉搜索树(BST)的基础上,红黑树通过 “颜色标记 + 旋转平衡” 解决了 BST 在极端情况下退化为链表的问题,同时相比 AVL 树的 “严格平衡”,红黑树的插入 / 删除效率更高(旋转次数更少),是c++ STL 中map/set的底层实现。本文结合完整 C++ 代码,从红黑树的核心规则入手,拆解节点设计、插入流程与平衡维护逻辑,帮你彻底搞懂红黑树的 “平衡密码”。

一、为什么需要红黑树?—— 从 BST 的缺陷说起

普通 BST 在插入有序数据(如1,2,3,4)时,会退化为单链表,此时查找 / 插入的时间复杂度从O(log n)骤降为O(n)。为解决这个问题,需要一种 “自平衡二叉搜索树”,常见的有 AVL 树和红黑树:

  • AVL 树:要求左右子树高度差不超过 1(严格平衡),插入 / 删除时旋转次数多,适合查询密集场景;
  • 红黑树:通过 “颜色规则” 维持 “近似平衡”(最长路径不超过最短路径的 2 倍),插入 / 删除时平均旋转次数仅 1~2 次,适合插入 / 删除频繁的场景。

红黑树的核心优势是 “平衡与效率的折中”,这也是它成为工业级标准的关键原因。

二、红黑树的 4 条核心规则(必须牢记)

红黑树的所有平衡操作都围绕以下 4 条规则展开,任何插入 / 删除操作后,都需通过 “变色” 或 “旋转” 恢复这些规则:

  1. 颜色约束:每个节点只能是红色(RED)或黑色(BLACK);
  2. 根节点规则:根节点必须是黑色;
  3. 连续红节点禁止:红色节点的父节点不能是红色(即不允许出现连续两个红色节点);
  4. 黑高一致规则:任意节点到其所有空子孙(nullptr)的路径上,黑色节点的数量必须相等(这个数量称为 “黑高”)。

规则 4 是红黑树 “近似平衡” 的关键 —— 它保证了 “最长路径(一黑一红交替)” 的长度不超过 “最短路径(全黑)” 的 2 倍,从而维持O(log n)的时间复杂度。

三、红黑树的节点设计 —— 数据结构基础

要实现红黑树,首先需要定义 “带颜色标记” 的节点结构。代码中使用模板类设计,支持任意键(K)值(V)类型,同时保留父节点指针(旋转和平衡维护必须依赖父节点关系)。

节点类代码解析

template<class K, class V>
class RBTreeNode
{
public:pair<K, V> _kv;          // 键值对(存储数据)RBTreeNode<K, V>* _left; // 左子节点RBTreeNode<K, V>* _right;// 右子节点RBTreeNode<K, V>* _parent;// 父节点(旋转需维护父子关系)Colour _col;             // 节点颜色(RED/BLACK)// 构造函数:初始化键值对,子节点和父节点默认为nullptrRBTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr){}
};
  • 父节点指针:是红黑树与普通 BST 的核心区别之一 —— 旋转时需要调整 “祖父 - 父 - 子” 三级节点的关系,必须通过父节点指针定位上层节点;
  • 颜色成员:使用enum Colour枚举(RED=0,BLACK=1),简化颜色判断逻辑

四、红黑树的插入流程 —— 从 BST 插入到平衡维护

红黑树的插入分为两步:1. 按 BST 规则插入新节点;2. 检查并修复平衡(若违反规则)。其中第二步是核心,需要根据 “叔叔节点的颜色” 决定用 “变色” 还是 “旋转” 修复。

步骤 1:按 BST 规则插入新节点

插入逻辑与普通 BST 一致:从根节点出发,根据键值大小找到插入位置,创建新节点并链接到父节点。注意新节点默认设为红色—— 原因是:

  • 若插入黑色节点,会直接导致 “插入路径的黑高 + 1”,违反规则 4(黑高一致);
  • 若插入红色节点,仅可能违反规则 3(连续红节点),修复成本更低。
代码解析(BST 插入部分)
bool Insert(const pair<K, V>& kv)
{// 情况1:树为空,直接创建根节点(根节点必须为黑色)if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK; // 规则2:根节点为黑return true;}// 情况2:树非空,按BST规则找插入位置Node* parent = nullptr;Node* pcur = _root;while (pcur){if (pcur->_kv.first < kv.first) // 键值比当前节点大,走右子树{parent = pcur;pcur = pcur->_right;}else if (pcur->_kv.first > kv.first) // 键值比当前节点小,走左子树{parent = pcur;pcur = pcur->_left;}else // 键值已存在,插入失败(红黑树不允许重复键){return false;}}// 创建新节点(默认红色),链接到父节点pcur = new Node(kv);pcur->_col = RED; // 新节点默认红色,降低修复成本if (parent->_kv.first < kv.first){parent->_right = pcur; // 新节点是父节点的右孩子}else{parent->_left = pcur;  // 新节点是父节点的左孩子}pcur->_parent = parent; // 维护父节点指针// 关键:插入后检查平衡(父节点为红时才可能违反规则3)// ... 平衡维护逻辑见下文 ...
}

步骤 2:插入后的平衡维护 —— 核心中的核心

平衡维护的触发条件是 “父节点为红色”(此时新节点 + 父节点为连续红节点,违反规则 3)。需根据 “祖父节点的另一个孩子(叔叔节点)的颜色”,分 3 种场景处理:

场景 1:叔叔节点存在且为红色 —— 仅需变色

适用条件

  • 父节点(parent)为红色;
  • 祖父节点(grandfather)存在(父节点为红则祖父必为黑,否则插入前已违反规则 3);
  • 叔叔节点(uncle,祖父的另一个孩子)存在且为红色。

变色逻辑

  • 父节点(parent)→ 黑色;
  • 叔叔节点(uncle)→ 黑色;
  • 祖父节点(grandfather)→ 红色;
  • 继续向上检查祖父节点的父节点(可能仍存在连续红节点)。

示意图(以父节点是祖父左孩子为例):

// 变色前(违反规则3:parent和pcur均为红)grandfather(BLACK)/                  \
parent(RED)          uncle(RED)  // 叔叔为红/
pcur(RED)  // 新插入节点// 变色后(修复规则3,祖父变红需继续向上检查)grandfather(RED)    // 祖父变红,可能与它的父节点冲突/                  \
parent(BLACK)        uncle(BLACK)  // 父和叔变黑/
pcur(RED)

代码解析

while (parent && parent->_col == RED) // 父节点为红才需要处理
{Node* grandfather = parent->_parent; // 祖父节点(必存在,否则父为根,根不能为红)// 子场景A:父节点是祖父的左孩子(对称处理右孩子场景)if (grandfather->_left == parent){Node* uncle = grandfather->_right; // 叔叔节点(祖父的右孩子)// 场景1:叔叔存在且为红 → 仅变色if (uncle && uncle->_col == RED){grandfather->_col = RED;    // 祖父变红parent->_col = BLACK;       // 父变黑uncle->_col = BLACK;        // 叔变黑// 继续向上检查祖父节点(祖父变红可能与它的父节点冲突)pcur = grandfather;parent = pcur->_parent;}// 场景2和3:叔叔不存在或为黑 → 需要旋转(见下文)else{// ... 旋转逻辑 ...}}// 子场景B:父节点是祖父的右孩子(与左孩子场景对称)else{// ... 对称逻辑 ...}
}
// 最终确保根节点为黑(防止祖父节点变红后根为红)
_root->_col = BLACK;
场景 2:叔叔不存在或为黑色,且新节点在父节点同侧 —— 单旋 + 变色

适用条件

  • 父节点为红,叔叔不存在或为黑;
  • 新节点(pcur)与父节点(parent)在祖父节点的同侧(如父是祖父左孩子,新节点是父的左孩子 → 左左 LL 场景;父是祖父右孩子,新节点是父的右孩子 → 右右 RR 场景)。

处理逻辑

  • 对祖父节点做 “单旋”(LL 场景做右单旋,RR 场景做左单旋);
  • 旋转后调整颜色:父节点→黑色,祖父节点→红色(修复规则 3)。

以 LL 场景(右单旋)为例,示意图

// 旋转前(违反规则3:parent和pcur均为红,叔叔为黑/不存在)grandfather(BLACK)/                  
parent(RED)        uncle(BLACK/不存在)/
pcur(RED)  // 新节点与父同侧(左)// 步骤1:对祖父做右单旋(调整节点关系)parent(RED)/          \
pcur(RED)    grandfather(BLACK)\uncle(BLACK/不存在)// 步骤2:变色(父变黑,祖父变红,修复规则3)parent(BLACK)  // 父节点变黑,消除连续红/          \
pcur(RED)    grandfather(RED)\uncle(BLACK/不存在)

单旋函数实现(右单旋 RotateR):单旋的核心是 “调整三级节点的指针关系”,避免出现悬空指针。以右单旋为例(处理 LL 场景):

// 右单旋:针对“左子树过高”的情况(如LL场景)
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非空,更新其父亲为parentsubLR->_parent = parent;// 2. 把parent挂到subL的右子树subL->_right = parent;parent->_parent = subL;// 3. 把subL挂到原祖父节点(pparent)的对应位置if (parent == _root) // 原parent是根节点,更新根为subL{_root = subL;subL->_parent = nullptr;}else{if (pparent->_left == parent) // 原parent是祖父的左孩子pparent->_left = subL;else // 原parent是祖父的右孩子pparent->_right = subL;subL->_parent = pparent;}
}

场景 2 代码解析(LL 场景右单旋 + 变色):

else // 叔叔不存在或为黑
{// 场景2:新节点与父节点同侧(LL场景)if (pcur == parent->_left){RotateR(grandfather); // 对祖父做右单旋// 变色:父节点变黑,祖父节点变红parent->_col = BLACK;grandfather->_col = RED;}// 场景3:新节点与父节点异侧(LR场景,见下文)else{// ... 双旋逻辑 ...}
}
场景 3:叔叔不存在或为黑色,且新节点在父节点异侧 —— 双旋 + 变色

适用条件

  • 父节点为红,叔叔不存在或为黑;
  • 新节点(pcur)与父节点(parent)在祖父节点的异侧(如父是祖父左孩子,新节点是父的右孩子 → 左右 LR 场景;父是祖父右孩子,新节点是父的左孩子 → 右左 RL 场景)。

处理逻辑

  • 先对父节点做 “单旋”(LR 场景做左单旋,RL 场景做右单旋),将异侧场景转化为同侧场景;
  • 再对祖父节点做 “单旋”(LR 场景做右单旋,RL 场景做左单旋);
  • 旋转后调整颜色:新节点→黑色,祖父节点→红色(修复规则 3)。

以 LR 场景(先左旋父,再右旋祖父)为例,示意图

// 旋转前(违反规则3:parent和pcur均为红,叔叔为黑/不存在)grandfather(BLACK)/                  
parent(RED)        uncle(BLACK/不存在)\pcur(RED)  // 新节点与父异侧(右)// 步骤1:对父节点做左单旋(转化为LL场景)grandfather(BLACK)/                  
pcur(RED)        uncle(BLACK/不存在)/
parent(RED)// 步骤2:对祖父节点做右单旋pcur(RED)/          \
parent(RED)    grandfather(BLACK)\uncle(BLACK/不存在)// 步骤3:变色(新节点变黑,祖父变红,修复规则3)pcur(BLACK)  // 新节点变黑,消除连续红/          \
parent(RED)    grandfather(RED)\uncle(BLACK/不存在)

场景 3 代码解析(LR 场景双旋 + 变色):

else // 新节点与父节点异侧(LR场景)
{RotateL(parent);        // 第一步:对父节点做左单旋RotateR(grandfather);   // 第二步:对祖父节点做右单旋// 变色:新节点(pcur)变黑,祖父节点变红pcur->_col = BLACK;grandfather->_col = RED;
}

五、上篇总结:红黑树插入的核心逻辑

红黑树的插入平衡本质是 “根据叔叔节点颜色选择修复方式”:

  1. 叔叔为红 → 仅变色,继续向上检查;
  2. 叔叔为黑 / 不存在 → 按新节点与父节点的位置关系,选择单旋或双旋 + 变色。

通过这三种场景的处理,最终能恢复红黑树的 4 条规则,维持 “近似平衡”。下篇将重点讲解红黑树的 “平衡验证逻辑”(如何判断插入后的树是否合法)、测试用例设计与常见问题调试技巧,帮你实现 “从代码到验证” 的完整闭环

对了,这里附上我自己写的完整红黑树代码,里面有很多注释哦,大家也可以结合代码看文章:
//可以复制到编译器,这样子看起来更舒服

#include <iostream>
using namespace std;

enum Colour
{
RED,
BLACK
};

//RBTree的实现
//红黑树规则
//1, 只能由红黑两个色
//2,根节点必须是黑色
//3,红色节点的两个孩子不可以是红色,即不可以同时出现连续两个红色节点
//4,任意一个节点到左右nullptr的最简路径出现的黑色节点必须相等
//假如一条路径有x个黑节点
//4说明了最短路径为x(即都是黑节点)
//234共同说明最长路径为2x(即一黑一红这样排列直到有x个黑节点)
namespace ym
{
template<class K, class V>
class RBTreeNode
{
public:
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;

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

    //K->key(比较一般使用关键词key)
template<class K, class V>
class RBTree
{
using Node = RBTreeNode<K, V>;
public:

        RBTree() = default; //强制生成默认构造

        bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
else
{
Node* parent = nullptr;
Node* pcur = _root;
while (pcur)
{
if (pcur->_kv.first < kv.first)
{
parent = pcur;
pcur = pcur->_right;
}
else if (pcur->_kv.first > kv.first)
{
parent = pcur;
pcur = pcur->_left;
}
else
{
return false;
}
}
pcur = new Node(kv);
pcur->_col = RED;
if (parent->_kv.first < kv.first)
{
parent->_right = pcur;
}
else
{
parent->_left = pcur;
}
// 链接父亲
pcur->_parent = parent;

                while (parent && parent->_col == RED) //如果父亲是红色,说明了出现了连续的两个红色节点,需要继续处理
{
Node* grandfather = parent->_parent;
//各个字母的含义
//g -> grandfather (父亲节点的父亲)
//p -> parent
//c -> children (父亲节点的孩子, 即插入的节点)
//u -> uncle (父亲的父亲节点的另一个孩子, 即和parent同高度的另一个节点)

                    //不管怎么插入什么,必须插入红色节点,因为插入黑色节点,就必然违反规则4,红色节点可能违反3,但也可能
//不违反,所以必须插入红色节点,并且parent节点在旋转后可能变为黑色,不然可能违反规则三
//简单表述就是插入必定是红色节点, parent可能会变色

                    //分三种大情况去讨论
if (grandfather->_left == parent)
{
//  g
// p u
//c
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
//第一种大情况(只变色)
//g为黑, p为红, c为红, u为红且存在
//-->g变为红, p变为黑, c变为红, u变为黑
//但是还没完,因为g的parent可能也是红色,所以继续上述过程(前提是满足g为黑, p为红, c为红, u为红且存在,不然就是别的情况了)
//直到g的parent为黑或者g到达了根节点(此时直接把g变为黑色,满足规则1)才可以退出
//变色
grandfather->_col = RED;
parent->_col = BLACK;
uncle->_col = BLACK;
//继续往上处理
pcur = grandfather;
parent = pcur->_parent;
}
else
{
//下面简绍要旋转的两种情况,区别是pcur插入的位置
//             双旋    单旋
//pcur插入在parent异侧还是同侧
if (pcur == parent->_left)
{
//第二种大情况(单旋+变色)
//g为黑, p为红, c为红且插入在parent同侧, u为黑或者不存在 这个情况也暗示了c是插入到中间了,因为为了保持规则四,说明p下面肯定还有黑色节点,不然左边肯定比右边多
//假如pcur在parent的左边,就使用右单旋,反之使用左单旋
//然后改变颜色

                                //下面演示右单旋
//  (黑)g            (红)p           (黑)p
// (红)p u(黑)  ->  (红)c g(黑) ->  (红)c g(红)
//(红)c                    u(黑)           u(黑)

                                //单旋
RotateR(grandfather);
//变色
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
//第三种大情况(双旋+变色)
//g为黑, p为红, c为红且插入在parent异侧, u为黑或者不存在 这个情况也暗示了c是插入到中间了,因为为了保持规则四,说明p下面肯定还有黑色节点,不然左边肯定比右边多
//假如pcur在parent的右边(前提是parent在grandfather的左边),就先使用左单旋,再使用右单旋
//然后改变颜色

                                //双旋
RotateL(parent);
RotateR(grandfather);
//变色
pcur->_col = BLACK;
grandfather->_col = RED;
}
}
}
else
{
//与上面都相反
// g
//u p
Node* uncle = grandfather->_left;
// 叔叔存在且为红,变⾊即可
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续往上处理
pcur = grandfather;
parent = pcur->_parent;
}
else // 叔叔不存在,或者存在且为⿊
{
// 旋转+变⾊
if (pcur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateR(parent);
RotateL(grandfather);
pcur->_col = BLACK;
grandfather->_col = RED;
}
}
}
}
_root->_col = BLACK;
return true;
}
}

        // 单旋是纯粹的一边高
void RotateR(Node* parent) //左边多,右单旋
{
Node* subL = parent->_left; //左边
Node* subLR = subL->_right; //左边的右边
Node* pparent = parent->_parent; //父亲的父亲节点
//          6(p)
//     4(L)       7
//   3   5(LR)
//(插入)
parent->_left = subLR;
if (subLR) //非空就可以修改
subLR->_parent = parent;
parent->_parent = subL;
subL->_right = parent;
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;
if (subRL)
subRL->_parent = parent;
parent->_right = subRL;
subR->_left = parent;
parent->_parent = subR;
if (_root == parent)
{
subR->_parent = nullptr;
_root = subR;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subR;
}
else
{
pparent->_right = subR;
}
subR->_parent = pparent;
}
}

        bool Find(const K& key)
{
Node* pcur = _root;
while (pcur)
{
if (pcur->_kv.first < key)
{
pcur = pcur->_right;
}
else if (pcur->_kv.first > key)
{
pcur = pcur->_left;
}
else
{
return true;
}
}
return false;
}

        bool IsBalence()
{
return _IsBalence(_root);
}

        void InOrder()
{
_InOrder(_root);
}

    private:
void _InOrder(const Node* root)
{
if (root == nullptr)
{
//cout << "Nullptr ";
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}

        bool check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
if (refNum != blackNum)
{
cout << "任意节点到REF(nullptr节点)最简路径上的black节点数量不相等,不满足规则4" << endl;
return false;
}
return true;
}
if (root->_col != RED && root->_col != BLACK)
{
cout << "出现了其他颜色的节点,不满足规则1" << endl;
return false;
}
if (root != _root && root->_col == RED && root->_parent->_col == RED)
{
cout << "出现了两个连续的红色节点,不满足规则3" << endl;
return false;
}
if (root->_col == BLACK)
{
blackNum++;
}
return check(root->_left, blackNum, refNum) && check(root->_right, blackNum, refNum);
}

        bool _IsBalence(Node* _root)
{
if (_root == nullptr)
{
return true;
}

            if (_root->_col == RED)
{
cout << "根节点颜色错误,不符合规则2" << endl;
return false;
}

            int refNum = 0; //这里使用refNum记录根节点到REF(nullptr节点)的路径上black的个数来判断是否任意节点到REF节点都满足规则4
Node* pcur = _root; 

while (pcur)
{
if (pcur->_col == BLACK)
{
refNum++;
}
pcur = pcur->_left;
}

return check(_root, 0, refNum);
}

        Node* _root = nullptr;
};
}

void TestRBTree1()
{
ym::RBTree<int, int> t;
// 常规的测试⽤例
int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
// 特殊的带有双旋场景的测试⽤例
//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto& e : a)
{
t.Insert({ e, e });
}

    t.InOrder();

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

int main()
{
TestRBTree1();
return 0;
}

http://www.dtcms.com/a/477637.html

相关文章:

  • 【AES加密专题】8.实战-测试加密网站和代码
  • 收费的电影网站怎么做可以打开任何网站的软件
  • 设计广告网站wordpress怎么换空间
  • React 18并发模式解析:Fiber架构与性能优化技巧
  • 火山引擎多媒体实验室画质理解大模型Q-Insight入选NeurIPS 2025 Spotlight
  • 【StarRocks】-- DATETIME 与 TIMESTAMP 区别详解
  • k8s nginx ingress介绍
  • 深入starrocks-怎样实现多列联合统计信息
  • 无锡百度网站推广廊坊seo优化排名
  • 小程序如何接入火山引擎埋点数据
  • 汝阳网站建设哪家好旅游社网站建设规划书
  • Qt MSVC_64bit在Release模式下调试与WinDbg调试exe
  • Flutter鸿蒙开发
  • 《Qt应用开发》笔记p2
  • 保定网站建设与seo贵州快速整站优化
  • SOLIDWORKS转换为3DXML全流程技术指南:附迪威模型网在线方案
  • 【Java Xml】Apache Commons Digester3解析
  • 一文读懂微软 MOS 国际认证
  • 微软Defender for Endpoint漏洞3个月未修复,攻击者可绕过认证并上传恶意文件
  • 柱状图的高级玩法:分组、堆叠、百分比对比
  • 湖南金科建设有限公司网站那些网站是做俄罗斯鞋子
  • 详解Jenkins 的 Declarative Pipeline中post 语法
  • 淘宝客怎么在网站做推广上海新闻坊
  • 无人机中继器模式技术对比
  • HTTP与HTTPS:从明文到加密的Web安全革命
  • LINUX1013 shell:sed ./sed.sh 1.txt sed -f sed.sh 1.txt awk
  • 无人机技术解析:遥传、数传与图传的核心作用
  • 反无人机和反无人机系统(C-UAS)技术
  • 基于ARM+FPGA的无人机数据采集卡,6通道24bit采集
  • 扬州哪里做网站玉树营销网站建设公司