跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

从红黑树到 map/set:一次完整的 STL 容器模拟实现

记录用 C++ 模板与仿函数,从红黑树逐步封装出 set 和 map 容器,涉及键提取、迭代器实现、常量正确性及 operator[] 等细节。

独立开发者发布于 2026/6/14更新于 2026/10/654 浏览
从红黑树到 map/set:一次完整的 STL 容器模拟实现

从红黑树到 map/set:一次完整的 STL 容器模拟实现

在继续之前,最好你已经知道红黑树的基本操作。这篇笔记记录的是:给定一个能跑的通的红黑树结构,如何把它封装成 std::set 和 std::map 那样的容器。

STL 里的那点模板把戏

看 set 的模板参数,红黑树部分传了两个 Key:

template<class Key, class Compare = less<Key>, class Alloc = alloc>
class set {
    typedef rb_tree<Key, Key, identity<Key>, key_compare, Alloc> rep_type;
    rep_type t;
};

多传一个重复的类型,为的是和 map 的模板参数保持一致,让底层红黑树能复用同一份代码。map 传的是 pair<const Key, T>:

template<class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
    typedef rb_tree<Key, pair<const Key, T>, select1st<value_type>, key_compare, Alloc> rep_type;
    rep_type t;
};

所以红黑树节点实际存的数据是泛化的 T,set 是 Key,map 是 pair。而第一个模板参数 Key 独立出来,方便 find 这类接口定义参数类型。

红黑树迭代器:按中序遍历走

为了支持容器遍历,我们需要给红黑树做一个迭代器。它本质上就是对节点指针的包装。

节点定义我改成这样,_data 存的是 T:

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

然后树的类模板改成 template<typename K, typename T> class RBTree,节点用 RBTreeNode<T>。

迭代器需要实现 * 返回数据引用,-> 返回数据地址,以及递增和递减按照中序遍历逻辑移动。

template<typename T>
struct __RBTree_iterator {
    typedef RBTreeNode<T> Node;
    typedef __RBTree_iterator<T> Self;
    Node* _node;

    __RBTree_iterator(Node* node) : _node(node) {}

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

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

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

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

红黑树内部提供 begin() 和 end()。begin 是最左节点,end 直接是 nullptr,用一个空迭代器表示结尾。这里 end() 的迭代器 _node 是空,一般没什么问题,不过要注意不能对 end 解引用。

插入时怎么从 T 里取出 Key

节点里只存了 T,可是红黑树插入时要比较大小,就必须能从 T 提取出 Key。这里的解决方案是加一个模板仿函数 KeyOfT。对于 set,T 就是 Key,仿函数直接返回本身;对于 map,T 是 pair,仿函数返回 first。

树的模板变成 template<typename K, typename T, typename KeyOfT> class RBTree。插入逻辑的关键部分:

bool Insert(const T& data) {
    if (_root == nullptr) {
        _root = new Node(data);
        _root->_col = BLACK;
        return true;
    }
    // ... 按照 kot(data) 比较大小找插入位置
    while (parent && parent->_col == RED) {
        // 变色与旋转逻辑...
    }
    return true;
}
KeyOfT kot;

这样树里面调用 kot(data) 拿到 key,完全不用管 T 是啥。

封装 set 和 map

有了上面这些东西,封装就很简单了。我用一个命名空间 wzx 放自己的实现。

set 模板:

namespace wzx {
template<typename K>
class set {
private:
    struct SetKeyOfT {
        const K& operator()(const K& data) { return data; }
    };
public:
    typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;
    iterator begin() { return _t.begin(); }
    iterator end() { return _t.end(); }
    bool Insert(const K& key) { return _t.Insert(key); }
private:
    RBTree<K, K, SetKeyOfT> _t;
};
}

map 模板,注意 pair 里的 key 要加 const,避免外部不小心修改它:

namespace wzx {
template<typename K, typename V>
class map {
private:
    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;
    iterator begin() { return _t.begin(); }
    iterator end() { return _t.end(); }
    bool Insert(const pair<const K, V> kv) { return _t.Insert(kv); }
private:
    RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
}

set 的 key 不允许修改,map 的 key 也不能修改但 value 可以。为了实现这个约束,set 的迭代器应该要返回 const 引用。目前上面的代码返回的是普通引用,这会导致可以修改 set 的元素。标准库的做法是让 set 的 iterator 和 const_iterator 都是常量迭代器。后面会说怎么处理。

让 set 的 key 真正不可变:const 迭代器

为了让 set 的项不可修改,最简单的方式是把 set 里的 iterator 和 const_iterator 都弄成常量迭代器。也就是说,从红黑树那里拿到的迭代器,不管普通版还是 const 版,解引用都返回 const T&。

迭代器模板可以进一步改造,多引入两个模板参数 Ptr 和 Ref:

template<typename T, typename Ptr, typename Ref>
struct __RBTree_iterator {
    // ...
    Ref operator*() { return _node->_data; }
    Ptr operator->() { return &_node->_data; }
};

然后在红黑树里定义两种迭代器类型:

typedef __RBTree_iterator<T, T*, T&> iterator;
typedef __RBTree_iterator<T, const T*, const T&> const_iterator;

set 里只需要把公开的 iterator 名字直接映射成 const_iterator,禁止通过迭代器修改:

typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;
typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;

这样 set 的 begin 和 end 返回的都是 const_iterator,自然就只读。

map 因为 pair 里 key 已经加了 const,所以普通迭代器也能保证 key 不可变,但 value 可变,因此 map 的普通迭代器还是返回非 const 引用。

有一种偷懒的办法是不用改迭代器模板,直接在 set 里把 iterator 变成 const_iterator 的别名,但需要红黑树提供 const_iterator 成员函数。这里为了通用性,我还是改了迭代器模板,这样控制更灵活。

insert 返回值与 operator[]

标准 map::insert 的返回值是 pair<iterator, bool>,可以知道插入是否成功和位置。我们需要修改红黑树 Insert 方法,在插入新节点后返回一个包含迭代器和布尔值的 pair。同时,因为旋转调整可能会改变树结构,但要返回的迭代器始终指向新插入的节点,所以我们在一开始 new 出来节点后就保存起来。

pair<iterator, bool> Insert(const T& data) {
    if (_root == nullptr) {
        _root = new Node(data);
        _root->_col = BLACK;
        return make_pair(iterator(_root), true);
    }
    // ... 找到位置,如果已存在,返回 make_pair(iterator(exist_node), false)
    // 如果不存在, newnode = new Node(data); 然后执行旋转调整
    return make_pair(iterator(newnode), true);
}

有了这个之后,map 的 operator[] 就很短了:

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

注意如果 key 不存在,Insert 会插入一个默认构造的 value,然后返回引用;如果存在,就返回已有的引用。

跑一下测试

写几个简单的测试用例,覆盖插入、遍历,以及修改限制的编译检查。比如:

