如果让你用 C++ 写一个 mymap 和一个 myset,你的第一反应大概是把之前学过的红黑树拷贝两份,改一改,各写各的。这当然能做出来,但你会发现一个问题:map 和 set 的底层都是红黑树,它们维护结构、插入、删除、查找的逻辑几乎一模一样,可你为了两个容器复制了海量的重复代码。

今天这节课,我们就来解决这个尴尬。我们的目标是:只写一棵通用的红黑树,让它同时撑起 mymap 和 myset。而标准库(SGI-STL 3.0 版本)恰好就是这么干的——map、set 都只是薄薄的一层壳,真正的核心还是那棵树。所以我强烈建议你先去打开 stl_map.h、stl_set.h、stl_tree.h 这几个头文件看看,你会发现我们下面要讲的,就是源码框架的简化复刻。

在动手之前,我必须先确认你手头有几个"零件"。如果你是刚学完 C++ 不久,下面的词如果看着眼熟但说不清,正好我们边讲边补。红黑树是一种自平衡的二叉搜索树,它靠一套"颜色+旋转"的规则保证树高保持在 $O(\log n)$,这样查找、插入、删除都很快。模板(template)是 C++ 里"按类型生成代码"的机制——我们写一个通用的树,调用时往尖括号里填具体类型,编译器就帮我们生成对应的实例。仿函数(functor)就是重载了 operator()、从而可以像函数一样被调用的类对象。pair 是 <utility> 里的一个小结构,就是装两个值(first 和 second)的袋子。而迭代器是让你能用类似指针的写法(*it、it++)去遍历容器的一种类型。这些零件下面都会一个个派上用场,你不需要现在全背下来。

本文所有代码的"零部件"——结点、树、迭代器、map、set——最终会拼成一份完整可编译、可运行的迷你 STL。我会在讲到 operator[] 之后再给出整份全程序代码,让你能真正复制进编译器跑起来。请你每读完一个概念,就想一想它在整棵树里扮演什么角色,这样看到全程序时不会陌生。

为什么 map 和 set 能共用一个红黑树

我们先想一个本质问题:map 和 set 到底差在哪?

set<K> 是"一堆不重复的 key",它的结点里存的就是一个 key。而 map<K, V> 是"key 到 value 的映射",它的结点里存的是一个 pair<K, V>。看上去差别挺大?但你换个角度看:对红黑树来说,它只需要关心一件事——怎么比较大小。树是搜索树,搜索靠的是比较 key;至于结点里除了 key 还塞了什么别的东西,树本身根本不在乎。

你可以把红黑树想象成一台"按大小排序的机器人",它只负责把一堆东西按某种"大小规则"排好、挂成树。set 丢给它一堆整数,它排队;map 丢给它一堆 pair,它还是排队。机器人关心的不是"你是什么东西",而是"你俩谁大谁小,据此决定往左叉还是往右叉挂"。这就是二者能共用一个底层树的思想基础。

用生活里的例子再想一遍。图书馆的管理员只在乎书的编号顺序,它负责把书按编号排上架,至于一本书是精装还是平装、是小说还是科普,管理员上架时不关心。你的 map 里 pair 的 second(value)就像书的装帧,管理员(树)排序时只看编号(first/key)。set 就像一排只有编号的藏品,树也同样把它们排好。管理员的技艺是同一套,管的东西不同而已。

源码里就是这样做的。看 stl_tree.h 中红黑树的部分声明:

// 红黑树的底层结点:只存颜色和三个指针,数据另放
struct __rb_tree_node_base
{
    typedef bool 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;                        // 结点真正存的值,Value 是个模板参数
};

注意最后那个 Value value_field;——结点里存的是模板参数 Value,也就是说结点到底存 key 还是存 pair,完全由 Value 决定,而在 Value 上做文章,正是源码泛型思想的精髓。set 实例化树时把 Value 填成 key,map 实例化时把 Value 填成 pair,一颗红黑树就同时搞定了两种容器。

为什么源码要把结点拆成"基类"和"派生类"两层?因为红黑树的一些操作(比如找中序前驱、找中序后继、旋转)只依赖结点里的指针和颜色,跟价值类型 Value 完全无关。把它们放进不依赖模板参数的基类,那些通用的算法就能写成一个普通类(__rb_tree_base_iterator),不用为每种数据类型都生成一份。这是一种"把不随类型变化的部分沉到底层"的优化,你以后在公司读大型模板库时会经常撞见这种分层思想。我们的教学实现里为了清晰,调用了模板结点、直接存 _data,所以暂时只需要一层结点;但你要知道源码为什么是两层。

这里要先吐槽一句,源码的命名风格比较随意,容易把人绕晕:set 的模板参数叫 Key,map 的模板参数叫 Key 和 T,而红黑树自己叫 Key、Value、KeyOfValue、Compare——同一个"值"在不同类里叫法完全不同。连写标准库的大神都会有这种"乱弹琴"的时候,你写代码时命名清晰规范,反而能比源码更易读。我们这节课干脆统一一下:key 用 K,map 的 value 用 V,红黑树结点里存的那个数据就用 T。这样接下来看代码会舒服很多。

顺带先摊开一个会在后面反复出现的源码小细节。你看课件里 stl_tree.h 那段声明,写得很明白:

// insert 用的是第二个模板参数的左形参
pair<iterator, bool> insert_unique(const value_type& x);
 
// erase 和 find 用第一个模板参数做形参
size_type erase(const key_type& x);
iterator find(const key_type& x);

也就是说,rb_tree 用**第二个模板参数 Value(即结点里存的 value_type)作为 insert 的参数类型,但用第一个模板参数 Key(key_type)**作为 erase 和 find 的参数类型。这个区别为什么重要,是我们下一节的引子——先记住:"插入的东西"是完整的 T,"找的东西"是精炼的 K。

红黑树模板化:底层结点存什么由 T 决定

现在,我们把之前那棵专门存 key 的红黑树,改造升级成一个"结点数据类型由模板参数决定"的通用树。这是整节课最关键的一步。这一步的精髓在于:把"存什么"的决定权从树里拿出来,交给模板调用方。

先定义结点。结点里存的类型不再是写死的 int,而是模板参数 T:

// 结点颜色枚举
enum Colour
{
    RED,      // 红色,新插入的结点默认都是红的
    BLACK     // 黑色,根结点必须是黑的
};
 
// 红黑树结点模板:T 是结点里真正要存的数据类型
template<class T>
struct RBTreeNode
{
    T _data;                    // 结点真正存的数据,T 可以是 key,也可以是 pair
    RBTreeNode<T>* _left;       // 左孩子指针
    RBTreeNode<T>* _right;      // 右孩子指针
    RBTreeNode<T>* _parent;     // 父结点指针,方便向上调整和迭代器++
    Colour _col;                // 结点颜色
 
    // 构造函数:数据由外部传入,三个指针置空,颜色取默认值
    RBTreeNode(const T& data)
        : _data(data)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _col(RED)             // 默认染红,后面插入时根再单独染黑
    {}
};

相比课件里"构造函数里三个指针置空、颜色不设",我在构造初始值列表里顺手把颜色也初始成 RED(红色)。原因后面讲红黑树插入规则时会说到:新插入的结点一律先染红,红色结点不会让某条路径上的黑色结点数量变多,从而不容易破坏"每条路径黑结点个数相同"这条性质,最多只会破坏"红结点不能有红孩子"这条,而这条正好能靠向上调整救回来。既然新结点几乎必然要染红,把它写进构造函数,代码就更稳妥,也省得每次 new Node 之后都惦记着补一句颜色。

