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

学习本博客之前建议看看我前面发布的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)

    // 构造函数:初始化键值对,子节点和父节点默认为nullptr
    RBTreeNode(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非空,更新其父亲为parent
        subLR->_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;

}

相关推荐
哆啦刘小洋4 小时前
T:堆的基本介绍
算法
BreezeJuvenile4 小时前
外设模块学习(5)——DS18B20温度传感器(STM32)
stm32·嵌入式硬件·学习·温度传感器·ds18b20
cimeo4 小时前
【C学习】13-数组使用与运算
学习·c#
初圣魔门首席弟子5 小时前
c++ bug 记录(merge函数调用时错误地传入了vector对象而非迭代器。)
java·c++·bug
AresXue5 小时前
你是否也在寻找二进制和字符串的高效转换算法?
算法
Swift社区5 小时前
从 0 到 1 构建一个完整的 AGUI 前端项目的流程在 ESP32 上运行
前端·算法·职场和发展
RTC老炮5 小时前
webrtc弱网-BitrateEstimator类源码分析与算法原理
网络·人工智能·算法·机器学习·webrtc
郝学胜-神的一滴5 小时前
Linux下的阻塞与非阻塞模式详解
linux·服务器·开发语言·c++·程序人生·软件工程
程序员烧烤5 小时前
【leetcode刷题007】leetcode116、117
算法·leetcode