如果让你用 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<",就说明它没提供严格弱序的比较——这正是课件强调"排序容器 = 编译器 + 你约定一个规则"的由来。
还没有评论 — 第一条由你来留。