T 可以是 K(set 用),也可以是 pair<const K, V>(map 用)——这就把"存什么"的决定权交还给了使用方。换句话说,底层树的结点已经和"你到底存 key 还是 value"解耦了。

但这里立刻冒出第二个关键问题,也是课件里答疑重点:既然 T 已经决定了结点存什么,为什么模板还要再传一个 K?尤其是 set,K 和 T 完全一样,不是多余吗?

答案在上面那段源码注释里:K 是 find 和 erase 的参数类型。insert 塞进来的是一个完整的 T(对 map 是 pair<key, value>),可 find 和 erase 是"拿着 key 去搜",你调用时传的就是一个精炼的 key。对 set 来说 K 和 T 恰好相等,看不出差别;但对 map 来说,find 传的是 K(一个 key),而 insert 传的是 pair<K, V>(一个键值对),两者根本不是同一个类型。如果没有独立的 K 参数,find 的函数签名就没法写了。

打个比方:insert 是"存进一份完整档案",find 是"报一个学号就查出档案"。map 的档案是(学号, 备注)的合订本,但你查档只用报学号。所以树需要两种入参:完整的档案(T),和用来检索的"学号"(K)。接口设计上必须把这两个类型分开,才能同时满足"能插档案"和"能按学号查"。

明白了 K 的角色,RBTree 的类骨架就很自然了:

// K    : 检索时用的 key 类型(find/erase 的形参类型)
// T    : 结点里真正存的那个数据(map 是 pair,set 是 key)
// KeyOfT: 从 T 里取出 K 的仿函数,树靠它只比较 key
template<class K, class T, class KeyOfT>
class RBTree
{
    typedef RBTreeNode<T> Node;   // 结点类型别名,把一长串模板写短
    Node* _root = nullptr;        // 整棵树的根
    // 其余成员函数(Insert、Find、Begin、End、RotateL、RotateR 等)
    // 都会在后续各节中补全
};

注意 typedef RBTreeNode<T> Node; 这一行。模板类里写 Node 而不是遍地 RBTreeNode<T>,不只图省字——你想想,一个 T 就够长了,后面还挂着 RBTreeNode<T>* 这种一长串,一旦读代码的人眼神不好就容易看岔。起个别名,一劳永逸。

用仿函数提取 key:KeyOfT 是这样解决"我比较谁"的

结点里存 T 之后,麻烦事来了。红黑树插入时要比较大小,它得知道"拿什么来比较"。如果 T 是整数,直接比 T 就行;但要是 T 是 pair<K, V>,直接拿 pair 去比较就会出问题——因为 pair 自带的 operator< 是"先比 first,再比 second",也就是说它会把 key 和 value 一起比。我们想要的是:任何时候都只比 key。

先把这个"问题"彻底看清楚。<utility> 里 std::pair 的 operator< 本质是字典序(词典序)比较:

// 这就是标准库 pair 提供的 operator< 的语义:
// 先比 first,first 相等才比 second
template <class T1, class T2>
bool operator<(const pair<T1, T2>& lhs, const pair<T1, T2>& rhs)
{
    return lhs.first < rhs.first
           || (!(rhs.first < lhs.first) && lhs.second < rhs.second);
}

什么叫字典序?就像查词典:先看首字母,首字母不同就直接出结果;首字母相同才看第二个字母。pair 的 first 就是"首字母",second 是"第二个字母"。于是 (1, "b") < (1, "a") 为假(因为 first 相同、second 决定),(1, "a") < (2, "x") 为真(因为 first 就分出了大小)。把这样的 pair 直接交给树排序,树就会按照"先 key 后 value"去排——这显然不是我们想要的:我们期望 map 里两个 key 相同的 pair 被视为"同一个东西",用哪个 value 根本不影响它在树里的位置。一旦 value 参与排序,同样的 (key, v1) 和 (key, v2) 就会被当成两个不同结点,整个"以 key 唯一"的语义就崩塌了。

这里有个坑:树的内部逻辑是通用的,它根本不知道 T 具体是 key 还是 pair,所以它没法自己去"取出 key"。标准解法是再传一个模板参数 KeyOfT,它是一个仿函数,专门负责从 T 里把 key 抠出来。红黑树在比较时只调用 KeyOfT 的 operator(),至于里面怎么取,是 map 和 set 各自在封装层告诉树的。

map 和 set 分别实现自己的取 key 仿函数:

// 在 map 这一层实现:从 pair<K, V> 里把 key 取出来
struct MapKeyOfT
{
    // 接收一个 pair 对象,返回它的 key(也就是 first)
    const K& operator()(const pair<K, V>& kv) const
    {
        return kv.first;   // pair 的第一个成员就是 key
    }
};
 
// 在 set 这一层实现:set 的 data 本身就是 key
struct SetKeyOfT
{
    // 接收一个 key,原样返回即可
    const K& operator()(const K& key) const
    {
        return key;        // 数据就是 key 本人,不用取
    }
};

看到没,这才叫"对症下药":map 的 data 是 pair,所以要 kv.first;set 的 data 就是 key 本人,所以原样返回。红黑树把"如何取 key"这件事彻底外包了出去,自己只需要记住"调用 kot(某个数据) 就能得到 key"。这就是容器与底层解耦的精髓:底层树不关心 key 长什么样、藏在哪里,它只认 kot(数据) 这个统一的取法。

注意我给两个 operator() 都加了 const。这不是可有可无的风格洁癖:拿 key 是一个"只读"动作,它不该也不能改动 T,声明成 const 就限制了 operator() 内部不能误改数据;同时,只有当 operator() 是 const 时,才能在一个 const 对象上调用它(后面 map 的 const 场景才顺)。另外返回类型我写的是 const K&——返回"常引用"而非拷贝,既避免了拷贝成本,又保证了取出来的 key 不会被调用的地方顺手改掉。养成给仿函数写 const、给"只读取字段"写 const 引用 的习惯,能防住一整类低级 bug。

现在你就能彻底理解为什么树"只关心 key"了:不是树豁达,而是比较的每一步都被我们截胡到 KeyOfT 上,value 压根没机会露头。map 的结点里虽然躺着一个完完整整的 pair<const K, V>,但树做插入、查找、求前后继时,触摸到的永远只有 pair.first。

插入的统一:MapKeyOfT 与 SetKeyOfT 里藏着同一套逻辑

有了 KeyOfT,红黑树的 Insert 内部逻辑对 set 和 map 就完全一样了:全程用 kot(cur->_data) 取 key 参与比较。

你已经接触过红黑树的插入,这里我把完整的泛型化 Insert 一次性摆出来。它的结构分成明确的四步:找位置、挂结点、往上调整、收尾染色。

