跳到主要内容C++ 从零开始封装 Map 与 Set:实现与优化 | 极客日志C++算法
C++ 从零开始封装 Map 与 Set:实现与优化
综述由AI生成深入讲解 C++ 中 Map 与 Set 容器的底层实现。基于红黑树(RBTree)理论,对比了 BST、AVL 树与红黑树的差异。重点阐述了迭代器机制的架构设计,包括 begin/end 定义及 ++/-- 操作符的中序遍历逻辑。通过模板参数设计与 KeyOfValue 仿函数,实现了 Map 与 Set 共享同一套红黑树底层代码,避免了冗余。最后展示了 Set 与 Map 的具体封装方法及测试用例,验证了键值存储的高效性。
修罗16 浏览 C++ 从零开始封装 Map 与 Set:实现与优化
1、理论基石——深度剖析 BSTree、AVLTree 与 RBTree 的概念区别
为了实现 Map 和 Set 的封装,我们首先进行了充分的知识储备,确保理解底层数据结构的原理与实现。
二叉搜索树及其变种
为了深入理解 Map 和 Set 的底层原理,我们从二叉搜索树 (BST) 入手。二叉搜索树在二叉树的基础上加入了以下重要特性:
- 若左子树不为空,则左子树上所有节点的值都小于根节点的值;
- 若右子树不为空,则右子树上所有节点的值都大于根节点的值;
- 左右子树也必须分别满足二叉搜索树的性质。
这种结构在大多数情况下可以有效支持快速查找,但在某些极端情况下(例如单侧树形结构),搜索效率会退化为 O(n)。因此,我们引入了更为高效的平衡二叉搜索树,其中 AVL 树和红黑树 (RBTree) 是两种经典的变种,它们通过不同的方式提高了性能与稳定性。
平衡二叉搜索树:AVL 树与红黑树
AVL 树和红黑树都是在保持二叉搜索树基本性质的基础上,通过旋转与重新平衡操作,确保树的高度维持在一个相对平衡的状态,从而保证了操作的时间复杂度始终为 O(log n)。
- AVL 树 增加了以下特性:
- 它的左右子树必须都是 AVL 树;
- 左右子树高度差(平衡因子)不能超过 1(即平衡因子为 -1、0 或 1);
- 如果平衡因子超出范围,树将进行旋转操作,包括右单旋、左单旋、左右双旋、右左双旋等。
- 红黑树 则在其基础上引入了以下特点:
- 每个节点必须是红色或黑色;
- 根节点必须是黑色;
- 红色节点的子节点必须是黑色;
- 从任何节点到其所有后代叶子节点的路径上,必须有相同数量的黑色节点;
- 叶子节点(空节点)必须是黑色。
虽然红黑树在旋转操作的频率上低于 AVL 树,但它通过较少的旋转保持树的接近平衡状态,能够确保良好的性能。
Map 与 Set 的底层实现
由于 Map 和 Set 大多用于高效检索,我们选择使用红黑树来实现它们的底层结构。红黑树能够在保证相对平衡的同时,确保操作时间复杂度为 O(log n),非常适合用于处理大量的键值对数据。
封装前的迭代器实现
在封装 Map 和 Set 之前,我们需要先实现一个关键的组件:迭代器。迭代器是遍历容器内元素的核心工具,它提供了访问和操作元素的方式,并为我们后续的封装工作奠定了基础。
2、迭代器机制——RBTree 迭代器的架构与工程实现
迭代器是容器中非常重要的组成部分,它使得我们可以方便地遍历数据。在红黑树的实现中,增加迭代器支持不仅仅是为了提供遍历的便利,更是为了使得红黑树能够符合现代 C++ 泛型编程的要求,遵循 STL 中对迭代器的严格规范。
首先,考虑到迭代器的设计,我们需要解决几个关键问题:
- 泛型编程与迭代器框架:迭代器的实现要支持泛型编程,因此需要考虑其接口设计,确保它能适应不同类型的容器结构。具体而言,我们需要实现一个适用于红黑树的迭代器模板,支持不同类型的键值对容器。
- begin() 与 end() 的设计:STL 明确规定了 begin() 与 end() 方法分别代表容器的起始位置和结束位置,且是前闭后开的区间。在红黑树的情况下,由于其节点经过中序遍历是有序的,begin() 应该指向树的最小节点(即最左侧节点),end() 则应指向一个空节点,即最大节点的下一个位置(为了简化设计,可以设为 nullptr)。
- operator++() 和 operator--() 的实现:在迭代器的操作符 ++ 和 -- 实现上,需要遵循红黑树的中序遍历顺序。与链表或数组不同,红黑树的节点并不是顺序排列的,因此简单的指针递增或递减操作无法满足需求。我们需要通过树的结构来确定'下一个'或'上一个'节点的位置,这通常通过旋转和父节点引用来实现。
迭代器框架的搭建
在确定了迭代器的需求后,接下来我们可以着手搭建迭代器框架。该框架需要支持以下功能:
- 构造与销毁:支持红黑树迭代器的构造与析构。
- 指针访问:提供对当前节点的访问能力,通常通过解引用操作符 * 和成员访问操作符 -> 来实现。
前进与后退:通过 ++ 和 -- 操作符来支持迭代器的前进与后退,确保其符合树的中序遍历规则。比较操作:支持 == 和 != 比较操作,判断两个迭代器是否指向同一节点。
template<class T, class Ptr, class Ref>
struct RBTreeIterator {
typedef RBTreeNode<T> Node;
typedef _RBTreeIterator<T, Ref, Ptr> Self;
Node* _node;
_RBTreeIterator(Node* node) :_node(node) {}
Ref operator*() { return _node->_data; }
Ptr operator&() { return &_node->_data; }
bool operator!= (const Self& s) { return _node != s._node; }
bool operator== (const Self& s) { return _node == s._node; }
};
迭代器的访问 ++ 和 --
++ 中序遍历的顺序是先遍历左边再遍历当前节点最后是右子树。
Self& operator++() {
if (_node->_right) {
Node* cur = _node->_right;
while (cur->_left) {
cur = cur->_left;
}
_node = cur;
} else {
Node* cur = _node;
Node* parent = _node->_parent;
while (parent && cur == parent->_right) {
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
return *this;
}
Self& operator--() {
if (_node == nullptr) {
Node* rightMost = _root;
while (rightMost && rightMost->_right) {
rightMost = rightMost->_right;
}
_node = rightMost;
} else if (_node->_left) {
Node* cur = _node->_left;
while (cur->_right) {
cur = cur->_right;
}
_node = cur;
} else {
Node* cur = _node;
Node* parent = _node->_parent;
while (parent && cur == parent->_left) {
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
return *this;
}
这里特殊情况是因为我们设置的容器迭代器 end() 为空指针(STL 库是加了个哨兵位节点)不可以再访问了所以特殊处理一下。
3、高级容器设计——Map 与 Set 的模块化封装
3.1 泛型支持增强——RBTree 模板参数设计优化
template<class K, class V>
struct RBTreeNode {
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
pair<K, V> _kv;
color _col;
RBTreeNode(pair<K, V> kv) :_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv) {}
};
在实现 Map 和 Set 的封装时,我们发现当前的设计使得每个红黑树的节点都存储了一个固定的数据类型 pair<K, V>。这种做法虽然对 Map 的实现来说非常直接,但当我们尝试为 Set 封装时,问题就显现出来了:Set 中的节点只需要存储键 K,而不是键值对 pair<K, V>。这意味着如果我们照这种方式实现 Set,我们就不得不重复编写一份类似的红黑树代码来处理不同的数据结构。
为了更好地封装 Map 和 Set,我们可以借鉴 STL 源码中的设计理念。STL 中通过模板编程和类型特化的方式,使得同一个数据结构能够支持不同类型的容器,而不需要重复编写代码。
STL 设计思路解析
首先,最底层的红黑树节点结构体只使用了一个模板参数 Value,该参数决定了节点中存储的数据类型。上层的红黑树根据提供的 Value 类型来选择具体的数据存储结构。
红黑树的关键模板参数
- Key:表示键的类型,主要用于查找操作(如 Find 函数)。
- Value:表示存储的数据类型。在 Map 中,Value 是
pair<K, V>,而在 Set 中,Value 则只有键 K。
- KeyOfValue:这是一个仿函数,用于从 Value 中提取 Key。
统一封装:如何实现 Map 与 Set 的共享底层红黑树
通过巧妙的模板设计,红黑树底层实现可以根据不同的需求使用不同的数据结构来存储:
- 对于 Map,底层红黑树将 Key 和
pair<K, V> 作为模板参数传入。
- 对于 Set,红黑树只需要 Key 作为模板参数。
enum color { Black, Red };
template<class T>
struct RBTreeNode {
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
T _data;
color _col;
RBTreeNode(T data) :_left(nullptr), _right(nullptr), _parent(nullptr), _data(data), _col(Red) {}
};
template<class T , class Ref , class Ptr>
struct _RBTreeIterator {
typedef RBTreeNode<T> Node;
typedef _RBTreeIterator<T, Ref, Ptr> Self;
Node* _node;
_RBTreeIterator(Node* node) :_node(node) {}
Ref operator*() { return _node->_data; }
Ptr operator&() { return &_node->_data; }
bool operator!= (const Self& s) { return _node != s._node; }
Self& operator++() { return *this; }
Self& operator--() { return *this; }
};
template<class K, class T , class KeyOfValue>
class RBTree {
public:
typedef _RBTreeIterator<T, T&, T*> Iterator;
typedef RBTreeNode<T> Node;
Iterator begin() { }
Iterator end() { }
pair<Iterator,bool> insert(const T& data) { }
Iterator find(const K& key) { }
private:
RBTreeNode<T>* _root = nullptr;
};
注意比较方式统一改成类似 kot(_data) < kot(node.data) 的样子哦!!!因为 map 与 set 的取出 key 的方式不同!!!**
3.2 Set 容器构建——面向集合操作的优化设计
对于 Set 封装,关键是要根据红黑树的底层实现要求提供相应的模板参数和仿函数。
- 模板参数传递:Set 需要满足 K 和 V 的模板参数传递。
- 底层红黑树的实例化:在 Set 类中,我们实例化一个底层红黑树,传入对应的模板参数 K、
const K,以及 SetOfT 仿函数。
- 迭代器的实现:在 Set 类中,我们还需要实现一个迭代器。
仿函数实现:我们需要实现一个仿函数,用于从存储的模板参数 V 中提取出 Key。
struct SetKeyOfT {
const K& operator()(const K& key) { return key; }
};
namespace xc {
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;
};
}
3.3 Map 容器构建——高效键值存储的封装实现
对于 Map 封装,关键是要根据红黑树的底层实现要求提供相应的模板参数和仿函数。
- 模板参数传递:Map 需要满足 K 和 V 的模板参数传递。
- 底层红黑树的实例化:在 Map 类中,我们实例化一个底层红黑树,传入对应的模板参数 K、
pair<const K, V>,以及 MapOfT 仿函数。
- 迭代器的实现:在 Map 类中,我们还需要实现一个迭代器。
仿函数实现:我们需要实现一个仿函数,用于从存储的 pair<const K, V> 中提取出 Key。
struct MapKeyOfT {
const K& operator()(const pair<K, V>& kv) { return kv.first; }
};
namespace xc {
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({ key, V() });
return ret.first->second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
}
void test_map() {
xc::map<string, string> dict;
dict.insert({ "sort", "排序" });
dict.insert({ "left", "左边" });
dict.insert({ "right", "右边" });
dict["left"] = "左边,剩余";
dict["insert"] = "插入";
dict["string"];
xc::map<string, string>::iterator it = dict.begin();
while (it != dict.end()) {
it->second += 'x';
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
for (auto &kv : dict) {
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
int main() {
test_map();
return 0;
}
可以看到我们实现了 map 的关键重载函数 []。
map 和 set 从零建立起来不仅需要二叉搜索树的知识还需要 AVL 树和红黑树的使用!!!甚至还需要对于模版的更深理解!!!
相关免费在线工具
- 加密/解密文本
使用加密算法(如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