1. 为什么需要自己实现红黑树容器
在C++标准库中,map和set是两种非常重要的关联容器,它们底层通常采用红黑树实现。但很多开发者可能会有疑问:既然标准库已经提供了这些容器,为什么我们还要自己动手实现呢?
首先,理解红黑树的实现原理对于深入掌握数据结构至关重要。红黑树作为一种自平衡二叉查找树,它能够在最坏情况下保证O(log n)的时间复杂度,这对于理解高效数据结构的运作机制非常有帮助。通过自己实现,我们可以更深入地理解:
- 红黑树的五大性质及其维护方式
- 插入和删除节点时的平衡调整策略
- 迭代器的实现原理
- 模板编程在容器设计中的应用
其次,标准库的实现虽然高效但不够透明。当我们遇到性能问题时,如果对底层实现不了解,很难进行针对性的优化。自己实现一遍后,你会对以下问题有更清晰的认识:
- 为什么map的插入操作有时会比unordered_map慢?
- 迭代器失效的具体条件是什么?
- 如何根据特定场景调整平衡策略?
提示:在实际项目中,除非有特殊需求,否则建议优先使用标准库实现。自己实现的主要目的是学习而非替代。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 红黑树基础结构设计
2.1 节点结构定义
红黑树的节点需要存储以下信息:
cpp复制enum Color { RED, BLACK };
template <typename T>
struct RBTreeNode {
T data;
Color color;
RBTreeNode* left;
RBTreeNode* right;
RBTreeNode* parent;
// 构造函数
RBTreeNode(const T& val, Color c = RED)
: data(val), color(c), left(nullptr), right(nullptr), parent(nullptr) {}
};
关键设计考虑:
- 使用枚举类型表示节点颜色,比bool更直观
- 包含父指针是为了方便回溯和旋转操作
- 新节点默认红色,这符合红黑树插入策略
2.2 树类框架
我们的红黑树类需要提供基本的CRUD接口:
cpp复制template <typename K, typename V, typename Compare = std::less<K>>
class RBTree {
public:
using Node = RBTreeNode<std::pair<K, V>>;
// 构造/析构
RBTree();
~RBTree();
// 基本操作
bool insert(const std::pair<K, V>& kv);
bool erase(const K& key);
Node* find(const K& key) const;
// 迭代器相关
class iterator;
iterator begin();
iterator end();
private:
Node* root;
Compare comp;
size_t size;
// 内部辅助函数
void leftRotate(Node* x);
void rightRotate(Node* y);
void fixAfterInsert(Node* z);
void fixAfterDelete(Node* x);
// ...其他辅助函数
};
3. 核心操作实现细节
3.1 插入操作与平衡调整
红黑树的插入分为两个阶段:
- 常规二叉搜索树插入
- 平衡调整(fixAfterInsert)
插入后的平衡调整需要考虑多种情况:
| 情况 | 描述 | 处理方式 |
|---|---|---|
| 1 | 插入的是根节点 | 直接染黑 |
| 2 | 父节点是黑色 | 无需处理 |
| 3 | 父节点和叔节点都是红色 | 父叔染黑,祖父染红,递归处理祖父 |
| 4 | 父红叔黑,且当前节点与父节点方向不一致 | 通过旋转转换为情况5 |
| 5 | 父红叔黑,且当前节点与父节点方向一致 | 旋转祖父并重新着色 |
实现代码示例:
cpp复制void fixAfterInsert(Node* z) {
while (z->parent && z->parent->color == RED) {
if (z->parent == z->parent->parent->left) {
Node* uncle = z->parent->parent->right;
if (uncle && uncle->color == RED) { // 情况3
z->parent->color = BLACK;
uncle->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent;
} else {
if (z == z->parent->right) { // 情况4
z = z->parent;
leftRotate(z);
}
// 情况5
z->parent->color = BLACK;
z->parent->parent->color = RED;
rightRotate(z->parent->parent);
}
} else {
// 对称情况...
}
}
root->color = BLACK; // 确保根节点为黑
}
3.2 删除操作与平衡调整
删除操作更为复杂,主要步骤:
- 找到要删除的节点
- 确定实际删除的节点(可能用后继节点替代)
- 执行删除
- 平衡调整(fixAfterDelete)
删除后的平衡调整需要考虑兄弟节点的颜色及其子节点的颜色:
| 情况 | 描述 | 处理方式 |
|---|---|---|
| 1 | 当前节点是根或红色 | 直接染黑 |
| 2 | 兄弟节点是红色 | 旋转父节点并重新着色 |
| 3 | 兄弟节点黑色且两个侄子黑色 | 兄弟染红,问题上移 |
| 4 | 父节点红色,兄弟和侄子黑色 | 交换父兄颜色 |
| 5 | 兄弟节点黑色且远侄子黑近侄子红 | 旋转兄弟并重新着色 |
| 6 | 兄弟节点黑色且远侄子红 | 旋转父节点并重新着色 |
关键实现片段:
cpp复制void fixAfterDelete(Node* x) {
while (x != root && (!x || x->color == BLACK)) {
if (x == x->parent->left) {
Node* sibling = x->parent->right;
if (sibling->color == RED) { // 情况2
sibling->color = BLACK;
x->parent->color = RED;
leftRotate(x->parent);
sibling = x->parent->right;
}
if ((!sibling->left || sibling->left->color == BLACK) &&
(!sibling->right || sibling->right->color == BLACK)) { // 情况3
sibling->color = RED;
x = x->parent;
} else {
if (!sibling->right || sibling->right->color == BLACK) { // 情况5
sibling->left->color = BLACK;
sibling->color = RED;
rightRotate(sibling);
sibling = x->parent->right;
}
// 情况6
sibling->color = x->parent->color;
x->parent->color = BLACK;
sibling->right->color = BLACK;
leftRotate(x->parent);
x = root; // 终止循环
}
} else {
// 对称情况...
}
}
if (x) x->color = BLACK;
}
4. 实现mymap和myset容器
4.1 mymap的实现
基于红黑树的map实现需要考虑:
- 键值对存储
- 比较函数
- 迭代器设计
类定义示例:
cpp复制template <typename Key, typename T, typename Compare = std::less<Key>>
class mymap {
private:
RBTree<Key, T, Compare> tree;
public:
using iterator = typename RBTree<Key, T, Compare>::iterator;
// 容量
bool empty() const { return tree.size() == 0; }
size_t size() const { return tree.size(); }
// 元素访问
T& operator[](const Key& key) {
auto it = tree.find(key);
if (it == end()) {
it = tree.insert({key, T()}).first;
}
return it->second;
}
// 修改操作
std::pair<iterator, bool> insert(const std::pair<Key, T>& kv) {
return tree.insert(kv);
}
size_t erase(const Key& key) {
return tree.erase(key);
}
// 查找
iterator find(const Key& key) {
return tree.find(key);
}
// 迭代器
iterator begin() { return tree.begin(); }
iterator end() { return tree.end(); }
};
4.2 myset的实现
set的实现相对简单,因为只需要存储键:
cpp复制template <typename Key, typename Compare = std::less<Key>>
class myset {
private:
RBTree<Key, bool, Compare> tree; // 值类型用bool占位
public:
using iterator = typename RBTree<Key, bool, Compare>::iterator;
// 容量
bool empty() const { return tree.size() == 0; }
size_t size() const { return tree.size(); }
// 修改操作
std::pair<iterator, bool> insert(const Key& key) {
return tree.insert({key, true});
}
size_t erase(const Key& key) {
return tree.erase(key);
}
// 查找
iterator find(const Key& key) {
return tree.find(key);
}
// 迭代器
iterator begin() { return tree.begin(); }
iterator end() { return tree.end(); }
};
5. 迭代器设计与实现
5.1 迭代器基本结构
红黑树迭代器需要支持中序遍历,核心是实现++和--操作:
cpp复制template <typename Node>
class TreeIterator {
Node* current;
public:
using iterator_category = std::bidirectional_iterator_tag;
using value_type = typename Node::value_type;
using difference_type = std::ptrdiff_t;
using pointer = value_type*;
using reference = value_type&;
TreeIterator(Node* p = nullptr) : current(p) {}
reference operator*() const { return current->data; }
pointer operator->() const { return &(current->data); }
// 前缀++
TreeIterator& operator++() {
if (current->right) {
current = current->right;
while (current->left) {
current = current->left;
}
} else {
Node* p = current->parent;
while (p && current == p->right) {
current = p;
p = p->parent;
}
current = p;
}
return *this;
}
// 后缀++
TreeIterator operator++(int) {
TreeIterator tmp = *this;
++(*this);
return tmp;
}
// 比较运算符
bool operator==(const TreeIterator& rhs) const { return current == rhs.current; }
bool operator!=(const TreeIterator& rhs) const { return current != rhs.current; }
};
5.2 边界情况处理
迭代器实现中容易出错的点:
- end()迭代器应该指向最后一个元素的下一个位置(通常用空指针表示)
- 对空树或end()迭代器执行--操作需要特殊处理
- 遍历到根节点时需要判断是否继续向上
注意:在红黑树中实现反向迭代器时,可以考虑使用std::reverse_iterator适配器,这样可以复用正向迭代器的代码。
6. 性能测试与优化
6.1 基本性能测试
我们可以通过插入和查找操作来测试实现的性能:
cpp复制void testPerformance() {
mymap<int, int> map;
std::map<int, int> stdMap;
// 插入测试
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
map.insert({i, i});
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "mymap insert: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms\n";
// 对比标准库
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
stdMap.insert({i, i});
}
end = std::chrono::high_resolution_clock::now();
std::cout << "std::map insert: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms\n";
}
6.2 常见优化策略
- 内存池优化:频繁的节点分配和释放会影响性能,可以使用内存池预分配节点
- 平衡策略调整:在某些场景下可以放宽平衡条件,减少旋转次数
- 缓存友好布局:将颜色位与父指针合并存储,减少内存占用
- 迭代器局部性优化:记录遍历路径,加速后续操作
优化后的节点结构示例:
cpp复制struct RBTreeNode {
uintptr_t color_parent; // 最低位存储颜色
T data;
RBTreeNode* left;
RBTreeNode* right;
// 通过位操作访问父指针和颜色
RBTreeNode* parent() const {
return reinterpret_cast<RBTreeNode*>(color_parent & ~1);
}
Color color() const {
return static_cast<Color>(color_parent & 1);
}
void setParentAndColor(RBTreeNode* p, Color c) {
color_parent = reinterpret_cast<uintptr_t>(p) | static_cast<uintptr_t>(c);
}
};
7. 实际应用中的问题与解决
7.1 自定义比较函数
当键类型需要特殊比较逻辑时,需要提供自定义比较函数:
cpp复制struct CaseInsensitiveCompare {
bool operator()(const std::string& a, const std::string& b) const {
return strcasecmp(a.c_str(), b.c_str()) < 0;
}
};
mymap<std::string, int, CaseInsensitiveCompare> caseInsensitiveMap;
7.2 异常安全性
确保在异常发生时容器保持一致性:
- 内存分配失败时应回滚已完成的修改
- 比较函数抛出异常时应恢复树的状态
- 析构函数需要正确处理部分构造的对象
7.3 线程安全考虑
基础实现不是线程安全的,如果需要多线程访问,可以考虑:
- 粗粒度锁:整个容器一把锁
- 细粒度锁:每个节点一把锁(实现复杂)
- 读写锁:区分读写操作
提示:在实际项目中,通常推荐在使用层加锁,而不是修改容器实现。或者使用并发容器如tbb::concurrent_map。
8. 扩展功能实现
8.1 范围查询
实现lower_bound和upper_bound支持范围查询:
cpp复制iterator lower_bound(const Key& key) {
Node* current = root;
Node* result = nullptr;
while (current) {
if (!comp(current->data.first, key)) { // current >= key
result = current;
current = current->left;
} else {
current = current->right;
}
}
return iterator(result);
}
iterator upper_bound(const Key& key) {
Node* current = root;
Node* result = nullptr;
while (current) {
if (comp(key, current->data.first)) { // key < current
result = current;
current = current->left;
} else {
current = current->right;
}
}
return iterator(result);
}
8.2 支持移动语义
现代C++应该支持移动构造和移动赋值:
cpp复制// 移动构造函数
RBTree(RBTree&& other) noexcept
: root(other.root), size(other.size), comp(std::move(other.comp)) {
other.root = nullptr;
other.size = 0;
}
// 移动赋值运算符
RBTree& operator=(RBTree&& other) noexcept {
if (this != &other) {
clear(); // 释放现有资源
root = other.root;
size = other.size;
comp = std::move(other.comp);
other.root = nullptr;
other.size = 0;
}
return *this;
}
8.3 支持初始化列表
方便容器初始化:
cpp复制// mymap中增加
mymap(std::initializer_list<std::pair<const Key, T>> init) {
for (const auto& kv : init) {
insert(kv);
}
}
// 使用示例
mymap<int, std::string> m = {
{1, "one"},
{2, "two"},
{3, "three"}
};
实现一个完整的红黑树容器是一项挑战,但通过这个过程,你会对平衡二叉树的运作机制、STL容器的设计理念以及C++模板编程有更深入的理解。虽然性能可能不及标准库实现,但学习价值是无法衡量的。