template<class K, class T, class KeyOfT>
class RBTree
{
    typedef RBTreeNode<T> Node;
    Node* _root = nullptr;
 
public:
    // 插入:返回 pair<指向"新结点或已存在结点"的迭代器, 是否真的插入了>
    // 第二个元素为 false 表示 key 重复,插入失败
    pair<Iterator, bool> Insert(const T& data)
    {
        if (_root == nullptr)              // 树为空,第一个结点直接当根
        {
            _root = new Node(data);        // 创建新结点(颜色已在构造函数里给 RED)
            _root->_col = BLACK;           // 根必须染成黑色(红黑树性质一)
            return make_pair(Iterator(_root, _root), true);
        }
 
        KeyOfT kot;                        // 实例化仿函数,用来取 key
        Node* parent = nullptr;            // 记录查找路径上的父结点
        Node* cur = _root;                 // 从根开始往下找插入位置
 
        // 第一步:按 key 找到应该挂载的位置(标准的 BST 向下搜索)
        while (cur)
        {
            if (kot(cur->_data) < kot(data))     // 当前 key 比待插 key 小
            {
                parent = cur;                    // 往右子树走
                cur = cur->_right;
            }
            else if (kot(cur->_data) > kot(data)) // 当前 key 更大
            {
                parent = cur;                    // 往左子树走
                cur = cur->_left;
            }
            else
            {
                // key 已存在:取消插入,返回那个已存在的结点 + false
                return make_pair(Iterator(cur, _root), false);
            }
        }
 
        // 第二步:把新结点挂到 parent 的孩子位置
        cur = new Node(data);              // 新结点默认红色(构造函数保证)
        Node* newnode = cur;               // 记住新结点,最后要返回它
        if (kot(parent->_data) < kot(data))
            parent->_right = cur;          // 比父大,挂右边
        else
            parent->_left = cur;           // 比父小,挂左边
        cur->_parent = parent;             // 设置父指针,形成双向链接
 
        // 第三步:调整颜色、必要时旋转,维持红黑树五条性质
        while (parent && parent->_col == RED)  // 只可能出现"父子双红"的违规
        {
            Node* grand = parent->_parent;     // 祖父结点
            if (parent == grand->_left)        // 父是祖父的左孩子
            {
                Node* uncle = grand->_right;   // 叔叔是祖父的右孩子
                if (uncle && uncle->_col == RED)
                {
                    // 情况一:叔叔存在且为红,直接变色,然后向上继续
                    parent->_col = BLACK;      // 父变黑
                    uncle->_col = BLACK;       // 叔变黑
                    grand->_col = RED;         // 祖父变红,作为新的 cur
                    cur = grand;               // 问题转移到祖父头上
                    parent = cur->_parent;     // 继续检查上一层
                }
                else
                {
                    // 情况二/三:叔叔为黑或不存在,旋转 + 变色
                    if (cur == parent->_left)
                    {
                        RotateR(grand);        // cur 也在左,做一次右单旋
                        parent->_col = BLACK;
                        grand->_col = RED;
                    }
                    else
                    {
                        RotateL(parent);       // cur 在右,先对父左旋
                        RotateR(grand);        // 再对祖父右旋,形成左右双旋
                        cur->_col = BLACK;
                        grand->_col = RED;
                    }
                    break;                     // 旋转后子树已平衡,结束
                }
            }
            else                                // 父是祖父的右孩子,镜像对称
            {
                Node* uncle = grand->_left;
                if (uncle && uncle->_col == RED)
                {
                    parent->_col = BLACK;
                    uncle->_col = BLACK;
                    grand->_col = RED;
                    cur = grand;
                    parent = cur->_parent;
                }
                else
                {
                    if (cur == parent->_right)
                    {
                        RotateL(grand);        // 右单旋
                        parent->_col = BLACK;
                        grand->_col = RED;
                    }
                    else
                    {
                        RotateR(parent);       // 右左双旋
                        RotateL(grand);
                        cur->_col = BLACK;
                        grand->_col = RED;
                    }
                    break;
                }
            }
        }
        _root->_col = BLACK;                   // 循环结束,把根强制染黑
        return make_pair(Iterator(newnode, _root), true); // 返回新结点迭代器
    }
 
private:
    // 左单旋:把 parent 作为旋转轴向下旋转,提升它的右孩子
    void RotateL(Node* parent)
    {
        Node* subR = parent->_right;          // 待提升的右孩子
        Node* subRL = subR->_left;            // 右孩子的左子树,要转嫁
        parent->_right = subRL;               // 把 subRL 挂到 parent 右边
        if (subRL)
            subRL->_parent = parent;          // 更新 subRL 的父指针
 
        Node* parentParent = parent->_parent; // 记下 parent 的原父
        subR->_left = parent;                 // parent 变成 subR 的左孩子
        parent->_parent = subR;               // parent 的父指向 subR
 
        if (parentParent == nullptr)          // parent 原本是根
        {
            _root = subR;                     // subR 晋升为根
            subR->_parent = nullptr;
        }
        else                                  // parent 原本不是根
        {
            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* parentParent = parent->_parent;
        subL->_right = parent;
        parent->_parent = subL;
 
        if (parentParent == nullptr)
        {
            _root = subL;
            subL->_parent = nullptr;
        }
        else
        {
            if (parent == parentParent->_left)
                parentParent->_left = subL;
            else
                parentParent->_right = subL;
            subL->_parent = parentParent;
        }
    }
};

你可以对比一下,这段 Insert 里从头到尾用的是 kot(cur->_data)、kot(data),没有任何一处直接假设 T 是 key 还是 pair。也就是说,map 和 set 插入的流程本来就是同一套,我们只是把"取 key"这一小步用仿函数抽象了出来。这正是"不要重复造轮子"的工程体现——写一遍树,两个容器共用。

我要特别请你注意这段代码里每一个比较都只发生在 kot(数据) 得到的 key 上。比如"决定往左还是往右",比的是 kot(cur->_data) < kot(data);"判断是否重复",比的是 key 是否相等。value 从始至终没有出现在任何比较表达式里。这正是前面那个问题的答案的代码兑现:map 存的是 pair,但树自始至终只拿 key 排序。你甚至可以在这个 Insert 里搜一遍,全场找不到一个 .second 或 value 出现的位置。

顺带问一个可能困扰你的问题:代码里判断重复用的是"既不大也不小"(!< && !>),为什么不直接"等于"?因为树并不知道 T 有没有定义 operator==,它只要求 T 能比较"小于"。用"既不小于、又不大于"来推导"相等",就把"需要定义 <"这一个条件当成了全部前提,而不需要 ==。这是泛型容器一个非常经典的取舍:只要求最弱的能力(<),用两次 < 拼出"相等"的判断。等会儿讲"对自定义类型开放比较"那一节,你会看到为什么这个设计对自定义类型格外友好。

再聊聊 RotateL / RotateR 里那段"接回祖父"的分支。为什么要区分"parent 是不是根"?因为旋转后要提升的 subR 取代了 parent 的位置,如果 parent 曾是整棵树的根,那么 subR 必须接住 _root 这个指针、并把自己的 _parent 置空;如果 parent 只是某个祖父的孩子,那么 subR 就要接替 parent 挂到祖父的对应孩子槽位上,并修正 _parent。这段逻辑最容易出问题的地方是:漏更新祖父的孩子槽位,或者忘记把 subRL/subLR 的 _parent 指向 parent。初次手写红黑树的同学,十有八九的 bug 都埋在这一行行的指针重接里。这也是为什么红黑树旋转往往配着"先画图,再写码"——纸面上把每条指针箭头的去向画清楚,代码才不会乱。

对了,你需要想一想:为什么 Insert 返回值要从课件初版的 bool 升级成 pair<Iterator, bool>?因为后面 map 的 operator[] 强烈依赖它:既要知道"这个 key 是否存在",又要拿到"这个 key 对应的结点(好改它的 value)"。如果只返回 bool,operator[] 想找到那个结点还得再 find 一次,白白多走一遍树。把"插入结果迭代器 + 是否新增"打包返回,operator[] 拿到手就能直接用。这个细节我们先记住,等会儿实现 operator[] 时会专门讲。

红黑树的迭代器与 begin/end

map 和 set 的用户要遍历容器,必须得有个迭代器。迭代器的大框架和之前实现 list 的迭代器思路完全一致:用个类把结点的指针包起来,然后重载一堆运算符,让它用起来像指针。

难点不在框架,而在 operator++ 和 operator--。为什么难?因为 map/set 遍历走的是中序遍历(左子树 -> 根 -> 右子树),而树里每个结点跟它的"下一个、上一个"之间并没有物理上的邻居关系,我们得靠它在树里的位置去推算。list 的结点有 next/prev 物理指针,++ 就是直接跳下一个;树的结点只有上/下/左/右,++ 到底跳到谁,得看当前结点在遍历序列里的相对位置。

先说结论,++ 的核心逻辑就一条:不看全局,只看局部,只推导"中序里要访问的下一个结点"是谁。