  • 遍历 set,尝试修改 *it,编译器应该报错。
  • 遍历 map,修改 it->second 可以,修改 it->first 报错。
  • map 的 operator[] 可以用做插入和更新。

代码不贴了,就是常规的遍历打印。至此,一个基本可用的、基于红黑树的 set 和 map 就算模拟完成了。还有很多细节没处理(比如拷贝构造、删除、迭代器失效),但核心的骨架已经能跑起来。

目录

  1. 从红黑树到 map/set:一次完整的 STL 容器模拟实现
  2. STL 里的那点模板把戏
  3. 红黑树迭代器:按中序遍历走
  4. 插入时怎么从 T 里取出 Key
  5. 封装 set 和 map
  6. 让 set 的 key 真正不可变:const 迭代器
  7. insert 返回值与 operator[]
  8. 跑一下测试

更多推荐文章

查看全部
  • 网络安全入门指南:成为安全工程师的十二个基础步骤
  • 无线蜂窝网络:原理、架构与代际演进
  • 基于 YOLO 的极端天气与道路交通目标检测数据集
  • OpenClaw 接入 QQ 开放平台:个人号一键部署 5 个 AI 机器人实战
  • 量化、算子融合与内存映射:C 语言实现边缘 AI 推理实战
  • Midjourney 第三方 API 服务技术原理与合规边界探讨
  • SeargeSDXL AI 绘画工作流使用指南
  • OpenClaw 开源智能 AI 助理:云端一键部署方案
  • 2026年3月18日人工智能早间新闻
  • 人形机器人与机器狗现场部署:单场与多机协同实战
  • .NET 中 Quartz.NET 任务调度核心概念与实战
  • Flutter 使用 web_socket 实现 OpenHarmony 跨平台 WebSocket 通信
  • 飞算 JavaAI 辅助 Java 项目开发流程与效率提升实践
  • C++ 继承机制详解:派生类函数、虚继承与菱形继承案例
  • JadeAI:开源 AI 简历生成器,支持 50 套模板与 Docker 部署
  • Linux 命名管道与共享内存基础
  • C++ 模板编程基础:泛型编程入门与实践
  • 大语言模型超参数调优指南
  • Linux 进程管理:创建、终止与回收全流程解析
  • 麦橘超然 WebUI 新手指南:Flux 离线图像生成部署与实战

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online