【C++】从红黑树到 map 和 set:封装设计与迭代器实现

引言

C++ 标准库中的 std::mapstd::set 底层均基于红黑树 实现,但两者存储的数据类型不同:set 仅存储关键字 key,而 map 存储键值对 pair<const Key, T>。在 SGI-STL 源码中,mapset 并没有各自独立实现一棵红黑树,而是通过复用同一棵红黑树模板,利用模板参数和仿函数来区分两种不同的语义。这种设计体现了泛型编程的精妙:用一份红黑树代码同时支持 key 模型和 key/value 模型。

本文将以 SGI-STL 源码框架为参考,从零开始模拟实现 bit::mapbit::set。我们将重点分析:

  • 红黑树如何通过模板参数 T 统一存储不同类型的数据;

  • 如何利用 KeyOfT 仿函数从 T 中提取比较所需的 key

  • 迭代器的实现,特别是 operator++operator-- 的中序逻辑;

  • mapoperator[] 如何基于 insert 返回值实现;

  • 如何控制 set 的迭代器不支持修改,以及 map 的迭代器不允许修改 key 但允许修改 value


目录

引言

[一、源码框架分析:SGI-STL 的设计思路](#一、源码框架分析:SGI-STL 的设计思路)

二、模拟实现的基本框架

[2.1 红黑树节点](#2.1 红黑树节点)

[2.2 红黑树模板声明](#2.2 红黑树模板声明)

[2.3 set 的封装](#2.3 set 的封装)

[2.4 map 的封装](#2.4 map 的封装)

三、迭代器的实现

[3.1 迭代器的结构](#3.1 迭代器的结构)

[3.2 operator++ 的逻辑](#3.2 operator++ 的逻辑)

[3.3 operator-- 的逻辑](#3.3 operator-- 的逻辑)

[3.4 begin() 与 end()](#3.4 begin() 与 end())

[3.5 set 和 map 中的迭代器类型](#3.5 set 和 map 中的迭代器类型)

[四、map 的 operator[] 实现](#四、map 的 operator[] 实现)

五、完整的封装示例

六、测试与验证

[6.1 测试 set](#6.1 测试 set)

[6.2 测试 map 与 operator[]](#6.2 测试 map 与 operator[])

总结


一、源码框架分析:SGI-STL 的设计思路

SGI-STL 中,setmap 并没有直接包含红黑树的具体实现,而是通过一个名为 rb_tree 的模板类来完成所有底层操作。rb_tree 的声明如下(简化):

cpp

复制代码
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc>
class rb_tree {
    // ...
};
  • Key :关键字类型(用于 finderase 等接口的参数)。

  • Value:红黑树节点中实际存储的数据类型。

  • KeyOfValue :一个仿函数,用于从 Value 中提取 Key

对于 setValue 就是 KeyKeyOfValue 通常是一个恒等仿函数(identity)。对于 mapValuepair<const Key, T>KeyOfValue 是一个提取 pair::first 的仿函数(select1st)。

rb_tree 提供了 insert_unique(不允许重复 key)和 insert_equal(允许重复 key)等接口。setmap 只使用 insert_unique,而 multisetmultimap 则使用 insert_equal

这种设计使得红黑树完全独立于具体的应用场景,只要外界提供合适的 KeyOfValue 仿函数即可。


二、模拟实现的基本框架

我们将按照以下层次构建:

  1. 红黑树节点 :模板参数 T 表示节点中存储的数据类型。

  2. 红黑树类 :模板参数 K(关键字类型)、T(存储的数据类型)、KeyOfT(提取关键字的仿函数)。

  3. set :封装红黑树,T = KKeyOfT 返回 K 本身。

  4. map :封装红黑树,T = pair<const K, V>KeyOfT 返回 pair.first

2.1 红黑树节点

节点中除了左右孩子和父节点指针外,还包含颜色和存储的数据 _data

cpp

复制代码
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)
    {}
};

2.2 红黑树模板声明

红黑树需要知道如何从 T 中取出 key 进行比较,因此引入 KeyOfT 仿函数:

cpp

复制代码
template<class K, class T, class KeyOfT>
class RBTree {
    typedef RBTreeNode<T> Node;
public:
    // 插入等接口
private:
    Node* _root = nullptr;
};

K 是关键字类型,主要用于 finderase 等接口的参数类型;T 是节点存储的数据类型;KeyOfT 是一个仿函数类,必须提供 operator()T 转换为 K

2.3 set 的封装

set 存储的是 K,因此 T = KKeyOfT 只需要原样返回传入的 key

cpp

复制代码
template<class K>
class set {
    struct SetKeyOfT {
        const K& operator()(const K& key) const { return key; }
    };
public:
    bool insert(const K& key) {
        return _t.Insert(key).second;
    }
private:
    RBTree<K, const K, SetKeyOfT> _t;
};

注意:set 不允许修改关键字,因此红黑树的 T 类型使用 const K,这样迭代器解引用后得到 const K&,无法修改。

2.4 map 的封装

map 存储的是 pair<const K, V>,因此 T = pair<const K, V>KeyOfT 提取 pair.first

cpp

复制代码
template<class K, class V>
class map {
    struct MapKeyOfT {
        const K& operator()(const pair<const K, V>& kv) const {
            return kv.first;
        }
    };
public:
    bool insert(const pair<const K, V>& kv) {
        return _t.Insert(kv).second;
    }
private:
    RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};

此时红黑树中存储的数据类型是 pair<const K, V>key 不可修改,value 可以修改。


三、迭代器的实现

迭代器是 STL 的核心组件,它封装了红黑树的节点指针,并提供 operator++operator--operator*operator-> 等操作,使得用户可以像使用指针一样遍历容器。

3.1 迭代器的结构

迭代器需要知道当前节点指针,以及树的根节点(用于处理 end() 的特殊情况)。我们实现一个模板类 RBTreeIterator,支持 RefPtr 来区分普通迭代器和 const 迭代器。

cpp

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

    Node* _node;
    Node* _root;   // 用于 --end() 时找到最右节点

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

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

    Self& operator++();
    Self& operator--();

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

3.2 operator++ 的逻辑

红黑树迭代器的 ++ 操作需要移动到中序遍历的下一个节点。中序遍历顺序:左子树 → 根 → 右子树。

情况1 :当前节点的右子树不为空

下一个节点是右子树中的最左节点(即右子树中中序第一个节点)。

情况2 :当前节点的右子树为空

说明当前节点所在的子树已经遍历完毕,需要向上回溯,找到第一个当前节点是其父节点的左孩子 的祖先节点,该祖先节点就是下一个要访问的节点。如果一直回溯到根都没有找到(即当前节点是整棵树的最右节点),则下一个节点为 nullptr,即 end()

cpp

复制代码
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 = parent->_parent;
        }
        _node = parent;
    }
    return *this;
}

3.3 operator-- 的逻辑

-- 操作是中序的逆过程,顺序为:右子树 → 根 → 左子树。

  • 如果当前节点为 nullptr(即 end()),则 -- 应指向中序遍历的最后一个节点,即整棵树的最右节点。

  • 如果当前节点的左子树不为空,则前驱节点是左子树中的最右节点

  • 否则,向上回溯,找到第一个当前节点是其父节点的右孩子的祖先节点。

cpp

复制代码
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 = parent->_parent;
        }
        _node = parent;
    }
    return *this;
}

3.4 begin()end()

  • begin() 返回中序第一个节点,即树中最左节点。

  • end() 返回 nullptr 迭代器(也可以像 SGI-STL 那样设计一个哨兵头节点,但 nullptr 更简单)。

cpp

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

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

3.5 setmap 中的迭代器类型

  • set 的迭代器:RBTree<K, const K, SetKeyOfT>::Iterator。由于 Tconst K,解引用得到 const K&,不能修改。

  • map 的迭代器:RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator。解引用得到 pair<const K, V>&first 不可修改,second 可修改。

setmap 中分别定义 iteratorconst_iterator 类型,并实现 begin()/end() 的 const 版本。


四、mapoperator[] 实现

mapoperator[] 是一个非常便利的接口,它支持:

  • 插入:若 key 不存在,则插入 pair(key, V())

  • 查找:若 key 存在,则返回对应 value 的引用。

  • 修改:通过返回的引用可以直接修改 value

要支持 [],首先需要修改红黑树的 Insert 接口,使其返回 pair<Iterator, bool>,其中 Iterator 指向插入成功或已存在元素的迭代器。

cpp

复制代码
pair<Iterator, bool> Insert(const T& data) {
    // ... 插入逻辑,成功或失败时返回对应的迭代器和 bool
}

然后在 map 中实现 operator[]

cpp

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

如果 key 不存在,insert 会插入 {key, V()}V() 是值类型的默认构造(例如 int 为 0,string 为空串),然后返回迭代器指向该新节点,并返回 second = true。如果 key 已存在,insert 返回的迭代器指向已有节点,second = false。无论哪种情况,ret.first->second 都是 value 的引用,可以用于读取或赋值。


五、完整的封装示例

以下给出 RBTreeInsert 返回 pair<Iterator, bool> 的关键修改(插入逻辑与之前红黑树相同,只是返回值类型变化):

cpp

复制代码
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);  // 已存在
        }
    }

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

    // ... 后续的平衡处理(变色和旋转)与之前相同 ...

    _root->_col = BLACK;
    return make_pair(Iterator(newnode, _root), true);
}

setmapinsert 只需调用 _t.Insert 并返回对应的 pair


六、测试与验证

6.1 测试 set

cpp

复制代码
void test_set() {
    bit::set<int> s;
    int a[] = {4,2,6,1,3,5,15,7,16,14};
    for (auto e : a) s.insert(e);
    for (auto e : s) cout << e << " ";   // 升序输出
    cout << endl;
}

6.2 测试 mapoperator[]

cpp

复制代码
void test_map() {
    bit::map<string, string> dict;
    dict.insert({"sort", "排序"});
    dict.insert({"left", "左边"});
    dict["left"] = "左边,剩余";
    dict["insert"] = "插入";
    dict["string"];   // 插入 {"string", ""}
    for (auto& kv : dict) {
        cout << kv.first << ":" << kv.second << endl;
    }
}

总结

本文基于 SGI-STL 的源码设计思想,使用同一棵红黑树模板实现了 bit::setbit::map。关键点包括:

  1. 泛型数据存储 :红黑树节点存储模板参数 T,通过 KeyOfT 仿函数提取比较所需的 key,使得红黑树与具体的数据类型解耦。

  2. 迭代器实现 :红黑树迭代器按照中序遍历顺序移动,operator++operator-- 分别处理右子树非空和回溯祖先两种情况,begin() 返回最左节点,end() 返回 nullptr

  3. setmap 的差异化setT 实例化为 const K,禁止修改;mapT 实例化为 pair<const K, V>,允许修改 value 但不允许修改 key

  4. mapoperator[] :基于 insert 返回的迭代器,直接获取 value 的引用,集插入、查找、修改于一体。

通过这次模拟实现,我们不仅加深了对红黑树底层机制的理解,也掌握了泛型容器封装的高级技巧。这种复用同一底层结构、通过模板参数和仿函数定制行为的方式,是 C++ 泛型编程的经典范例,也是阅读 STL 源码的重要基础。

相关推荐
用户824451499701 小时前
一行代码让 sign()、round() 可微:sll-core 源码解读与边界梯度机制。
github
Hello eveybody1 小时前
介绍一下动态树LCT(Python)
开发语言·python·算法
handler011 小时前
速通蓝桥杯省一:二分算法
c语言·开发语言·c++·笔记·算法·职场和发展·蓝桥杯
橙子圆1231 小时前
Redis知识2
java·数据库·redis
lbb 小魔仙1 小时前
DolphinDB:以“存算一体“重新定义工业时序数据的边界
开发语言·人工智能·python·langchain·jenkins
callJJ1 小时前
Codex 联动 OpenSpec 提效方法论
java·开发语言·codex·openspec
过期动态1 小时前
【RabbitMQ基础篇】RabbitMQ从入门到实战
java·jvm·数据库·分布式·spring·rabbitmq·intellij-idea
MandalaO_O1 小时前
MySQL:数据库约束
数据库·mysql
上弦月-编程1 小时前
Java编程:跨平台开发利器
java·开发语言