  • 如果 it 指向的结点右子树不为空:说明当前结点访问完了,下一个应该是"右子树的中序第一个"。一棵树的中序第一个是谁?是最左边的结点。所以直接跳到右子树,一路往左走到底。
  • 如果右子树为空:说明不光当前结点访问完了,连它所在的这棵子树也访问完了。下一个结点一定藏在祖先里。我们需要沿着"当前结点 -> 根"的祖先路径往上爬:
    • 如果当前结点是父的左孩子,根据"左->根->右",下一个访问的正好是父,直接返回父。
    • 如果当前结点是父的右孩子,说明"父的整棵左子树+父"都访问完了,还得继续往上找,一直找到"某个孩子是它父的左"的那个祖先,那才是中序的下一个。

用课件里的例子走一遍你就有体感了。设 it 现在指向 25,而 25 在树里的位置是 30 的左孩子,且 25 右子树为空:因为 25 是父(30)的左孩子,中序里"左子树逛完就轮到根",所以 ++ 直接回到 30,这就是第一种"爬一层就停"的情况。再设 it 指向 15,15 是 10 的右孩子、右子树为空:15 所在的以 10 为根的子树的"10 的左子树 + 10"都逛完了,15 又是 10 的右孩子,所以 15 这棵子树逛完了;继续往上,10 是谁的孩子?10 是 18 的左孩子,于是下一个就是 18——这就是第二种"一路爬到'孩子的父'为止"的情况。

operator-- 的思路和 ++ 完全对称:把"左"和"右"互换,往上爬的条件改成"当前结点是父的左孩子就继续爬"。唯一多出来的一点是处理 --end():因为 end 我们在底层用 nullptr 表示,当用户对 end() 做 -- 时,它该指向中序最后一个结点,也就是整棵树最右边的结点,所以要做一次特判。

下面把迭代器完整实现出来。T 是数据类型,Ref 是引用类型(T& 或 const T&),Ptr 是指针类型(T* 或 const T*)——把引用和指针也做成模板参数,是 STL 容器迭代器的通行做法。iterator 解引用返回 T&(可改),const_iterator 解引用返回 const T&(只读)。这是后面 set/map 控制"能否修改"的关键开关。

// 红黑树迭代器模板
// T 是数据类型,Ref 是解引用后的引用类型(T& 或 const T&),Ptr 是指针类型
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)
    {}
 
    // ++:走到中序的下一个结点
    Self& operator++()
    {
        if (_node->_right)                    // 右子树不为空
        {
            // 下一个是中序第一个 -> 右子树最左结点
            Node* leftMost = _node->_right;
            while (leftMost->_left)
                leftMost = leftMost->_left;
            _node = leftMost;
        }
        else                                  // 右子树为空,往上找祖先
        {
            Node* cur = _node;
            Node* parent = cur->_parent;
            // 只要是父的右孩子就继续爬,直到找到"是父左孩子"的结点
            while (parent && cur == parent->_right)
            {
                cur = parent;
                parent = cur->_parent;
            }
            _node = parent;                   // 那个祖父就是中序下一个(可能为 nullptr 即 end)
        }
        return *this;
    }
 
    // --:走到中序的前一个结点
    Self& operator--()
    {
        if (_node == nullptr)                 // 针对 --end() 的特例
        {
            // 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; } // 相等
};

你可能要问:迭代器里为什么要多背一个 _root 指针,多占一份空间?纯粹是因为我们选择了"用 nullptr 表示 end()"这个简化方案。--end() 时 _node 是空,得从树的根一路往右走到最右结点,如果没有 _root,这段就没法实现。而源码 SGI 的红黑树用的是哨兵位头结点当 end(),--end() 直接拿到的是 header 的左指针指向的最小结点(源码迭代器里甚至不需要存根指针,靠 header 就能自我定位)。我们为了让"--end()"能工作,就需要保存一份树根。这是"简化实现"换来的一个必要代价,你理解它为什么存在就行了。

关于 ++ 走到最后一步 _node = parent 时 parent 可能为 nullptr:这正是中序遍历走完整个树的情形。想象遍历到最右的那个结点,它右子树为空,而且它一定是某个祖先的"右链"末端——沿 parent 一路往上会一直满足 cur == parent->_right,直到 parent 变成 nullptr。此时 _node 被置空,恰好和 end()(用 nullptr 表示)对上。也就是说,it == end() 这个"循环终止"的判断,是靠 ++ 一路把 _node 推到空来自然成立的。这是本设计的精巧之处,也提醒你:绝不要对 end() 解引用——此时 _node 为空,*end() 会去解引用空指针,行为未定义,多半直接崩溃。这正是标准库容器迭代器的通用戒律:"不能解引用 end"。

那 --end() 又怎么解释?语义上,end() 是"最后一个元素的后一个位置",--end() 就该回到"最后一个元素",即中序最后一个结点 = 整棵树最右结点。所以我们特判 _node == nullptr:从根一路 _right 走到最右。若树为空,则 --end() 依然得到空,行为不会越界,最多产生"奇怪的自我引用",这属于你已经对空容器做越界操作的病态输入,不在正常使用契约内。

有了迭代器,begin 和 end 就顺理成章了。begin() 要返回中序的第一个结点,那就是整棵树最左边的结点;end() 我们用一个指向 nullptr 的迭代器来表示。同时我们提供常量和非常量两个版本——重载规则是:const 版本的函数只能被 const 对象调用,并返回只读的 ConstIterator:

// 在 RBTree 类里补充的类型别名
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);
}
 
// 中序遍历终点:用 nullptr 表示
Iterator End()
{
    return Iterator(nullptr, _root);      // 哨兵,指向空
}
 
// const 版本:返回只读迭代器
ConstIterator Begin() const
{
    Node* leftMost = _root;
    while (leftMost && leftMost->_left)
        leftMost = leftMost->_left;
    return ConstIterator(leftMost, _root);
}
 
ConstIterator End() const
{
    return ConstIterator(nullptr, _root);
}

请注意 Begin() 里的 while (leftMost && leftMost->_left) 那个 leftMost && 的前置判空。它在防什么?空树。当 _root 是 nullptr 时,leftMost 一上来就是空,如果不先判断 leftMost 是否为空就贸然去读写 leftMost->_left,就会解引用空指针崩溃。这个前缀 && leftMost 保证空树时直接退出循环,返回 Iterator(nullptr, _root),恰好等于 End()。于是你会得到一个漂亮的性质:空树的 begin() 与 end() 相等,for (it = c.begin(); it != c.end(); ++it) 这种循环在空树上一个元素都不跑就自然结束。这个"空容器 begin==end"的约定是所有标准容器的通用语义,我们这里顺手也满足它。

有同学会问:标准库里不是用了一个哨兵头结点当 end() 吗?你说得没错。SGI 的红黑树确实加了一个"哨兵位头结点":它和根互为父,左指针指向最左结点,右指针指向最右结点。用哨兵的好处是很多操作不用判空:begin() 直接取 header 的左孩子,end() 就是 header 本身,--end() 直接取 header 的右孩子,连"空树"都不用特判。但对我们的教学实现来说,用 nullptr 当 end() 完全够用——--end() 那一个特殊分支我们已经处理好了,空树判空也处理了。他那个实现能做的,咱这个也能做,只是边界处理的位置不同。先把能跑的版本写对,再谈优化。

