从0到1手撕红黑树:封装实现 my_map 与 my_set(SGI-STL 源码级深度解析)

本文基于 SGI-STL 3.0 源码框架,从零开始用红黑树封装实现 mapset,涵盖泛型设计思想、迭代器中序遍历、插入平衡调整(变色+旋转)、operator[] 原理、const_iterator 与 key 不可修改等核心知识点。所有代码均可直接编译运行,适合 C++ 进阶与数据结构面试备考。


从0到1手撕红黑树:封装实现 my_map 与 my_set(SGI-STL 源码级深度解析)

    • [一、前言:为什么要亲手封装 map 和 set?](#一、前言:为什么要亲手封装 map 和 set?)
    • [二、SGI-STL 源码框架揭秘](#二、SGI-STL 源码框架揭秘)
      • [2.1 头文件依赖关系](#2.1 头文件依赖关系)
      • [2.2 rb_tree 的五模板参数设计:各司其职](#2.2 rb_tree 的五模板参数设计:各司其职)
      • [2.3 节点结构:基类与派生类](#2.3 节点结构:基类与派生类)
    • 三、红黑树基础快速回顾
      • [3.1 红黑树的五条性质](#3.1 红黑树的五条性质)
      • [3.2 为什么新插入的节点默认是红色?](#3.2 为什么新插入的节点默认是红色?)
    • 四、红黑树节点与迭代器设计
    • 五、红黑树核心实现
      • [5.1 插入 Insert:完整流程拆解](#5.1 插入 Insert:完整流程拆解)
      • [5.2 旋转操作:核心指针交接](#5.2 旋转操作:核心指针交接)
        • [左旋转 RotateL](#左旋转 RotateL)
      • [5.3 查找 Find](#5.3 查找 Find)
      • [5.4 析构与内存管理](#5.4 析构与内存管理)
      • [5.5 Begin / End](#5.5 Begin / End)
    • [六、封装 my_set](#六、封装 my_set)
      • [6.1 完整实现](#6.1 完整实现)
      • [6.2 为什么第二个模板参数是 const K?](#6.2 为什么第二个模板参数是 const K?)
    • [七、封装 my_map](#七、封装 my_map)
      • [7.1 完整实现](#7.1 完整实现)
      • [7.2 为什么 pair 的 first 是 const K?](#7.2 为什么 pair 的 first 是 const K?)
      • [7.3 operator\[\] 的实现原理与副作用](#7.3 operator[] 的实现原理与副作用)
    • [八、const_iterator 与 key 不可修改的完整机制](#八、const_iterator 与 key 不可修改的完整机制)
      • [8.1 迭代器模板参数 Ref / Ptr 的设计](#8.1 迭代器模板参数 Ref / Ptr 的设计)
      • [8.2 set vs map 的迭代器修改权限对比](#8.2 set vs map 的迭代器修改权限对比)
      • [8.3 const 对象只能用 const_iterator](#8.3 const 对象只能用 const_iterator)
    • 九、易错点与深度思考汇总
      • [9.1 --end() 的特殊处理](#9.1 --end() 的特殊处理)
      • [9.2 依赖模板参数的类型必须加 typename](#9.2 依赖模板参数的类型必须加 typename)
      • [9.3 红黑树 vs AVL 树:为什么 STL 选红黑树?](#9.3 红黑树 vs AVL 树:为什么 STL 选红黑树?)
      • [9.4 迭代器失效问题](#9.4 迭代器失效问题)
    • 十、完整源码汇总
      • [10.1 RBTree.h](#10.1 RBTree.h)
      • [10.2 my_set.h](#10.2 my_set.h)
      • [10.3 my_map.h](#10.3 my_map.h)
      • [10.4 测试代码](#10.4 测试代码)
    • 十一、总结

一、前言:为什么要亲手封装 map 和 set?

在 C++ STL 中,std::mapstd::set 是每天都在用的容器,但大多数人只停留在"会用"的层面。面试时一旦被追问:

  • mapset 底层是什么数据结构?为什么选它?
  • 迭代器 ++ / -- 是怎么实现中序遍历的?
  • operator[] 为什么有副作用?
  • 为什么 set 的迭代器不能修改元素,而 map 的迭代器能修改 value 却不能修改 key
  • 红黑树插入后是如何通过变色和旋转维持平衡的?

这些问题的答案,都藏在 SGI-STL 的 stl_tree.hstl_map.hstl_set.h 三个头文件里。

本文的目标不是简单贴源码,而是分层递进地把整个设计讲透:

  1. 先看懂 SGI-STL 的泛型框架设计(为什么一颗红黑树能同时服务 map 和 set);
  2. 再深入红黑树的节点、迭代器、插入与旋转实现;
  3. 最后用我们自己的红黑树封装出 my_mapmy_set,并补充易错点与深度思考。

📖 前置推荐阅读

本篇聚焦于如何将红黑树封装为 map / set 容器,侧重于泛型设计与迭代器实现 。如果你对红黑树的基础概念(五条性质、插入调整的变色与旋转)还不够熟悉,建议先阅读我的另一篇博客 ------ 《红黑树(Red-Black Tree)从原理到C++实现|面试深度解析》,那篇从零实现了红黑树的插入与旋转,是理解本文封装逻辑的必要前置。

二、SGI-STL 源码框架揭秘

2.1 头文件依赖关系

SGI-STL 3.0 中,mapset 的实现分布在以下头文件:

容器 头文件 底层依赖
set / multiset stl_set.h / stl_multiset.h stl_tree.h(红黑树)
map / multimap stl_map.h / stl_multimap.h stl_tree.h(红黑树)

核心关系:map 和 set 都不自己实现树,而是持有一个 rb_tree 成员对象,所有操作转发给它。

cpp 复制代码
// stl_set.h 节选
template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set {
public:
    typedef Key key_type;
    typedef Key value_type;          // set 的 value_type 就是 Key 本身
private:
    typedef rb_tree<key_type, value_type,
                    identity<value_type>, key_compare, Alloc> rep_type;
    rep_type t;  // 红黑树成员对象
};

// stl_map.h 节选
template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:
    typedef Key key_type;
    typedef T mapped_type;
    typedef pair<const Key, T> value_type;  // map 的 value_type 是 pair
private:
    typedef rb_tree<key_type, value_type,
                    select1st<value_type>, key_compare, Alloc> rep_type;
    rep_type t;  // 红黑树成员对象
};

2.2 rb_tree 的五模板参数设计:各司其职

rb_tree 的模板签名是理解整个框架的钥匙:

cpp 复制代码
template <class Key, class Value, class KeyOfValue,
          class Compare, class Alloc = alloc>
class rb_tree;
参数 含义 set 实例化时传什么 map 实例化时传什么
Key 键的类型,用于 find / erase 的形参 Key Key
Value 节点中实际存储的数据类型 Key pair<const Key, T>
KeyOfValue 从 Value 中取出 Key 的仿函数 identity<Key> select1st<pair>
Compare 比较器 less<Key> less<Key>
Alloc 空间配置器 alloc alloc

设计精髓 :红黑树根本不关心自己存的是单纯的 key,还是 key/value 对------这完全由第二个模板参数 Value 决定。节点里存什么,Value 就是什么。

  • set:节点存 Key,所以 Value = Key
  • map:节点存 pair<const Key, T>,所以 Value = pair<const Key, T>

为什么需要 KeyOfValue 仿函数?

因为节点里存的数据类型 T 是泛型的(可能是 int,也可能是 pair<int, string>),树本身不知道怎么从 T 里拿出 key 来比较。解决方案:由上层传入一个仿函数,告诉树"怎么从数据里取 key"。

cpp 复制代码
// map 传入的仿函数:从 pair 中取 first(即 key)
struct MapKeyOfT {
    const K& operator()(const pair<K, V>& kv) {
        return kv.first;
    }
};

// set 传入的仿函数:key 就是数据本身
struct SetKeyOfT {
    const K& operator()(const K& key) {
        return key;
    }
};

易错点std::pair 的默认 operator< 会同时比较 firstsecond(字典序)。如果直接用 pair 比较,当 key 相同但 value 不同时会错误地认为两个节点不相等。所以必须用 KeyOfT 只提取 key 来比较

为什么还要第一个模板参数 Key?

Key 是给 finderase 做形参类型用的:

cpp 复制代码
// stl_tree.h 节选
pair<iterator, bool> insert_unique(const value_type& x);  // insert 用 Value
size_type erase(const key_type& x);                       // erase 用 Key
iterator find(const key_type& x);                         // find 用 Key
  • insert 时传的是完整数据(Value);
  • find / erase 时只需要传 key(Key)。

set 来说 Key == Value,看起来重复;但对 map 来说,insertpairfind 只传 Key,两者完全不同。

2.3 节点结构:基类与派生类

SGI-STL 把节点拆成了基类和派生类两层:

cpp 复制代码
// 基类:只存颜色和三个指针,不涉及数据类型
struct __rb_tree_node_base {
    typedef __rb_tree_color_type color_type;
    typedef __rb_tree_node_base* base_ptr;

    color_type color;
    base_ptr parent;
    base_ptr left;
    base_ptr right;
};

// 派生类:增加数据字段
template <class Value>
struct __rb_tree_node : public __rb_tree_node_base {
    typedef __rb_tree_node<Value>* link_type;
    Value value_field;  // 实际存储的数据
};

为什么要拆成两层? 因为旋转、变色等操作只需要操作指针和颜色,不需要知道数据类型。基类指针 base_ptr 可以在不涉及模板的代码中通用,减少代码膨胀。我们自己实现时为了简洁,可以合并成一个模板结构体。


三、红黑树基础快速回顾

在动手实现之前,先把红黑树的核心性质过一遍,这是后续插入调整的理论基础。

3.1 红黑树的五条性质

  1. 每个节点不是红色就是黑色;
  2. 根节点是黑色;
  3. 每个叶子节点(NIL 空节点)是黑色;
  4. 如果一个节点是红色,它的两个孩子都是黑色(不能有连续的红色节点);
  5. 从任一节点到其每个叶子的所有路径,包含相同数量的黑色节点(黑高相同)。

满足这五条性质的红黑树,能保证最长路径不超过最短路径的 2 倍,从而保证 O(log n) 的查找、插入、删除效率。

3.2 为什么新插入的节点默认是红色?

新节点插入时,如果默认黑色,会导致所有经过该节点的路径黑高 +1,必然破坏性质 5,调整起来非常麻烦。

如果默认红色:

  • 若父亲是黑色 → 五条性质全部满足,无需任何调整
  • 若父亲是红色 → 只破坏性质 4(连续红节点),只需局部调整。

所以新节点默认红色是最优选择。


四、红黑树节点与迭代器设计

4.1 节点结构 RBTreeNode

我们的实现不搞基类派生,直接用一个模板结构体:

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

template<class T>
struct RBTreeNode {
    T _data;                 // 节点存储的数据(set中是K,map中是pair<const K,V>)
    RBTreeNode<T>* _left;    // 左孩子
    RBTreeNode<T>* _right;   // 右孩子
    RBTreeNode<T>* _parent;  // 父指针------迭代器和旋转都需要它
    Colour _col;             // 颜色

    RBTreeNode(const T& data)
        : _data(data)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
    {}
};

为什么必须有 _parent 指针? 两个原因:

  1. 迭代器 ++ / -- 需要从当前节点向上找祖先;
  2. 旋转操作 需要知道父节点的父节点(grandfather),以及更新各个指针的指向。

如果没有 parent 指针,每次都要从根遍历找父亲,效率会从 O(1) 退化为 O(log n)。

4.2 迭代器设计思想

迭代器的本质是对节点指针的封装 ,通过重载运算符让它表现得像指针一样。和 list 的迭代器思路完全一致。

cpp 复制代码
template<class T, class Ref, class Ptr>
struct RBTreeIterator {
    typedef RBTreeNode<T> Node;
    typedef RBTreeIterator<T, Ref, Ptr> Self;

    Node* _node;  // 当前指向的节点
    Node* _root;  // 保存根指针,用于 --end() 的特殊处理

    RBTreeIterator(Node* node, Node* root)
        : _node(node), _root(root)
    {}

    // 解引用:返回数据的引用
    Ref operator*() { return _node->_data; }

    // 箭头:返回数据的指针
    Ptr operator->() { return &_node->_data; }

    // 比较
    bool operator!=(const Self& s) const { return _node != s._node; }
    bool operator==(const Self& s) const { return _node == s._node; }
};

三个模板参数的含义:

参数 普通迭代器 iterator 常量迭代器 const_iterator
T 数据类型 数据类型
Ref T& const T&
Ptr T* const T*

通过 RefPtr 的不同实例化,一套迭代器代码同时支持普通和常量两种迭代器,这是 STL 的经典泛型技巧。

在红黑树类中定义:

cpp 复制代码
typedef RBTreeIterator<T, T&, T*> Iterator;
typedef RBTreeIterator<T, const T&, const T*> ConstIterator;

4.3 operator++ 深度解析:中序遍历的下一个节点

mapset 的迭代器走的是中序遍历 (左 → 根 → 右),所以 ++it 要找到中序序列中的下一个节点。

只看局部,分两种情况:

情况一:右子树不为空

当前节点访问完了,下一个要访问的是右子树的中序第一个节点 ,也就是右子树的最左节点(最小值)。

复制代码
      30
     /  \
   15    40
  /  \     \
 10  25     50

it 指向 30,右子树不为空 → 找右子树最左节点 40
it 指向 15,右子树不为空 → 找右子树最左节点 25
cpp 复制代码
if (_node->_right) {
    Node* min = _node->_right;
    while (min->_left) {
        min = min->_left;
    }
    _node = min;
}
情况二:右子树为空

当前节点和它所在的子树都访问完了,下一个节点在祖先 中。沿着 parent 向上找,直到找到"孩子是父亲的左孩子"的那个祖先------根据中序遍历,左子树访问完后就该访问根(父亲)了。

复制代码
      18
     /  \
   10    30
    \    / \
    15  25  40

it 指向 25,右为空,25 是 30 的左孩子 → 下一个是 30
it 指向 15,右为空,15 是 10 的右孩子 → 继续向上,10 是 18 的左孩子 → 下一个是 18
it 指向 40,右为空,40 是 30 的右 → 30 是 18 的右 → 18 无父 → 下一个是 nullptr(end)
cpp 复制代码
else {
    Node* cur = _node;
    Node* parent = cur->_parent;
    while (parent && cur == parent->_right) {
        cur = parent;
        parent = cur->_parent;
    }
    _node = parent;  // 可能为 nullptr(即 end())
}

end() 的表示 :我们用 nullptr 充当 end()。当中序遍历到最后一个节点(最大节点)再 ++ 时,向上找祖先的循环最终会让 parent 变为 nullptr,迭代器自然就变成了 end()

4.4 operator-- 深度解析:中序遍历的上一个节点

--it++it 的镜像,访问顺序反过来(右 → 根 → 左),但有一个特殊情况需要单独处理。

特殊情况:--end()

end() 指向 nullptr,对它执行 -- 应该走到中序最后一个节点,也就是整棵树的最右节点 (最大值)。这就是迭代器需要保存 _root 指针的原因。

cpp 复制代码
if (_node == nullptr) {  // --end()
    Node* rightMost = _root;
    while (rightMost && rightMost->_right) {
        rightMost = rightMost->_right;
    }
    _node = rightMost;
}
情况一:左子树不为空

上一个节点是左子树的中序最后一个节点 ,也就是左子树的最右节点(最大值)。

cpp 复制代码
else if (_node->_left) {
    Node* rightMost = _node->_left;
    while (rightMost->_right) {
        rightMost = rightMost->_right;
    }
    _node = rightMost;
}
情况二:左子树为空

沿着 parent 向上找,直到找到"孩子是父亲的右孩子"的那个祖先。

cpp 复制代码
else {
    Node* cur = _node;
    Node* parent = cur->_parent;
    while (parent && cur == parent->_left) {
        cur = parent;
        parent = cur->_parent;
    }
    _node = parent;
}

4.5 SGI 源码的哨兵位 header vs 我们的 nullptr

SGI-STL 的实现比我们更精巧,它用了一个哨兵位头节点(header) 来充当 end(),而不是 nullptr

对比项 SGI-STL(header 哨兵位) 我们的实现(nullptr)
end() 表示 header 节点 nullptr
header 与根的关系 互为 parent
header->left 指向最左节点(begin)
header->right 指向最右节点
--end() 实现 node = node->right(直接取 header 的右) 需从 root 遍历找最右节点
代码复杂度 较高 较低

SGI 的 decrement() 里有一段很巧妙的判断:

cpp 复制代码
void decrement() {
    if (node->color == __rb_tree_red &&
        node->parent->parent == node)
        node = node->right;  // --end():header 的右就是最右节点
    else if (node->left != 0) {
        // ... 左子树最右节点
    } else {
        // ... 向上找祖先
    }
}

header 节点是红色的,且 header->parent->parent == header(根的父亲是 header,header 的父亲是根),所以用这个条件判断当前是否是 end()。我们用 nullptr 方案虽然 --end() 多了一次遍历,但逻辑更直观,功能完全等价。


五、红黑树核心实现

5.1 插入 Insert:完整流程拆解

插入是红黑树最核心的操作,整体思路是:先按 BST 规则找到插入位置,创建红色新节点并链接;然后从新节点开始,沿祖先向上检查,若父节点为红则通过变色或旋转调整,直到满足红黑树性质;最后强制根变黑。

第一步:BST 搜索插入位置

从根节点开始,按照二叉搜索树的规则向下搜索:

cpp 复制代码
template<class K, class T, class KeyOfT>
class RBTree {
    typedef RBTreeNode<T> Node;
public:
    pair<Iterator, bool> Insert(const T& data) {
        // 空树:直接创建根节点,根必须是黑色
        if (_root == nullptr) {
            _root = new Node(data);
            _root->_col = BLACK;
            return { Iterator(_root, _root), true };
        }

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

        // 按 BST 规则搜索插入位置
        while (cur) {
            if (kot(cur->_data) < kot(data)) {
                parent = cur;
                cur = cur->_right;
            } else if (kot(cur->_data) > kot(data)) {
                parent = cur;
                cur = cur->_left;
            } else {
                // key 已存在,插入失败,返回已有节点的迭代器
                return { Iterator(cur, _root), false };
            }
        }

注意 :这里比较用的是 kot(cur->_data) 而不是 cur->_data,因为 T 可能是 pair,必须通过仿函数提取 key 来比较。

第二步:创建红节点并链接
cpp 复制代码
        cur = new Node(data);
        Node* newnode = cur;   // 保存新节点指针,用于返回
        cur->_col = RED;       // 新节点默认红色

        // 链接到父节点
        if (kot(parent->_data) < kot(data)) {
            parent->_right = cur;
        } else {
            parent->_left = cur;
        }
        cur->_parent = parent; // 别忘了链接父指针!

易错点 :创建新节点后,除了设置父节点的 left / right必须同时设置新节点的 _parent。漏掉这一步会导致后续迭代器和旋转全部出错。

第三步:调整平衡(while 循环)

如果父亲是黑色,性质 4 没有被破坏,无需调整。只有当父亲是红色时,才需要进入调整循环。

调整时需要三个角色:cur(当前节点)、parent(父亲)、grandfather(祖父)。根据父亲是祖父的左孩子还是右孩子,分为两大分支,每个分支下又根据叔叔的颜色分情况。

cpp 复制代码
        while (parent && parent->_col == RED) {
            Node* grandfather = parent->_parent;

            // ========== 分支A:父亲是祖父的左孩子 ==========
            if (parent == grandfather->_left) {
                Node* uncle = grandfather->_right;

                // 情况1:叔叔存在且为红 → 变色,继续向上
                if (uncle && uncle->_col == RED) {
                    parent->_col = uncle->_col = BLACK;
                    grandfather->_col = RED;

                    cur = grandfather;       // 继续向上处理
                    parent = cur->_parent;
                }
                // 情况2:叔叔不存在或为黑 → 旋转+变色
                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;  // 旋转后平衡已恢复,无需继续向上
                }
            }
            // ========== 分支B:父亲是祖父的右孩子(镜像) ==========
            else {
                Node* uncle = grandfather->_left;

                // 情况1:叔叔存在且为红 → 变色,继续向上
                if (uncle && uncle->_col == RED) {
                    parent->_col = uncle->_col = BLACK;
                    grandfather->_col = RED;

                    cur = grandfather;
                    parent = cur->_parent;
                }
                // 情况2:叔叔不存在或为黑 → 旋转+变色
                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 { Iterator(newnode, _root), true };
    }
插入调整四种旋转场景总结表
场景 父亲位置 cur 位置 叔叔状态 操作 变色规则
左左 祖父左 父亲左 不存在/黑 祖父右旋 父变黑,祖父变红
左右 祖父左 父亲右 不存在/黑 父左旋+祖父右旋 cur变黑,祖父变红
右右 祖父右 父亲右 不存在/黑 祖父左旋 父变黑,祖父变红
右左 祖父右 父亲左 不存在/黑 父右旋+祖父左旋 cur变黑,祖父变红
变色 任意 任意 存在且红 仅变色不旋转 父和叔变黑,祖父变红,继续向上

记忆口诀:叔叔红就变色往上走;叔叔黑就旋转,单旋变父色,双旋变 cur 色。
为什么变色情况要继续向上,而旋转情况可以 break?

  • 变色后祖父变成了红色,如果祖父的父亲也是红色,就会再次出现连续红节点,所以必须继续向上检查。
  • 旋转后,子树的根(原来的 parent 或 cur)变成了黑色,这保证了不会再和上层产生连续红节点,所以可以直接 break。

5.2 旋转操作:核心指针交接

旋转是红黑树维持平衡的核心手段。我们以左旋转为例,看关键指针的变化(完整实现见文末附录)。

左旋转 RotateL

左旋转是把"右孩子提上来,当前节点降为左孩子"。

复制代码
    parent                  subR
    /    \                 /   \
   a     subR    ===>   parent  c
         /  \           /   \
        b    c         a     b

关键指针交接(仅 4 行核心代码):

cpp 复制代码
parent->_right = subRL;      // subR 的左子树过继给 parent
subR->_left = parent;        // parent 降为 subR 的左孩子
// 更新父指针(略,详见附录)

五步拆解(理解指针如何变动):

步骤 操作 目的
1 保存 subRsubRL 防止指针覆盖后丢失
2 parent->_right = subRL,更新 subRL->_parent 右孩子的左子树过继给 parent
3 保存 parentParent 旋转后需要让 subR 接替 parent 的位置
4 subR->_left = parentparent->_parent = subR parent 降为 subR 的左孩子
5 根据 parentParent 更新根或父子关系 subR 正式接替 parent 的位置

右旋转是左旋转的镜像,不再赘述。完整的 RotateLRotateR 函数实现见文末 第十章 完整源码汇总

旋转操作易错点

  1. subRL / subLR 可能为 nullptr,更新父指针前必须判空;
  2. parentParent 可能为 nullptr(parent 是根节点),此时要更新 _root
  3. 父指针 _parent 必须同步更新,只改 left / right 不改 _parent 会导致树结构断裂;
  4. 旋转只改变结构,不改变中序遍历序列------这是旋转的基本性质。

5.3 查找 Find

查找就是标准的 BST 搜索,利用 KeyOfT 提取 key 比较:

cpp 复制代码
Iterator Find(const K& key) {
    KeyOfT kot;
    Node* cur = _root;
    while (cur) {
        if (kot(cur->_data) < key)
            cur = cur->_right;
        else if (kot(cur->_data) > key)
            cur = cur->_left;
        else
            return Iterator(cur, _root);
    }
    return End();
}

5.4 析构与内存管理

红黑树是动态内存结构,析构时需要后序遍历释放所有节点:

cpp 复制代码
~RBTree() {
    Destroy(_root);
    _root = nullptr;
}

void Destroy(Node* root) {
    if (root == nullptr)
        return;
    Destroy(root->_left);
    Destroy(root->_right);
    delete root;
}

必须用后序遍历(左 → 右 → 根),因为要先释放孩子再释放自己。如果先释放根,孩子指针就丢失了。

5.5 Begin / End

cpp 复制代码
Iterator Begin() {
    Node* leftMost = _root;
    while (leftMost && leftMost->_left) {
        leftMost = leftMost->_left;
    }
    return Iterator(leftMost, _root);  // 最左节点 = 中序第一个
}

Iterator End() {
    return Iterator(nullptr, _root);   // nullptr 充当 end
}

Begin() 返回最左节点(中序最小值),End() 返回 nullptrconst 版本同理,返回 ConstIterator


六、封装 my_set

有了泛型红黑树,封装 set 就非常简单了------本质是一层接口转发。

6.1 完整实现

cpp 复制代码
#pragma once
#include "RBTree.h"

namespace bit {
    template<class K>
    class set {
        // 仿函数:set 的数据就是 key 本身
        struct SetKeyOfT {
            const K& operator()(const K& key) {
                return key;
            }
        };

    public:
        // 迭代器类型转发(注意 typename 关键字)
        typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
        typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;

        iterator begin() { return _t.Begin(); }
        iterator end() { return _t.End(); }

        const_iterator begin() const { return _t.Begin(); }
        const_iterator end() const { return _t.End(); }

        pair<iterator, bool> insert(const K& key) {
            return _t.Insert(key);
        }

        iterator find(const K& key) {
            return _t.Find(key);
        }

    private:
        // 第二个模板参数是 const K ------ 保证 set 的元素不可修改
        RBTree<K, const K, SetKeyOfT> _t;
    };
}

6.2 为什么第二个模板参数是 const K?

这是一个关键设计。set 中的元素就是 key,而 key 是排序的依据------如果允许修改 key,树的有序性就被破坏了

通过把第二个模板参数设为 const K,红黑树节点中存储的就是 const K,迭代器解引用得到的是 const K&,自然就无法修改了:

cpp 复制代码
bit::set<int> s;
s.insert(5);
auto it = s.begin();
// *it = 10;  // 编译错误:不能给常量赋值

易错点typedef 中依赖模板参数的类型必须加 typename 关键字。RBTree<K, const K, SetKeyOfT>::Iterator 是一个依赖名,编译器在实例化前不知道它是类型还是静态成员,所以必须用 typename 显式声明。


七、封装 my_map

7.1 完整实现

cpp 复制代码
#pragma once
#include "RBTree.h"

namespace bit {
    template<class K, class V>
    class map {
        // 仿函数:从 pair 中提取 key(first)
        struct MapKeyOfT {
            const K& operator()(const pair<K, V>& kv) {
                return kv.first;
            }
        };

    public:
        typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
        typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;

        iterator begin() { return _t.Begin(); }
        iterator end() { return _t.End(); }

        const_iterator begin() const { return _t.Begin(); }
        const_iterator end() const { return _t.End(); }

        pair<iterator, bool> insert(const pair<K, V>& kv) {
            return _t.Insert(kv);
        }

        iterator find(const K& key) {
            return _t.Find(key);
        }

        // operator[]:核心功能
        V& operator[](const K& key) {
            pair<iterator, bool> ret = insert({ key, V() });
            return ret.first->second;
        }

    private:
        // pair 的 first 是 const K ------ key 不可修改,value 可修改
        RBTree<K, pair<const K, V>, MapKeyOfT> _t;
    };
}

7.2 为什么 pair 的 first 是 const K?

set 的道理一样:map 的 key 是排序依据,不能修改。但 map 的 value 是可以修改的。

通过 pair<const K, V>,迭代器解引用得到 pair<const K, V>&

  • it->firstconst K不可修改
  • it->secondV可以修改
cpp 复制代码
bit::map<string, string> dict;
dict.insert({ "sort", "排序" });
auto it = dict.begin();
// it->first = "xxx";  // 编译错误:first 是 const
it->second = "排序,分类";  // OK:second 可以修改

7.3 operator\[\] 的实现原理与副作用

operator[]map 最常用也最容易误用的接口。它的实现只有两行:

cpp 复制代码
V& operator[](const K& key) {
    pair<iterator, bool> ret = insert({ key, V() });
    return ret.first->second;
}

工作流程:

  1. 调用 insert({key, V()}) 尝试插入一个 key 对应默认值的 pair;
  2. insert 返回 pair<iterator, bool>
    • 如果 key 不存在 → 插入成功,bool = true,迭代器指向新插入的节点;
    • 如果 key 已存在 → 插入失败,bool = false,迭代器指向已有节点;
  3. 返回该节点 value 的引用 ret.first->second

三种典型用法:

cpp 复制代码
bit::map<string, int> countMap;

// 用法1:插入 + 修改
countMap["apple"] = 5;        // key 不存在,插入默认值0,再赋值为5

// 用法2:读取(key 必须存在,否则会意外插入)
int n = countMap["apple"];    // key 存在,返回 5

// 用法3:计数统计(最经典)
countMap["banana"]++;         // key 不存在,插入默认值0,再++变成1

重大易错点:operator\[\] 有副作用!

如果你只是想查询一个 key 是否存在,绝对不要用 if (m[key])cout << m[key] ,因为即使 key 不存在,operator[] 也会插入一个默认值。这会导致:

  • 容器意外变大;
  • 后续 size() 结果不符合预期;
  • 多线程环境下引发数据竞争。

正确的查询方式是用 find()

cpp 复制代码
auto it = countMap.find("apple");
if (it != countMap.end()) {
    // key 存在,it->second 是值
}

扩展:multiset / multimap 只差一行代码

STL 中不仅有 set / map,还有 multiset / multimap------它们与 set / map 的唯一区别是允许重复的 key 。对应到红黑树层面,rb_tree 提供了两条插入路径:

  • insert_unique():key 已存在则插入失败(set / map 使用);
  • insert_equal():key 已存在仍插入到右子树(multiset / multimap 使用)。

在我们的 RBTree::Insert 实现中,只需将相等判断分支从 return { Iterator(cur, _root), false }; 改为 parent = cur; cur = cur->_right; 让相同 key 的节点始终向右走,即可支持重复键。理解了这一点,multiset / multimap 的封装只是换一个 Insert 接口调用,其余代码完全复用。


八、const_iterator 与 key 不可修改的完整机制

8.1 迭代器模板参数 Ref / Ptr 的设计

我们的迭代器用三个模板参数 T, Ref, Ptr 来同时支持普通和常量迭代器:

cpp 复制代码
template<class T, class Ref, class Ptr>
struct RBTreeIterator {
    Ref operator*() { return _node->_data; }   // Ref 决定返回普通引用还是 const 引用
    Ptr operator->() { return &_node->_data; } // Ptr 决定返回普通指针还是 const 指针
};

在红黑树中定义两种迭代器:

cpp 复制代码
typedef RBTreeIterator<T, T&, T*> Iterator;              // 普通迭代器
typedef RBTreeIterator<T, const T&, const T*> ConstIterator;  // 常量迭代器

T = const K(set 的情况)时,即使是普通 IteratorRef 也是 const K&,元素依然不可修改。这就是"set 的普通迭代器也不能修改元素"的底层原因。

8.2 set vs map 的迭代器修改权限对比

容器 节点存储类型 T 普通迭代器解引用 能否修改 key 能否修改 value
set const K const K& 不能(无 value) 无 value
map pair<const K, V> pair<const K, V>& 不能(first 是 const) 能(second 非 const)

8.3 const 对象只能用 const_iterator

cpp 复制代码
void Print(const bit::set<int>& s) {
    // s 是 const 引用,begin()/end() 返回 const_iterator
    bit::set<int>::const_iterator it = s.begin();
    while (it != s.end()) {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

如果这里用 iterator 而不是 const_iterator,编译会报错------因为 const 对象的 begin() 返回的是 const_iterator,不能隐式转换为 iterator(反向转换可以:普通迭代器可以隐式转为常量迭代器)。


九、易错点与深度思考汇总

本节精选 正文尚未强调过的 5 个易错点,帮助你在编码和面试中避坑。

9.1 --end() 的特殊处理

end()nullptr,对它执行 -- 时,_node == nullptr 分支会触发,从 _root 出发找最右节点。这就是迭代器必须保存 _root 指针的原因。

易错点 :如果迭代器不保存 _root--end() 就无法实现。SGI-STL 用 header 哨兵位避免了这个问题,但我们的 nullptr 方案必须存根指针。

9.2 依赖模板参数的类型必须加 typename

set/map 中定义迭代器类型时:

cpp 复制代码
typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;

RBTree<K, const K, SetKeyOfT>::Iterator 是依赖模板参数的类型,编译器在解析时不知道它是个类型,所以必须加 typename 显式声明,否则编译报错。


9.3 红黑树 vs AVL 树:为什么 STL 选红黑树?

对比项 AVL 树 红黑树
平衡标准 左右子树高度差 ≤ 1 最长路径 ≤ 2 × 最短路径
插入调整 最多需要 O(log n) 次旋转 最多 2 次旋转 + 变色
删除调整 较复杂 相对简单
查找效率 略高(更严格平衡) 略低但仍是 O(log n)
插入/删除效率 较低(旋转次数多) 较高(变色为主,旋转少)
适用场景 查找密集型 插入删除频繁型

STL 选择红黑树的原因:实际应用中插入删除和查找是混合的,红黑树在插入删除时性能更优(变色是 O(1) 操作,旋转次数有上限),而查找性能损失很小(高度差不超过 2 倍)。综合来看红黑树是更均衡的选择。

9.4 迭代器失效问题

红黑树的插入和删除操作不会导致其他节点的迭代器失效 (只有被删除的节点迭代器失效)。这是因为红黑树的节点在堆上独立分配,旋转只改变指针指向,不移动节点内存。这和 vector 的迭代器失效有本质区别。


十、完整源码汇总

10.1 RBTree.h

cpp 复制代码
#pragma once

enum Colour {
    RED,
    BLACK
};

template<class T>
struct RBTreeNode {
    T _data;
    RBTreeNode<T>* _left;
    RBTreeNode<T>* _right;
    RBTreeNode<T>* _parent;
    Colour _col;

    RBTreeNode(const T& data)
        : _data(data)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
    {}
};

template<class T, class Ref, class Ptr>
struct RBTreeIterator {
    typedef RBTreeNode<T> Node;
    typedef RBTreeIterator<T, Ref, Ptr> Self;

    Node* _node;
    Node* _root;

    RBTreeIterator(Node* node, Node* root)
        : _node(node), _root(root)
    {}

    Self& operator++() {
        if (_node->_right) {
            Node* min = _node->_right;
            while (min->_left)
                min = min->_left;
            _node = min;
        } else {
            Node* cur = _node;
            Node* parent = cur->_parent;
            while (parent && cur == parent->_right) {
                cur = parent;
                parent = cur->_parent;
            }
            _node = parent;
        }
        return *this;
    }

    Self& operator--() {
        if (_node == nullptr) {  // --end()
            Node* rightMost = _root;
            while (rightMost && rightMost->_right)
                rightMost = rightMost->_right;
            _node = rightMost;
        } else if (_node->_left) {
            Node* rightMost = _node->_left;
            while (rightMost->_right)
                rightMost = rightMost->_right;
            _node = rightMost;
        } else {
            Node* cur = _node;
            Node* parent = cur->_parent;
            while (parent && cur == parent->_left) {
                cur = parent;
                parent = cur->_parent;
            }
            _node = parent;
        }
        return *this;
    }

    Ref operator*() { return _node->_data; }
    Ptr operator->() { return &_node->_data; }

    bool operator!=(const Self& s) const { return _node != s._node; }
    bool operator==(const Self& s) const { return _node == s._node; }
};

template<class K, class T, class KeyOfT>
class RBTree {
    typedef RBTreeNode<T> Node;
public:
    typedef RBTreeIterator<T, T&, T*> Iterator;
    typedef RBTreeIterator<T, const T&, const T*> ConstIterator;

    Iterator Begin() {
        Node* leftMost = _root;
        while (leftMost && leftMost->_left)
            leftMost = leftMost->_left;
        return Iterator(leftMost, _root);
    }

    Iterator End() {
        return Iterator(nullptr, _root);
    }

    ConstIterator Begin() const {
        Node* leftMost = _root;
        while (leftMost && leftMost->_left)
            leftMost = leftMost->_left;
        return ConstIterator(leftMost, _root);
    }

    ConstIterator End() const {
        return ConstIterator(nullptr, _root);
    }

    RBTree() = default;

    ~RBTree() {
        Destroy(_root);
        _root = nullptr;
    }

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

        KeyOfT kot;
        Node* parent = nullptr;
        Node* cur = _root;
        while (cur) {
            if (kot(cur->_data) < kot(data)) {
                parent = cur;
                cur = cur->_right;
            } else if (kot(cur->_data) > kot(data)) {
                parent = cur;
                cur = cur->_left;
            } else {
                return { Iterator(cur, _root), false };
            }
        }

        cur = new Node(data);
        Node* newnode = cur;
        cur->_col = RED;
        if (kot(parent->_data) < kot(data))
            parent->_right = cur;
        else
            parent->_left = cur;
        cur->_parent = parent;

        while (parent && parent->_col == RED) {
            Node* grandfather = parent->_parent;
            if (parent == grandfather->_left) {
                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) {
                    parent->_col = uncle->_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 { Iterator(newnode, _root), true };
    }

    Iterator Find(const K& key) {
        KeyOfT kot;
        Node* cur = _root;
        while (cur) {
            if (kot(cur->_data) < key)
                cur = cur->_right;
            else if (kot(cur->_data) > key)
                cur = cur->_left;
            else
                return Iterator(cur, _root);
        }
        return End();
    }

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

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

        Node* parentParent = parent->_parent;
        subR->_left = parent;
        parent->_parent = subR;

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

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

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

        Node* pParent = 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 Destroy(Node* root) {
        if (root == nullptr) return;
        Destroy(root->_left);
        Destroy(root->_right);
        delete root;
    }

private:
    Node* _root = nullptr;
};

10.2 my_set.h

cpp 复制代码
#pragma once
#include "RBTree.h"

namespace bit {
    template<class K>
    class set {
        struct SetKeyOfT {
            const K& operator()(const K& key) { return key; }
        };

    public:
        typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
        typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;

        iterator begin() { return _t.Begin(); }
        iterator end() { return _t.End(); }
        const_iterator begin() const { return _t.Begin(); }
        const_iterator end() const { return _t.End(); }

        pair<iterator, bool> insert(const K& key) {
            return _t.Insert(key);
        }

        iterator find(const K& key) {
            return _t.Find(key);
        }

    private:
        RBTree<K, const K, SetKeyOfT> _t;
    };
}

10.3 my_map.h

cpp 复制代码
#pragma once
#include "RBTree.h"

namespace bit {
    template<class K, class V>
    class map {
        struct MapKeyOfT {
            const K& operator()(const pair<K, V>& kv) { return kv.first; }
        };

    public:
        typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
        typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;

        iterator begin() { return _t.Begin(); }
        iterator end() { return _t.End(); }
        const_iterator begin() const { return _t.Begin(); }
        const_iterator end() const { return _t.End(); }

        pair<iterator, bool> insert(const pair<K, V>& kv) {
            return _t.Insert(kv);
        }

        iterator find(const K& key) {
            return _t.Find(key);
        }

        V& operator[](const K& key) {
            pair<iterator, bool> ret = insert({ key, V() });
            return ret.first->second;
        }

    private:
        RBTree<K, pair<const K, V>, MapKeyOfT> _t;
    };
}

10.4 测试代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
using namespace std;

#include "my_set.h"
#include "my_map.h"

void test_set() {
    bit::set<int> s;
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    for (auto e : a) s.insert(e);

    // 正向遍历(中序:升序)
    for (auto e : s) cout << e << " ";
    cout << endl;

    // 反向遍历
    auto it = s.end();
    while (it != s.begin()) {
        --it;
        cout << *it << " ";
    }
    cout << endl;
}

void test_map() {
    bit::map<string, string> dict;
    dict.insert({ "sort", "排序" });
    dict.insert({ "left", "左边" });
    dict.insert({ "right", "右边" });

    dict["left"] = "左边,剩余";   // key 存在,修改 value
    dict["insert"] = "插入";       // key 不存在,插入
    dict["string"];                // key 不存在,插入默认空串

    for (auto& kv : dict) {
        cout << kv.first << ":" << kv.second << endl;
    }
}

int main() {
    test_set();
    test_map();
    return 0;
}

十一、总结

本文从 SGI-STL 源码出发,完整实现了一颗泛型红黑树,并基于它封装了 my_mapmy_set。核心知识点回顾:

  1. 泛型设计 :通过 Value 模板参数控制节点存储类型,通过 KeyOfT 仿函数提取 key,一颗树同时服务 map 和 set;
  2. 迭代器 :封装节点指针,++ / -- 基于中序遍历的局部逻辑,nullptr 充当 end()--end() 需特殊处理;
  3. 插入调整:新节点默认红色,父亲为红时进入调整循环------叔叔红则变色向上传播,叔叔黑则旋转(单旋/双旋)后 break;
  4. 旋转:左旋转五步拆解,核心是指针交接和父指针同步更新;
  5. key 不可修改 :set 用 const K,map 用 pair<const K, V>,从类型层面保证 key 不可变;
  6. operator\[\]:基于 insert 返回值实现,有副作用,查询应用 find();
  7. const_iterator :通过 Ref / Ptr 模板参数一套代码支持两种迭代器。

红黑树封装 map/set 是 C++ 进阶和数据结构面试的高频考点,理解了这套设计,再去看 STL 源码或其他平衡树(如 AVL、B 树)都会触类旁通。

相关推荐
波特率1152001 小时前
C++新特性---属性说明符与标准属性
开发语言·c++
程序员小八7771 小时前
上海百度B端java后端日常实习一面
java·开发语言
wp123_11 小时前
IPX8 防水 Type‑C连接器:安费诺 124018792112A 与 TONEVEE TY48087‑24A 技术梳理
c语言·开发语言
不会代码的小猴1 小时前
3. 控件学习1
开发语言·c++·笔记·qt·算法
fangzhanpeng1682 小时前
(前端)2.js变量作用域样例
开发语言·前端·javascript
疯狂打码的少年3 小时前
【数据结构】图的存储结构:邻接矩阵与邻接表
数据结构·笔记
203号居民3 小时前
LeetCode hot 100 —41. 缺失的第一个正数
数据结构·算法·leetcode
蒸蒸yyyyzwd3 小时前
cpp 选手准备秋招学习笔记 day10
笔记·面试·求职招聘
mqiqe4 小时前
线程调度与 Schedulers:Project Reactor 并发模型的核心引擎
java·开发语言·网络