红黑树:从概念到实现

红黑树:从概念到实现

核心口诀:非空插红;父黑结束;父红看叔;叔红变色上推;叔黑旋转加变色;根最后染黑。

红黑树是一种近似平衡的二叉搜索树 。它不追求 AVL 那样严格的左右高度差约束,而是通过给结点增加"颜色"并约束颜色分布,保证从根到叶子的最长路径不会超过最短路径的 2 倍,从而让插入、查找、删除都保持在 O(log N) 量级。


1. 红黑树的概念

红黑树是一棵二叉搜索树,每个结点额外增加一个存储位表示颜色,颜色只能是红色 或黑色。通过对任意一条从根到 NULL 结点的路径上的颜色进行约束,红黑树保证没有一条路径会比另一条路径长出 2 倍,因此是接近平衡的。

1.1 红黑树的四条规则

编号 规则 说明
1 每个结点不是红色就是黑色 颜色类型天然保证
2 根结点是黑色 空树插入后也要染黑
3 红色结点的两个孩子必须是黑色 任意路径不能有连续红结点
4 任意结点到其所有 NULL 结点的简单路径上,黑色结点数量相同 即"黑高"一致

有些教材会把 NULL 显式看作黑色叶结点,本质与"到 NULL 路径黑结点数相同"一致。本文按原稿说法,把 NULL 作为不存储颜色的外部结点,只参与黑高计数。

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

  • 从根到 NULL 的每条路径都有相同数量的黑色结点,设黑高为 bh。
  • 极端最短路径:路径上全是黑色结点,长度为 bh。
  • 极端最长路径:黑色、红色交替出现,由于不能连续红,最长长度为 2 * bh。
  • 因此任意一条从根到 NULL 的路径长度 h 满足:
text 复制代码
bh <= h <= 2 * bh

所以:

text 复制代码
最长路径 / 最短路径 <= 2

重点 :红黑树不是"绝对平衡",而是"黑高平衡 + 无连续红"共同保证的近似平衡。


2. 红黑树的实现

以下采用 KV 模型的教学版实现:每个结点存储 pair<K, V>,便于理解 map/set 的底层思想。删除操作较复杂,本文聚焦插入、查找与验证。

2.1 红黑树的结构

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

enum Colour {
    RED,
    BLACK
};

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

    RBTreeNode(const pair<K, V>& kv)
        : _kv(kv)
        , _col(RED)          // 非空树插入时,新结点默认红色
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
    {}
};

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

public:
    RBTree() : _root(nullptr) {}

    bool Insert(const pair<K, V>& kv);
    Node* Find(const K& key);
    bool IsBalance();

private:
    void RotateL(Node* parent);
    void RotateR(Node* parent);
    bool _Check(Node* root, int blackNum, int refBlackNum);

private:
    Node* _root;
};

结构要点 :三叉链结构 left / right / parent 便于插入修复时向上回溯;新结点构造时默认红色。


2.2 红黑树的插入

2.2.1 插入的大致流程
  1. 按二叉搜索树规则找到插入位置并插入。
  2. 如果是空树插入:新结点作为根,染成黑色。
  3. 如果是非空树插入:新结点默认红色。
  4. 插入后检查是否违反规则:
    • 如果父亲是黑色:没有违反规则,插入结束。
    • 如果父亲是红色:违反"不能连续红",需要根据叔叔结点 u 的情况修复。

记:

  • c:当前结点,cur
  • p:父亲,parent
  • g:祖父,grandfather
  • u:叔叔,uncle

为什么非空插入默认红色?

插入黑色会立刻破坏规则 4,影响整棵树的所有路径,极难维护;插入红色只可能破坏规则 3,且只需局部修复。

2.2.2 情况 1:叔叔存在且为红 ------ 只变色

条件:

text 复制代码
c 红,p 红,g 黑,u 存在且为红

处理:

text 复制代码
p 变黑,u 变黑,g 变红

然后把 g 当作新的 c,继续向上更新。

原因 :p、u 变黑后,左右子树路径各增加一个黑结点;g 变红相当于保持 g 所在子树黑结点总数不变,同时解决了 c-p 连续红的问题。但 g 变红后可能与 g 的父亲形成连续红,所以要继续上推。若 g 是根,最后统一染黑。

特点 :只变色,不旋转;无论 c 是 p 的左还是右,p 是 g 的左还是右,处理方式相同。


2.2.3 情况 2:叔叔为黑或不存在,且同向 ------ 单旋 + 变色