operator-> 这里多叮嘱一句:Ptr operator->() 返回 &_node->_data,也就是"数据指针"。当容器元素是个结构(比如 map 里的 pair)时,it->first 会被编译器翻译成 (it.operator->())->first,这样就能用箭头一路穿到 pair 的成员。后面 map 遍历打印 it->first : it->second 靠的正是它。

比较的开放性:让自定义类型也能上树

现在把视角从容器内部拉高到"用户视角"。你也看出来了,一棵树整个生命里对结点类型唯一的、死硬的要求就是:能拿 key 做 < 比较。所以当我们把自定义类型塞进 map/set 时,只需要满足一个条件——给这个类型提供一种 operator<(严格弱序)即可。

什么叫做"严格弱序"?简单说就是:a < a 必须为假、比较具有传递性(若 a<b 且 b<c 则 a<c)、而且任意两个元素要么 a<b、要么 b<a、要么相等(等价),三者恰好一个成立。所有依赖排序的标准容器都要求这个性质,否则树会"排得乱七八糟",find 时甚至会找不到明明存进去了的元素。让你第一次体会"排序容器 = 编译器 + 你约定一个规则"这句话。

来看一个自定义类型的例子。我们希望把学生 Student 放进 myset,并按"学号"排序:

#include <iostream>
#include <string>
using namespace std;
 
// 自定义类型:学生。我们要让它能进树,需要给它定义 <
struct Student
{
    int    _id;      // 学号,决定排序
    string _name;    // 姓名,不影响排序
 
    Student(int id, const string& name) : _id(id), _name(name) {}
};
 
// 关键:重载 operator<,只按学号比
bool operator<(const Student& lhs, const Student& rhs)
{
    return lhs._id < rhs._id;
}
 
int main()
{
    // 用 std::set 演示读者最熟悉的形态,同一套思路适用于我们的 myset
    set<Student> s;
    s.insert(Student(3, "小红"));
    s.insert(Student(1, "小明"));
    s.insert(Student(2, "小刚"));
 
    // 按学号从小到大输出:小明->小刚->小红
    for (const Student& st : s)
        cout << st._id << ":" << st._name << " ";
    cout << endl;
    return 0;
}

这里有三个要点。第一,operator< 只跟 _id 有关、跟 _name 无关,所以姓名不同的两位同学只要学号相等就被视为"同一个元素",这跟 map 里 pair 的 second 不参与排序是同一件事的两种表现——你终于能体会为什么树要把"比较维度"从"数据本身"里剥出来了。第二,如果你不给自定义类型写 operator< 就硬塞进 set,编译器会在 a < b 那个位置报"没有匹配的 operator<"错误——好消息是报错非常具体,能立刻定位到是哪个对象不会比。第三,Student 里我写了成员和构造函数但没有默认构造函数,用 set 没问题;可一旦换成 map<Student, X>,operator[] 因为要"插入一个默认构造的 value",就会要求 X 能默认构造——这是个埋得很深的坑,operator[] 那节我们会专门回头讲。

再进一步,源码里 set/map 其实还允许你自定义比较规则,而不必非得给类型重载 <。看课件开头那份声明,模板是:

// 课件里截取的源码声明:模板里多出 Compare(默认 less)与 Alloc 两个参数
template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set;

多出来的 Compare 是一个"比较仿函数",默认用 std::less<Key>(也就是调 <)。如果你想让 set 按"自定义规则"(比如降序、按字段 B 而非字段 A 排序)来排,就传一个自己的比较仿函数进去,树内部就不再调用 <,而是调用 Compare(a, b) 判断"a 是否排在 b 前面"。这样连 < 都不用给类型重载了——只要给树一个"判定谁在前"的仿函数即可。

这个概念正是我们 KeyOfT 思路的直接延伸:KeyOfT 解决了"取哪个值来比",Compare 解决"按什么规则比"。两者都是把"策略"通过仿函数注入给通用的树,让树本身保持对具体类型的无知。你可以把 KeyOfT 和 Compare 看成树的两个可插拔"指令卡":一张告诉它"比较对象藏在哪",另一张告诉它"谁排前面"。容器底层与具体类型彻底解耦,靠的就是把这两张卡交给使用者自由选择。

顺带修正一个我们教学版本的细节:课件/常见简化写法里 Insert 用 kot(a) < kot(b) 和 kot(a) > kot(b) 两种运算符。更"通用"的写法其实只用 < 切换方向:

// 等价且只依赖 < 的写法(标准库语义一致)
if (kot(cur->_data) < kot(data))          // cur 的 key 小 -> 往右走
    cur = cur->_right;
else if (kot(data) < kot(cur->_data))     // data 的 key 小 -> 往左走
    cur = cur->_left;
else
    break;                                // 既不小也不大 -> 视为 key 重复

为什么这样更好?因为这样树就只要求 K 具备 < 一个能力,而你给自定义类型只需重载 < 一个运算符即可(Student 例子正是这么干的)。反过来,如果你让树既调 < 又调 >,自定义类型就得同时重载两个运算符,门槛翻倍。标准库深谙此道——它的比较仿函数默认就是 std::less,只依赖 <。所以我们的 Find 我会用这种"只用 <"的写法,并在下面实现 Find 时体现它。

现在我们顺手把课程里 find 的实现补全——注意课件里的 Find 存在一处与泛型化自相矛盾的地方(它写着 cur->_kv.first,可一旦 T 被泛型化成 key 或 pair,结点里根本没有 _kv 这个成员),正确写法应该继续依赖 KeyOfT 取出 key 再比较:

// 按 key 查找:返回指向该结点的迭代器,找不到返回 End()
// 注意:这是在 RBTree 类内部,所以使用的是本类的 Iterator(大写)别名
Iterator Find(const K& key)
{
    KeyOfT kot;                     // 取 key 的仿函数
    Node* cur = _root;
    while (cur)
    {
        if (kot(cur->_data) < key)          // cur 的 key 比要找的小
            cur = cur->_right;
        else if (key < kot(cur->_data))     // cur 的 key 比要找的大
            cur = cur->_left;
        else
            return Iterator(cur, _root);    // 相等,找到了
    }
    return End();                           // 走到空,没找到
}

Find 的入参是 K(一个 key),它把结点里的 key kot(cur->_data) 拿出来与传入的 key 比大小。这里你又一次看到 K 那个模板参数的意义:find 是"拿 key 找结点",参数类型必须得是 K,不能是 T(map 的 T 是 pair,用户 find 时只给 key,不给你再造个 pair)。这就是"为什么树除了 T 还要传 K"最直接的落地。

set 的 insert:一个 key

地基打完了,现在盖楼。set 这一层极其简单,本质上就是个"传话筒":把暴露给用户的接口,转发给底层的红黑树。它的要点有两个:

第一,结点的 T 填 const K。set 的语义是"元素不可被修改",所以第二个模板参数给 const K。这一下就从类型层面卡死了"改 set 元素"的行为——编译期直接报错,比运行时靠约定安全得多。(后面你会看到,set 的 iterator 解引用类型于是自动变成 const K&,连"用迭代器改元素"的路径也被同一处设计堵死。)

第二,typedef 导出迭代器类型。迭代器类型定义在 RBTree 里,属于类内依赖类型,所以这里要用 typename 关键字告诉编译器"这是类型,不是成员变量"。这是模板体系里一个特别容易踩的坑,忘了写 typename 编译就会报错。

