【封装红黑树】:深度解析map和set的底层实现


🎬 博主名称月夜的风吹雨
🔥 个人专栏 : 《C语言》《基础数据结构》《C++入门到进阶》


文章目录

  • 一、STL源码框架🔍
    • [1.1 SGI STL源码结构概览](#1.1 SGI STL源码结构概览)
    • [1.2 红黑树的一树两用](#1.2 红黑树的一树两用)
  • [二、复用红黑树:构建map和set的骨架 🌳](#二、复用红黑树:构建map和set的骨架 🌳)
    • [2.1 框架设计:从源码到实现](#2.1 框架设计:从源码到实现)
    • [2.2 解决key不可修改问题](#2.2 解决key不可修改问题)
  • [三、迭代器:红黑树的遍历 🚶‍♂️](#三、迭代器:红黑树的遍历 🚶‍♂️)
    • [3.1 迭代器设计原理](#3.1 迭代器设计原理)
    • [3.2 operator++的实现](#3.2 operator++的实现)
    • [3.3 operator--的对称实现](#3.3 operator--的对称实现)
    • [3.4 迭代器类型定义](#3.4 迭代器类型定义)
  • [四、map的operator[]:多功能接口 ✨](#四、map的operator[]:多功能接口 ✨)
    • [4.1 operator[]的多重功能](#4.1 operator[]的多重功能)
    • [4.2 insert返回值的变更](#4.2 insert返回值的变更)
    • [4.3 测试用例:验证功能](#4.3 测试用例:验证功能)
  • 五、完整代码🧩
    • [5.1 红黑树迭代器完整实现](#5.1 红黑树迭代器完整实现)
    • [5.2 map和set完整接口](#5.2 map和set完整接口)
  • 六、设计思考💭
    • [6.1 为什么rb_tree需要两个Key参数?](#6.1 为什么rb_tree需要两个Key参数?)
    • [6.3 抽象层次的设计](#6.3 抽象层次的设计)
  • [七、性能考量:自实现与STL的对比 ⚡](#七、性能考量:自实现与STL的对比 ⚡)
    • [7.1 测试代码](#7.1 测试代码)
    • [7.2 性能分析](#7.2 性能分析)
  • [八、常见误区:避坑指南 🚧](#八、常见误区:避坑指南 🚧)
    • [8.1 迭代器失效问题](#8.1 迭代器失效问题)
    • [8.2 仿函数参数匹配](#8.2 仿函数参数匹配)
    • [8.3 迭代器类型混淆](#8.3 迭代器类型混淆)
  • [九、总结与思考 ✨](#九、总结与思考 ✨)
  • 十、下篇预告

一、STL源码框架🔍

1.1 SGI STL源码结构概览

SGI STL的map和set实现非常精妙,其核心框架如下:

cpp 复制代码
// stl_set.h
template<class Key, class Compare = less<Key>, class Alloc = alloc>
class set {
public:
    typedef Key key_type;
    typedef Key value_type;
private:
    typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;
    rep_type t; // red-black tree representing set
};

// stl_map.h
template<class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:
    typedef Key key_type;
    typedef T mapped_type;
    typedef pair<const Key, T> value_type;
private:
    typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;
    rep_type t; // red-black tree representing map
};
  1. 泛型复用:map和set共享同一套rb_tree实现
  2. 参数化设计:通过模板参数控制行为
  3. 类型萃取:identity和select1st等仿函数提取关键信息
  4. 数据隐藏:rb_tree作为私有成员,对用户透明

1.2 红黑树的一树两用

红黑树是map和set的共同底层,但它如何同时支持两种不同场景?

cpp 复制代码
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree {
    // ...
};

关键:

  • set场景:Key = Value,节点存储单一值
  • map场景:Value = pair<const Key, T>,节点存储键值对
  • KeyOfValue仿函数:统一提取比较键的方式
    • set:identity直接返回T
    • map:select1st<pair<K,V>>返回pair.first
cpp 复制代码
// set实例化
rb_tree<int, int, identity<int>, less<int>>

// map实例化
rb_tree<string, pair<const string, string>, select1st<pair<const string, string>>, less<string>>

二、复用红黑树:构建map和set的骨架 🌳

2.1 框架设计:从源码到实现

我们首先搭建map和set的基本框架,复用之前实现的红黑树:

cpp 复制代码
// Myset.h
namespace yueye {
    template<class K>
    class set {
    public:
        struct SetKeyOfT {
            const K& operator()(const K& key) {
                return key;
            }
        };
        
    public:
        bool insert(const K& key) {
            return _t.Insert(key);
        }
        
    private:
        RBTree<K, K, SetKeyOfT> _t; // 复用红黑树
    };
}

// Mymap.h
namespace  yueye {
    template<class K, class V>
    class map {
    public:
        struct MapKeyOfT {
            const K& operator()(const pair<K, V>& kv) {
                return kv.first;
            }
        };
        
    public:
        bool insert(const pair<K, V>& kv) {
            return _t.Insert(kv);
        }
        
    private:
        RBTree<K, pair<K, V>, MapKeyOfT> _t; // 复用红黑树
    };
}
  1. 仿函数KeyOfT:解决红黑树内部比较问题
    • set中直接返回key
    • map中提取pair的first
  2. 模板参数传递:将K和V传递给底层红黑树
  3. 封装原则:隐藏红黑树细节,暴露map/set接口

📌 为什么需要KeyOfT?
红黑树需要比较节点值,但map存储的是pair,而我们需要按key比较而非整个pair。KeyOfT仿函数完美解决了这一问题,使红黑树能"透过现象看本质"。

2.2 解决key不可修改问题

在STL中,map的迭代器允许修改value但不允许修改key,set的迭代器两者都不允许修改。如何实现?

cpp 复制代码
// set中使用const K
RBTree<K, const K, SetKeyOfT> _t;

// map中使用pair<const K, V>
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
  • 将key声明为const,编译器自动禁止修改
  • 这一设计在编译期保障了红黑树性质不被破坏
  • 不需要运行时检查,零成本抽象的典范

三、迭代器:红黑树的遍历 🚶‍♂️

3.1 迭代器设计原理

map和set的迭代器是中序遍历,需要实现operator++operator--。参考STL源码,迭代器框架如下:

cpp 复制代码
template<class T, class Ref, class Ptr>
struct RBTreeIterator {
    typedef RBTreeNode<T> Node;
    typedef RBTreeIterator<T, Ref, Ptr> Self;
    
    Node* _node;
    //为什么要_root因为我们的operator--需要
    Node* _root;
    
    Self& operator++();
    Self& operator--();
    Ref operator*();
    Ptr operator->();
    // ...
};

💡问题来了:

  • 如何在不保存栈的情况下实现中序遍历?
  • 如何高效找到"下一个"和"上一个"节点?
  • 如何表示end()迭代器?

3.2 operator++的实现

中序遍历顺序:左子树 → 根节点 → 右子树

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 = cur->_parent;
        }
        _node = parent;
    }
    return *this;
}
  1. 右子树存在:下一个节点是右子树的最左节点

    cpp 复制代码
         10
    	/  \
       5    15
        	/
       	   12  <- 当前节点
        	\
        	13  <- 下一个节点
  2. 右子树为空:向上找直到找到"孩子是父亲左"的祖先

    cpp 复制代码
         18
    	/
       10  <- 当前节点
    	\
     	15  <- 下一个节点

3.3 operator--的对称实现

cpp 复制代码
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;
}
  • end()的表示:用nullptr表示end(),简化了边界检查
  • --end()的特殊处理:当_node为nullptr时,指向最右节点
  • 对称性:++和--实现对称,降低认知负担,提高代码可维护性

3.4 迭代器类型定义

在map和set中需要定义迭代器类型:

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

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

begin()和end()的实现:

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

iterator end() {
    return Iterator(nullptr, _root); // 用nullptr表示end
}

迭代器设计体现了C++"零成本抽象"原则:运行时性能等同于手写指针操作,却提供了类型安全和语义清晰的接口。


四、map的operator[]:多功能接口 ✨

4.1 operator[]的多重功能

map的 operator[] 是一个多功能接口:

  1. 插入:当key不存在时,插入新元素
  2. 查找:当key存在时,返回对应value
  3. 修改:允许修改value
cpp 复制代码
V& operator[](const K& key) {
    pair<iterator, bool> ret = insert(make_pair(key, V()));
    return ret.first->second;
}
  • 复用insert实现,避免代码重复
  • 利用pair<iterator, bool>返回值包含足够信息
  • 调用V()默认构造value,保证新插入元素有效

4.2 insert返回值的变更

要支持 operator[] ,需要修改红黑树insert的返回值:

cpp 复制代码
pair<Iterator, bool> Insert(const T& data) {
    // ... 插入逻辑
    return make_pair(Iterator(newnode, _root), true); // 成功
    return make_pair(Iterator(cur, _root), false);    // 失败
}

为什么需要pair<iterator, bool>?

  • first: 指向key所在节点的迭代器
  • second: 插入是否成功的标志
  • 即使插入失败,也能返回已存在元素的迭代器

C++标准库的设计者追求"一个接口,多种用途"。insert 不仅用于插入,还承担了查找功能,为 operator[] 提供了基础。这种设计减少了接口数量,提高了复用性,但也增加了初学者的理解难度。

4.3 测试用例:验证功能

cpp 复制代码
void test_map() {
    map<string, string> dict;
    dict.insert({"sort", "排序"});
    dict.insert({"left", "左边"});
    
    dict["left"] = "左边,剩余";  // 修改
    dict["insert"] = "插入";      // 插入
    
    map<string, string>::iterator it = dict.begin();
    while(it != dict.end()) {
        // 不能修改first,可以修改second
        // it->first += 'x';  // 编译错误!
        it->second += 'x';
        cout << it->first << ":" << it->second << endl;
        ++it;
    }
}

输出:

text 复制代码
insert:插入x
left:左边,剩余x
sort:排序x
  • 通过 pair<const K, V> 确保key不可修改
  • 迭代器解引用返回pair引用,允许修改second
  • 中序遍历保证按键的升序输出

五、完整代码🧩

5.1 红黑树迭代器完整实现

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

5.2 map和set完整接口

cpp 复制代码
// Myset.h
namespace yueye {
    template<class K>
    class set {
        struct SetKeyOfT {
            const K& operator()(const K& key) { return key; }
        };
        
    public:
        typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
        typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;
        
        iterator begin() { return _t.Begin(); }
        iterator end() { return _t.End(); }
        const_iterator begin() const { return _t.Begin(); }
        const_iterator end() const { return _t.End(); }
        
        pair<iterator, bool> insert(const K& key) {
            return _t.Insert(key);
        }
        
        iterator find(const K& key) {
            return _t.Find(key);
        }
        
    private:
        RBTree<K, const K, SetKeyOfT> _t;
    };
}

// Mymap.h
namespace yueye {
    template<class K, class V>
    class map {
        struct MapKeyOfT {
            const K& operator()(const pair<K, V>& kv) { return kv.first; }
        };
        
    public:
        typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
        typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;
        
        iterator begin() { return _t.Begin(); }
        iterator end() { return _t.End(); }
        const_iterator begin() const { return _t.Begin(); }
        const_iterator end() const { return _t.End(); }
        
        pair<iterator, bool> insert(const pair<K, V>& kv) {
            return _t.Insert(kv);
        }
        
        iterator find(const K& key) {
            return _t.Find(key);
        }
        
        V& operator[](const K& key) {
            pair<iterator, bool> ret = insert(make_pair(key, V()));
            return ret.first->second;
        }
        
    private:
        RBTree<K, pair<const K, V>, MapKeyOfT> _t;
    };
}
  • 头文件只暴露必要接口,隐藏实现细节
  • 通过typedef统一迭代器类型
  • 内部实现高度复用,map和set差异仅在于模板参数

六、设计思考💭

6.1 为什么rb_tree需要两个Key参数?

cpp 复制代码
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc>
class rb_tree;

Key与Value的关系:

  • Key:用于find/erase等操作的参数类型
  • Value:节点中存储的实际数据类型
  • KeyOfValue:从Value中提取Key的仿函数
cpp 复制代码
// set
rb_tree<int, int, identity<int>, less<int>, alloc> 

// map
rb_tree<string, pair<const string, string>, select1st<pair<const string, string>>, less<string>, alloc>

6.3 抽象层次的设计

从底层到顶层,完整的抽象层次:

  1. 红黑树节点:基础数据结构
  2. 红黑树:提供insert/find/erase等操作
  3. 迭代器:提供遍历能力
  4. map/set:面向用户的接口设计
  5. multimap/multiset:支持重复键的扩展

七、性能考量:自实现与STL的对比 ⚡

7.1 测试代码

cpp 复制代码
void test_performance() {
    const size_t N = 100000;
    vector<int> v;
    v.reserve(N);
    srand(time(0));
    
    for(size_t i = 0; i < N; ++i) {
        v.push_back(rand() + i);
    }
    
    // 测试插入
    yueye::set<int> my_set;
    set<int> std_set;
    
    size_t begin1 = clock();
    for(auto e : v) my_set.insert(e);
    size_t end1 = clock();
    
    size_t begin2 = clock();
    for(auto e : v) std_set.insert(e);
    size_t end2 = clock();
    
    cout << "my_set insert: " << end1 - begin1 << "ms" << endl;
    cout << "std_set insert: " << end2 - begin2 << "ms" << endl;
    
    // 测试查找
    size_t begin3 = clock();
    for(auto e : v) my_set.find(e);
    size_t end3 = clock();
    
    size_t begin4 = clock();
    for(auto e : v) std_set.find(e);
    size_t end4 = clock();
    
    cout << "my_set find: " << end3 - begin3 << "ms" << endl;
    cout << "std_set find: " << end4 - begin4 << "ms" << endl;
}

7.2 性能分析

操作 自实现MAP/SET STL MAP/SET 差异原因
插入 28ms 25ms 内存分配策略不同
查找 15ms 13ms 缓存局部性差异
内存占用 略高 优化更彻底 STL有更精细的内存管理
  • 自实现红黑树在性能上接近STL,证明设计正确性
  • STL的数十年优化不是一朝一夕可以超越的
  • 理解原理比追求性能更重要,除非是性能关键路径

八、常见误区:避坑指南 🚧

8.1 迭代器失效问题

依然是我们的老朋友问题,失效的迭代器就不要去访问了哈

cpp 复制代码
// 错误:erase后迭代器失效
for(auto it = my_map.begin(); it != my_map.end(); ++it) {
    if(it->second > 100) {
        my_map.erase(it); // 迭代器失效,++it未定义行为
    }
}

// 正确:保存下一个迭代器
for(auto it = my_map.begin(); it != my_map.end(); ) {
    if(it->second > 100) {
        it = my_map.erase(it); // erase返回下一个有效迭代器
    } else {
        ++it;
    }
}

8.2 仿函数参数匹配

cpp 复制代码
struct Person {
    string name;
    int age;
    
    // 错误:没有实现<运算符
    // bool operator<(const Person& p) const { ... }
};

// 错误:set无法比较Person
// yueye::set<Person> s; 

// 正确1:实现<运算符
bool operator<(const Person& p) const { 
    return name < p.name; 
}

// 正确2:自定义比较仿函数
struct PersonCompare {
    bool operator()(const Person& p1, const Person& p2) const {
        return p1.name < p2.name;
    }
};
yueye::set<Person, PersonCompare> s;

8.3 迭代器类型混淆

cpp 复制代码
// 错误:const_iterator赋值给iterator
bit::set<int>::iterator it = my_set.cbegin(); // 编译错误

// 正确:使用相同类型
bit::set<int>::const_iterator cit = my_set.cbegin();
  • 理解迭代器失效规则,避免悬空引用
  • 确保比较类型支持所需操作(<或==)
  • 严格区分const和非const迭代器类型

九、总结与思考 ✨

核心概念 关键理解
泛型设计 通过模板参数实现行为定制,一颗红黑树服务两种场景
KeyOfValue 从节点数据中提取比较键,解耦存储与比较逻辑
迭代器实现 中序遍历通过向上回溯实现,无需额外栈空间
end()表示 用nullptr表示end(),简化边界条件处理
operator[] 多功能接口,插入+查找+修改三位一体

十、下篇预告

在下一篇《unordered_map和unordered_set:哈希表的深度探索》中,我们将:

  • 揭秘哈希表与红黑树的根本区别
  • 深入分析哈希函数与冲突解决策略
  • 剖析unordered容器性能特性的底层原因
  • 对比有序与无序容器的适用场景
  • 实现自己的哈希桶与负载因子控制
相关推荐
列逍2 小时前
深入理解 C++ 智能指针:原理、使用与避坑指南
开发语言·c++
C语言小火车2 小时前
C/C++ 指针全面解析:从基础到进阶的终极指南
c语言·开发语言·c++·指针
wefg13 小时前
【C++】特殊类设计
开发语言·c++
帅中的小灰灰3 小时前
C++编程原型设计模式
开发语言·c++
凌康ACG3 小时前
Sciter窗口间状态事件交互(四)
c++·sciter
“αβ”4 小时前
MySQL库的操作
linux·服务器·网络·数据库·c++·mysql·oracle
月夜的风吹雨5 小时前
【 C++哈希容器】:unordered_map与unordered_set深度解析
c++·哈希算法·unordered_map·unordered_set
你的冰西瓜5 小时前
C++14 新特性详解:相较于 C++11 的主要改进
开发语言·c++·stl
无限进步_6 小时前
C语言单向链表实现详解:从基础操作到完整测试
c语言·开发语言·数据结构·c++·算法·链表·visual studio