条件:

text 复制代码
c 红,p 红,g 黑,u 不存在或 u 为黑
并且 c、p、g 同向:
p 是 g 的左,c 是 p 的左;
或 p 是 g 的右,c 是 p 的右。

处理:

场景 操作
p 是 g 的左,c 是 p 的左 以 g 为旋转点右单旋,p 变黑,g 变红
p 是 g 的右,c 是 p 的右 以 g 为旋转点左单旋,p 变黑,g 变红

旋转后 p 成为该子树新的根,颜色为黑;g 变红。此时子树黑高不变,没有连续红,且新根 p 的父亲无论是红、黑还是空,都不再违反规则,不需要继续上推。

注意 :u 不存在 时,c 一定是新增结点;u 存在且为黑 时,c 通常不是新增结点,而是情况 1 变色后上推上来的。


2.2.4 情况 3:叔叔为黑或不存在,且异向 ------ 双旋 + 变色

条件与情况 2 类似,但 c、p、g 方向相反:

场景 操作
p 是 g 的左,c 是 p 的右 先以 p 为旋转点左单旋,再以 g 为旋转点右单旋;最后 c 变黑,g 变红
p 是 g 的右,c 是 p 的左 先以 p 为旋转点右单旋,再以 g 为旋转点左单旋;最后 c 变黑,g 变红

旋转后 c 成为该子树新的根,颜色为黑;g 变红。同样不需要继续上推。

易错点 :双旋后变色对象是 c 和 g,不是 p 和 g。可以记作"谁最终成为子树根,谁染黑;原来的祖父染红"。


插入修复情况总结
情况 叔叔 u 操作 是否继续上推
1 存在且红 p、u 变黑,g 变红 是,把 g 当新 c
2 黑或不存在,且同向 单旋 + p 黑、g 红 否
3 黑或不存在,且异向 双旋 + c 黑、g 红 否

2.3 红黑树的插入代码实现

cpp 复制代码
template<class K, class V>
void RBTree<K, V>::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;
        subR->_parent = nullptr;
    } else {
        if (parent == ppNode->_left)
            ppNode->_left = subR;
        else
            ppNode->_right = subR;
        subR->_parent = ppNode;
    }
}

template<class K, class V>
void RBTree<K, V>::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 (ppNode == nullptr) {
        _root = subL;
        subL->_parent = nullptr;
    } else {
        if (parent == ppNode->_left)
            ppNode->_left = subL;
        else
            ppNode->_right = subL;
        subL->_parent = ppNode;
    }
}

template<class K, class V>
bool RBTree<K, V>::Insert(const pair<K, V>& kv) {
    if (_root == nullptr) {
        _root = new Node(kv);
        _root->_col = BLACK;   // 空树插入,根必须黑
        return true;
    }

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

    while (cur) {
        if (kv.first < cur->_kv.first) {
            parent = cur;
            cur = cur->_left;
        } else if (kv.first > cur->_kv.first) {
            parent = cur;
            cur = cur->_right;
        } else {
            return false;      // 不允许重复 key
        }
    }

    cur = new Node(kv);        // 非空树插入,新结点默认红色
    cur->_parent = parent;

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

    while (parent && parent->_col == RED) {
        Node* grand = parent->_parent;

        if (parent == grand->_left) {
            Node* uncle = grand->_right;

            // 情况 1:叔叔存在且为红,只变色
            if (uncle && uncle->_col == RED) {
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grand->_col = RED;

                cur = grand;
                parent = cur->_parent;
            } else {
                // 情况 3:异向,双旋 + 变色
                if (cur == parent->_right) {
                    RotateL(parent);
                    swap(parent, cur);
                }

                // 情况 2:同向,单旋 + 变色
                parent->_col = BLACK;
                grand->_col = RED;
                RotateR(grand);
                break;
            }
        } else {
            Node* uncle = grand->_left;

            // 情况 1:叔叔存在且为红,只变色
            if (uncle && uncle->_col == RED) {
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grand->_col = RED;

                cur = grand;
                parent = cur->_parent;
            } else {
                // 情况 3:异向,双旋 + 变色
                if (cur == parent->_left) {
                    RotateR(parent);
                    swap(parent, cur);
                }

                // 情况 2:同向,单旋 + 变色
                parent->_col = BLACK;
                grand->_col = RED;
                RotateL(grand);
                break;
            }
        }
    }

    _root->_col = BLACK;       // 根最后统一染黑
    return true;
}