// 迷你 set:只支持插入、查找、遍历
template<class K>
class set
{
    // 仿函数:set 的 data 就是 key,原样取出
    struct SetKeyOfT
    {
        const K& operator()(const K& key) const
        {
            return key;      // 数据本身就是 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(); }
 
    // 插入单个 key
    pair<iterator, bool> insert(const K& key)
    {
        return _t.Insert(key);     // 转发给底层树
    }
 
    // 按 key 查找
    iterator find(const K& key)
    {
        return _t.Find(key);       // 转发给底层树
    }
 
private:
    // 关键:第二个模板参数用 const K,保证 set 元素只读
    RBTree<K, const K, SetKeyOfT> _t;   // 底层就是一棵通用红黑树
};

有一个非常容易忽略、却极能体现设计精妙的点值得单独拎出来讲。因为 RBTree<K, const K, SetKeyOfT> 里 T 被填成 const K,所以:

typedef RBTreeIterator<const K, const K&, const K*> Iterator;        // 底层实际展开
typedef RBTreeIterator<const K, const K&, const K*> ConstIterator;   // 完全一样

看见了吗?set 的 iterator 和 const_iterator 在类型上完全等价——因为 T 已经是 const K,无论解引用成 T& 还是 const T&,落到用户手里的都是 const K&。也就是说,set 的普通 iterator 天然就具备了只读性,"改成 set 元素"这条路从迭代器类型这个层面就被焊死了。这与 STL 真实 std::set 的语义完全一致(std::set 的迭代器本来就不允许改 key),但实现原理略有差异:真实 STL 的 set<T> 直接让 value_type = const T,而我们是通过同一个 const 跑遍了整条链。不管怎样,"set 元素只读"这件事,靠类型系统保证,而非运行时约定——你即便写下 *it += 2; 想改,编译期就直接拒绝。

试试反向思考:如果把 set 的 T 错填成 K(不写 const),会发生什么?用户拿 iterator 就能 *it = 某个值 改写元素的 key——那可不得了,红黑树是按照 key 排好序的,你偷偷把某结点的 key 改成别的值,整棵树的顺序就塌了,之后 find 全乱套。正因为这个后果如此严重,C++ 才选择在类型层面"不许改"。这一课浓缩成一句话:凡是会影响排序的键,都应该被做成不可变。

map 的 insert:pair<key, value>

接下来是 map。map 和 set 唯一的结构性差别,就是结点里存的数据从 K 换成了 pair<const K, V>。我们之前说过,pair 第一个成员叫 first,第二个叫 second,map 里 first 就是 key、second 就是 value。而 const K 的意义,是只允许修改 value,禁止修改 key——key 一旦可变,红黑树的排序关系就会瞬间被打破,树就废了。所以 map 必须把 first 钉死为 const。

MapKeyOfT 负责从 pair 里取 first,底层树的比较因此在"只看 key"的意义上工作得严丝合缝。这正好回应了前面埋下的疑问:map 的结点明明存的是 pair<key, value>,为什么底层比较却只比 key?——因为所有比较动作都经过了 MapKeyOfT,它只把 first 送进去比较,second 从头到尾不参与。pair 自带的那种"first 相同再比 second"的 operator<,在我们的封装里永远都不会被触发。

想想看,这是不是跟 Student 那个自定义类型的故事一模一样?pair 的 second 之于 first,就像 Student 的 _name 之于 _id——都是"不参与排序的附件"。map 用 pair<const K, V> 把"排序键"和"附件"捆在一起存在同一结点里,但树的排序只看 first。你把 map 理解成"一本以键为索引、值随手带上的字典",就通透了:字典条目的排列由词条(键)决定,解释文字(值)再多也不影响它在字典里的位置。

// 迷你 map:key 带映射 value
template<class K, class V>
class map
{
    // 仿函数:从 pair 里把 key(first)取出来
    struct MapKeyOfT
    {
        const K& operator()(const pair<K, V>& kv) const
        {
            return kv.first;   // pair 的 first 就是 key
        }
    };
 
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(); }
 
    // 插入一个 key -> value 的映射
    pair<iterator, bool> insert(const pair<K, V>& kv)
    {
        return _t.Insert(kv);      // 底层树只认这个 pair
    }
 
    // 按 key 查找
    iterator find(const K& key)
    {
        return _t.Find(key);
    }
 
    // 用 [] 访问或修改:不存在就插入,存在就返回 value 引用
    V& operator[](const K& key)
    {
        // 先尝试插入一个"key 配默认构造的 value"
        pair<iterator, bool> ret = insert(make_pair(key, V()));
        // ret.first 指向 key 所在的结点(刚插入的或已存在的)
        return ret.first->second;   // 取出并返回它的 value 引用
    }
 
private:
    // 关键:pair 的 first 是 const K,保证 key 不可改、value 可改
    RBTree<K, pair<const K, V>, MapKeyOfT> _t;  // 底层同样是那棵通用树
};

跟 set 对照着看,map 的 iterator 由于 T = pair<const K, V>,解引用类型是 T&,也就是 pair<const K, V>&——first 是 const K 不可改,second 是 V& 可改。这恰好就是 map 想要的精确语义:键不许动,值随便改。 const 出现在 T 内部(而不是 T 本身),意味着它能做到"一部分 const、一部分可变",这是"把 const 放在 pair 的第一个成员上"这个设计最漂亮的地方。如果你把 map 的 T 错填成 pair<K, V>(去掉 first 的 const),*it 就能把 pair.first 改成别的 key,树的排序又崩了——跟 set 忘记 const K 是同一个祸根。

insert 接口里还有一个值得细看的类型小把戏:insert(const pair<K, V>& kv) 的用户接口参数是 pair<K, V>,但转发进去的 _t.Insert(kv) 期望的是 pair<const K, V>。这俩能对上吗?能——因为标准库的 pair 提供了一个模板化的转换构造函数,pair<A, B> 可以隐式转换成 pair<const A, B>(把 first 的 const 加上)。于是用户舒舒服服地传 make_pair(k, v) 或 { k, v },底层稳稳收到 pair<const K, V>。这也顺便解释了为什么很多教学实现里 insert 的形参就写成 pair<K, V> 或直接 const pair<K,V>&——正是为了用户在调用处不必亲手写 const。

map 的 operator[] 实现思路

最后我们聊聊 map 里最"魔法"的东西:operator[]。它为什么能做到 dict["没有的key"] = 值 也能赋值成功?因为 C++ 的 operator[] 对 map 而言,语义并不是"按索引取",而是"给我这个 key 对应的 value,不存在我就先插一个空的"。

把这句话彻底读透之前,先做一个生活类比。operator[] 就像一个前台:你报一个工号,它要么把人领出来给你,要么先给这个空位站上一个"空的接待员"(默认构造的 value),然后把人领给你。你点名的人不存在?没关系,它负责自动补位——这正是 dict["不存在的词"] = "释义" 能直接"凭空造词条"的原因。

拆开看 operator[] 的三行代码:

第一行,insert(make_pair(key, V()))。注意它在做"探查式插入":如果 key 已经存在,insert 会返回 (指向已存在结点的迭代器, false);如果 key 不存在,insert 会真的插入一个 (key, V()),其中 V() 是用值初始化构造出的"空的" value(比如整数 0、空字符串、nullptr),然后返回 (指向新结点的迭代器, true)。这里能插一个"默认构造的空 value",就要求 V 具备默认构造函数——这是 operator[] 对 value 类型唯一的隐含要求。想想 map<int, int>:V() 就是 0;map<string, string>:V() 就是空串;可要是换成 map<int, 你自定义的没有默认构造的X>,operator[] 就编不过。这是用 operator[] 的一个深坑,来源就在这一行 V() 上。

第二行,ret.first 拿到那个结点——不管它是一开始就有的,还是刚被我们插进去的,它一定是"key 对应的那个结点"。这正是我们前面把 Insert 返回 pair<Iterator, bool> 的意义:ret.first 直接是"指向 key 所在结点的迭代器",无需再 find 一遍。若 Insert 只返回 bool,这里就得再 find 一次 O(log n),纯属浪费。

第三行,ret.first->second 用 -> 走迭代器的 operator-> 拿到 pair,再取 .second 就是 value,直接返回它的引用。V& 引用能返出去,外部赋值或读取都作用在树里那个真实的 second 上,这就是"改的确实是容器里的值,不是拷贝"的根本保证。

于是三种用法全部成立:

dict["left"] = "左边,剩余";   // key 已存在:拿到它的 value 引用并覆盖
dict["insert"] = "插入";       // key 不存在:先插入空 value,再赋新值
dict["string"];                // key 不存在:只插入一个空 value,不做赋值

前两句无论 key 是否存在,operator[] 都返回那个 key 对应的 value 引用,赋值就生效。第三句更狠:什么都不赋,只是 dict["string"] 这一句"读"也会把 string 这个 key 闷声插进去(配一个空 value)——因为它内部照样先 insert(make_pair("string", string()))。这就是 operator[] "能读能写还能增"的魔法来源。

这就是为什么 operator[] 既能"读"又能"写"还能"增"。你可能会说,这哪里是"取下标",分明是"insertOrReturn"——对,你抓住了本质。也正因如此,用 operator[] 去判断"key 是否存在"是不严谨的,因为它总会凭空插一个空 value 进去;要精确判断存在性,应该用 find。这是 map 使用者非常容易踩的一个坑,值得记在脑子里。光是"读一下不存在的 key"这种看似无害的操作,就会让容器悄悄膨胀,在长循环里尤其隐蔽。

再补一个约束视角。正因为 operator[] 有"插空"的副作用,标准库里它没有 const 版本:一个 const map 是不能用 [] 读的——你读键却可能改容器,这在 const 语义上是自相矛盾的。真正"只读、查不到就报错"的接口是 at()(标准库 map::at 在 key 不存在时抛 std::out_of_range)。这跟我们教学 map 目前只提供非 const operator[] 的设计完全吻合——const 地图上要用 find 或 at,而不是 []。

我们落一段完整的测试,把这三件事串起来走一遍:

void test_map()
{
    bit::map<string, string> dict;              // 建立 string->string 的字典
    dict.insert({ "sort", "排序" });            // 用 insert 插入三个词条
    dict.insert({ "left", "左边" });
    dict.insert({ "right", "右边" });
 
    dict["left"] = "左边,剩余";                // key 存在:修改 value
    dict["insert"] = "插入";                    // key 不存在:插入新词条
    dict["string"];                             // key 不存在:只插空 value
 
    bit::map<string, string>::iterator it = dict.begin();
    while (it != dict.end())
    {
        // 你可以试试解开下面这行:改 key 会在编译期被拒
        // it->first += 'x';
        it->second += 'x';                      // 改 value 是允许的
        cout << it->first << ":" << it->second << endl; // 用 -> 访问 key 和 value
        ++it;                                   // 中序遍历推进
    }
    cout << endl;
}

你运行这段代码,看到的是一个按 key(字符串字典序)排好序的字典,其中 "insert" 和 "string" 这两个词条是 operator[] 顺手帮我们插进去的,而 it->second += 'x' 在每个 value 末尾又追加了字符 x。map 的这份"自动排序 + 快捷赋值"的体验,全部建立在我们前面做的每一层抽象之上。同时也演示了那两行关键结论:it->first 由于 first 是 const K 不可改(解开那行注释试试,编译期就报错),it->second 则可以随便改。

set 的测试就简单多了,验证插入、遍历和"元素不可改":

void test_set()
{
    bit::set<int> s;                            // 建一个整数 set
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 }; // 乱序数据
    for (auto e : a)
        s.insert(e);                            // 逐个插入
 
    for (auto e : s)                            // 范围 for 遍历底层是中序
        cout << e << " ";                       // 会输出有序结果
    cout << endl;
}

这里 for (auto e : s) 是范围 for,它会被编译器翻译成使用 begin()/end() 的循环,而我们的 begin()/end() 返回的就是红黑树迭代器,所以遍历天然就是中序,输出必然是升序的 1 2 3 4 5 6 7 14 15 16。auto e 按值拷贝,你如果想改的话得写 auto& e,但对 set 来说 *it 是 const K&,范围 for 用 auto& e 也改不了——这正是 set 元素只读设计在语法糖(范围 for)下依然有效的原因。

到这里,你应该已经把文章里出现的所有零件在心里拼过两三次了。为了让你能真正"复制即编译、编译即运行",我把整棵树的完整实现整合成一份可直接运行的完整程序。它包含 RBTreeNode、RBTree(含 Insert/Find/Begin/End/旋转变色/析构 Destroy)、RBTreeIterator、bit::map、bit::set 以及两个测试函数和一个 main。你把它存成一个 const.cpp 用支持 C++11 的编译器编译即可跑(比如 MSVC/GCC/Clang):

#include <iostream>
#include <string>
#include <utility>
using namespace std;
 