代码说明 :swap(parent, cur) 是为了把"双旋"统一成"先转成同向,再按单旋处理"。如果不理解,可以拆成两次旋转理解:例如 p 是 g 左、c 是 p 右时,先 RotateL(p),再 RotateR(g)。


2.4 红黑树的查找

红黑树本质仍是二叉搜索树,查找逻辑与普通 BST 相同,时间复杂度为 O(log N)。

cpp 复制代码
template<class K, class V>
typename RBTree<K, V>::Node* RBTree<K, V>::Find(const K& key) {
    Node* cur = _root;

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

    return nullptr;
}

2.5 红黑树的验证

不能只检查"最长路径是否不超过最短路径的 2 倍",因为满足这个条件也不一定满足红黑树颜色规则。正确做法是直接检查红黑树的四条规则:

  1. 颜色类型天然保证每个结点非红即黑。
  2. 直接检查根是否为黑色。
  3. 前序遍历检查红色结点:反向检查父亲颜色更方便。
  4. 前序遍历记录从根到当前结点的黑色结点数量 blackNum;走到 NULL 时与参考黑高比较。
cpp 复制代码
template<class K, class V>
bool RBTree<K, V>::_Check(Node* root, int blackNum, int refBlackNum) {
    if (root == nullptr) {
        return blackNum == refBlackNum;
    }

    // 规则 3:不能有连续红结点。反向检查父亲更方便。
    if (root->_col == RED) {
        if (root->_parent && root->_parent->_col == RED) {
            cout << "出现连续红色结点" << endl;
            return false;
        }
    }

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

    return _Check(root->_left, blackNum, refBlackNum)
        && _Check(root->_right, blackNum, refBlackNum);
}

template<class K, class V>
bool RBTree<K, V>::IsBalance() {
    if (_root == nullptr)
        return true;

    // 规则 2:根必须是黑色
    if (_root->_col == RED) {
        cout << "根结点不是黑色" << endl;
        return false;
    }

    // 取最左路径的黑结点数量作为参考值
    int refBlackNum = 0;
    Node* cur = _root;
    while (cur) {
        if (cur->_col == BLACK)
            ++refBlackNum;
        cur = cur->_left;
    }

    return _Check(_root, 0, refBlackNum);
}

验证重点 :规则 4 必须用"参考黑高"逐条路径比较,不能只看某一条路径。_Check 中的 blackNum 按值传递,保证每条递归路径独立计数。


3. 总结与易错点

主题 关键结论
平衡方式 黑高一致 + 无连续红,保证最长路径不超过最短路径 2 倍
插入颜色 空树插黑;非空树插红
父黑 直接结束
父红叔红 只变色,g 当新 c 继续上推
父红叔黑/不存在,同向 单旋 + p 黑、g 红
父红叔黑/不存在,异向 双旋 + c 黑、g 红
根 最后必须染黑
查找 与普通 BST 相同
验证 直接检查四条规则,不要只检查路径长度

常见易错点:

  1. 空树插入忘记把根染黑。
  2. 叔叔为红时错误地旋转;情况 1 只变色。
  3. 双旋后把 p 染黑,正确应是最终成为子树根的 c 染黑。
  4. 旋转后忘记维护 parent 指针和 _root。
  5. 验证规则 4 时没有使用参考黑高逐路径比较。
  6. 插入修复结束后忘记执行 _root->_col = BLACK。

与 AVL 树相比,红黑树不追求严格高度平衡,插入/删除时旋转次数更少,查找略慢但整体稳定,因此被广泛用于 std::map、std::set 等关联容器。

相关推荐
ljt27249606611 小时前
Python笔记(十三)--异常
笔记·python
动词ing2 小时前
【学习笔记】动态数码管显示
笔记·学习
陈年老古董2 小时前
ClickHouse 单机搭建记录
hive·笔记·学习·clickhouse·硬件架构
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】ChatServer接口测试
网络·c++·人工智能·学习·ai·大模型·sdk
蓝祁子yoky2 小时前
Qt学习日记 Day15:网络编程(二)- UDP基础
网络·qt·学习
D e re k ZH2 小时前
C++ 模板与泛型编程进阶(No 2)
开发语言·c++
一级新生2 小时前
LLM _ Agent _ Skill _ 专家 的关系与选型笔记
java·开发语言·人工智能·笔记·大模型·智能体·skill
YSL0701242 小时前
一文通关-----树和二叉树
数据结构·算法
小糯米6012 小时前
数据结构 数组与链表 内存与缓存
数据结构