namespace bit
{
    // ---------- 结点 ----------
    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), _col(RED) {}   // 新结点默认染红
    };
 
    // ---------- 迭代器 ----------
    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* leftMost = _node->_right;
                while (leftMost->_left) leftMost = leftMost->_left;
                _node = leftMost;
            }
            else
            {
                Node* cur = _node;
                Node* parent = cur->_parent;
                while (parent && cur == parent->_right)
                {
                    cur = parent;
                    parent = cur->_parent;
                }
                _node = parent;   // 可能为 nullptr,即 end()
            }
            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;
 
        RBTree() = default;
        ~RBTree() { Destroy(_root); }            // 记得释放所有结点
 
        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); }
 
        pair<Iterator, bool> Insert(const T& data)
        {
            if (_root == nullptr)
            {
                _root = new Node(data);
                _root->_col = BLACK;             // 根必须黑
                return make_pair(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 make_pair(Iterator(cur, _root), false); // key 重复
            }
 
            cur = new Node(data);
            Node* newnode = cur;
            if (kot(parent->_data) < kot(data)) parent->_right = cur;
            else parent->_left = cur;
            cur->_parent = parent;
 
            while (parent && parent->_col == RED)      // 父子双红,需要调整
            {
                Node* grand = parent->_parent;
 
                if (parent == grand->_left)            // 父在祖父左边
                {
                    Node* uncle = grand->_right;
                    if (uncle && uncle->_col == RED)   // 情况一:叔红 -> 变色上提
                    {
                        parent->_col = BLACK;
                        uncle->_col = BLACK;
                        grand->_col = RED;
                        cur = grand;
                        parent = cur->_parent;
                    }
                    else                               // 情况二/三:叔黑或空 -> 旋转
                    {
                        if (cur == parent->_left)      // 左左:右单旋
                        {
                            RotateR(grand);
                            parent->_col = BLACK;
                            grand->_col = RED;
                        }
                        else                           // 左右:左旋+右旋
                        {
                            RotateL(parent);
                            RotateR(grand);
                            cur->_col = BLACK;
                            grand->_col = RED;
                        }
                        break;
                    }
                }
                else                                   // 父在祖父右边(镜像)
                {
                    Node* uncle = grand->_left;
                    if (uncle && uncle->_col == RED)
                    {
                        parent->_col = BLACK;
                        uncle->_col = BLACK;
                        grand->_col = RED;
                        cur = grand;
                        parent = cur->_parent;
                    }
                    else
                    {
                        if (cur == parent->_right)     // 右右:左单旋
                        {
                            RotateL(grand);
                            parent->_col = BLACK;
                            grand->_col = RED;
                        }
                        else                           // 右左:右旋+左旋
                        {
                            RotateR(parent);
                            RotateL(grand);
                            cur->_col = BLACK;
                            grand->_col = RED;
                        }
                        break;
                    }
                }
            }
            _root->_col = BLACK;                       // 最终根染黑
            return make_pair(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 (key < kot(cur->_data))   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* parentParent = parent->_parent;
            subL->_right = parent;
            parent->_parent = subL;
 
            if (parentParent == nullptr)
            {
                _root = subL;
                subL->_parent = nullptr;
            }
            else
            {
                if (parent == parentParent->_left) parentParent->_left = subL;
                else parentParent->_right = subL;
                subL->_parent = parentParent;
            }
        }
 
        void Destroy(Node* root)                    // 后序释放整棵树
        {
            if (root == nullptr) return;
            Destroy(root->_left);
            Destroy(root->_right);
            delete root;
        }
 
        Node* _root = nullptr;
    };
 
    // ---------- set ----------
    template<class K>
    class set
    {
        struct SetKeyOfT
        {
            const K& operator()(const K& key) const { 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;   // T 用 const K,set 元素只读
    };
 
    // ---------- map ----------
    template<class K, class V>
    class map
    {
        struct MapKeyOfT
        {
            const K& operator()(const pair<K, V>& kv) const { 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(make_pair(key, V()));
            return ret.first->second;
        }
 
    private:
        RBTree<K, pair<const K, V>, MapKeyOfT> _t;  // first 用 const K,key 不可改
    };
 
    // ---------- 测试 ----------
    void test_set()
    {
        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;
    }
 
    void test_map()
    {
        map<string, string> dict;
        dict.insert({ "sort", "排序" });
        dict.insert({ "left", "左边" });
        dict.insert({ "right", "右边" });
 
        dict["left"] = "左边,剩余";   // 改已有
        dict["insert"] = "插入";       // 新增
        dict["string"];                // 只插入空 value
 
        map<string, string>::iterator it = dict.begin();
        while (it != dict.end())
        {
            it->second += 'x';         // 改 value 允许
            cout << it->first << ":" << it->second << endl;
            ++it;
        }
        cout << endl;
    }
}
 
int main()
{
    bit::test_set();
    bit::test_map();
    return 0;
}

这份完整程序里,~RBTree() { Destroy(_root); } 是课件没明说、但我必须补上的一环:Insert 里每个 new Node 出来的结点,都要在树销毁时用后序遍历释放掉,否则就是内存泄漏。Destroy 用"后序"(先左、再右、最后 delete 自己)是因为必须先把孩子的结点释放完,才能安全地释放当前结点——先删父而孩子还活着,孩子的 _parent 就成了悬垂指针。看到这里,你应该能体会到文章里每一节的动作组合起来,就是一份完整、可运行、不泄漏的迷你 STL。

写到这里,你可以回头想想:标准库那棵赫赫有名的红黑树,本质上就是"一个把比较策略完全交给仿函数的通用容器",map 和 set 只是它外面的两件定制外套。KeyOfT 解决"比哪个字段",Compare 解决"按什么规则比",K/T 分工解决"插入什么、拿什么查",const 的位置解决"谁能改"——这些机制环环相扣,共同搭出了 map 和 set 的整个体验。

你可能会问,multimap、multiset 怎么来的?思路也一样——只要把 Insert 里"key 重复就返回 false"那段改成"重复也允许插入",就得到了允许 key 重复的版本。mymap/myset 再往下还能加 erase(红黑树的删除比插入更费劲,涉及合并结点、多种情况换色旋转)、count、upper_bound/lower_bound、at()(下标找不到抛异常,而不是插入)等接口。所有知识都在这一棵树的变体里,你消化透今天这一课,再去看四五个相关容器都不费力。

代码这种能力,光看是看不会的。建议你现在就把上面那份全程序存成文件编译跑通,然后再动手做几个小改造检验理解:比如给 map 加一个 at()(找不到抛异常,而不是像 operator[] 那样插入);把 Insert 改成支持重复 key 的 InsertMulti(对照实现一个 multimap);或写一个自定义结构塞进 myset 并只给它重载 <。当你亲手跑通、亲手改坏又修好,红黑树从"吓人的数据结构"变成"顺手的朋友"那一刻,这门课的价值才真正兑现。

参考答案与详解

小改造 ①:给 map 加一个 at()

at() 的语义是"只读地返回 value 引用,查不到就抛异常",和 operator[] 相反。实现要点是用 find 而不是 insert,这样绝不会像 [] 那样自动插入:

// 非 const 版本:查不到抛 std::out_of_range
V& at(const K& key)
{
    iterator it = find(key);
    if (it == end())
        throw std::out_of_range("map::at: key not found");
    return it->second;
}
 
// const 版本:调用 const 版本的 find,返回只读引用
const V& at(const K& key) const
{
    const_iterator it = find(key);
    if (it == end())
        throw std::out_of_range("map::at: key not found");
    return it->second;
}

注意:const 版本的 find、begin/end 需要返回 ConstIterator,所以给 map(或 RBTree)补一个 const 的 find 重载才完整。at 和 [] 的分工由此一目了然:[] = 可能插入(适合写),at = 只读兜底(适合读且接受抛异常)。

小改造 ②:把 Insert 改成支持重复 key 的 InsertMulti(multimap/multiset)

关键就一处:把 Insert 里"key 已存在就 return false"那个分支删掉,改成相等时也按统一约定继续往下走(比如 <= 往右),让新结点照常落到叶子空位挂上去。红黑树的颜色性质不考究 key 是否重复,只要新结点默认红、必要时照常做叔叔变色/旋转调整即可。改动后的查找循环示意:

pair<Iterator, bool> InsertMulti(const T& data)
{
    KeyOfT kot;
    if (_root == nullptr) { _root = new Node(data); _root->_col = BLACK; return make_pair(Iterator(_root, _root), true); }
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
        parent = cur;
        if (kot(cur->_data) <= kot(data))   // 相等也往右:保持一致,允许重复
            cur = cur->_right;
        else
            cur = cur->_left;
    }
    cur = new Node(data);
    Node* newnode = cur;
    if (kot(parent->_data) <= kot(data)) parent->_right = cur;
    else parent->_left = cur;
    cur->_parent = parent;
    // ……从这里开始的红黑修复(while (parent && parent->_col == RED) { … })与原来完全一致
    _root->_col = BLACK;
    return make_pair(Iterator(newnode, _root), true);  // 此时 second 恒为 true
}

由此再包一层 multimap/multiset 即可。要留神两点:multimap 没有 operator[](key 不唯一,[] 不知道该返回哪个 value),所以即使支持了重复插入也不要去实现它;其次 find 此时返回的是"中序第一个匹配",想拿全区间还得靠 lower_bound/upper_bound/equal_range(和 STL 一致)。

小改造 ③:自定义结构塞进 myset,只给类型重载 <

因为我们的树内部比较只用 <(Find 也都写成 key < kot(cur->_data) 这种只用 < 的形式),所以只需给自定义类型提供一个"严格弱序"的 operator< 即可,完全不需要 == 或 >:

struct Student
{
    int    _id;
    string _name;
    Student(int id, const string& name) : _id(id), _name(name) {}
};
bool operator<(const Student& a, const Student& b) { return a._id < b._id; }
 
bit::set<Student> s;
s.insert(Student(3, "小红"));

set 底层 _t 在比较两个 Student 时用的是 SetKeyOfT(返回原对象)之后只调 <,所以编译能过、排序按学号进行。若某个自定义类型硬塞进来却报"找不到匹配的 operator<",就说明它没提供严格弱序的比较——这正是课件强调"排序容器 = 编译器 + 你约定一个规则"的由来。