1. 理解map与set的核心特性
在C++标准库中,map和set是两种最常用的关联容器,它们底层通常采用红黑树实现。map存储的是键值对(key-value pairs),而set只存储键(key)。这两种容器都具备自动排序的特性,查找时间复杂度为O(log n)。
我曾在实际项目中遇到过这样的场景:需要快速查询某个元素是否存在,同时要求数据按特定顺序排列。这时set就派上了大用场。比如用户ID集合、关键词过滤等场景,set的自动去重和排序特性可以省去大量手动处理的工作。
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) {}
};
对于map的实现,节点需要存储键值对:
cpp复制template <typename Key, typename Value>
struct MapNode {
std::pair<const Key, Value> kv;
Color color;
MapNode* left;
MapNode* right;
MapNode* parent;
};
2.2 树的基本操作框架
红黑树的核心操作包括:
- 左旋(leftRotate)
- 右旋(rightRotate)
- 插入修复(insertFixup)
- 删除修复(deleteFixup)
以左旋操作为例:
cpp复制void leftRotate(Node* x) {
Node* y = x->right;
x->right = y->left;
if (y->left != nullptr) {
y->left->parent = x;
}
y->parent = x->parent;
// ... 后续处理父节点指向
}
3. set的完整实现
3.1 类定义与迭代器
set的基本框架:
cpp复制template <typename Key, typename Compare = std::less<Key>>
class Set {
public:
using iterator = RBTreeIterator<Key>;
// 构造函数
Set() : root(nullptr), size_(0) {}
// 容量相关
bool empty() const { return size_ == 0; }
size_t size() const { return size_; }
// 修改操作
std::pair<iterator, bool> insert(const Key& key);
size_t erase(const Key& key);
// 查找操作
iterator find(const Key& key);
size_t count(const Key& key) const;
private:
RBTreeNode<Key>* root;
size_t size_;
Compare comp;
};
3.2 插入操作实现
插入操作需要考虑红黑树的五个性质:
- 每个节点是红色或黑色
- 根节点是黑色
- 每个叶子节点(NIL)是黑色
- 红色节点的子节点必须是黑色
- 从任一节点到其每个叶子的路径包含相同数目的黑色节点
插入代码框架:
cpp复制std::pair<iterator, bool> insert(const Key& key) {
Node* parent = nullptr;
Node* current = root;
while (current != nullptr) {
parent = current;
if (comp(key, current->data)) {
current = current->left;
} else if (comp(current->data, key)) {
current = current->right;
} else {
return {iterator(current), false}; // 已存在
}
}
Node* newNode = new Node(key);
newNode->parent = parent;
if (parent == nullptr) {
root = newNode;
} else if (comp(key, parent->data)) {
parent->left = newNode;
} else {
parent->right = newNode;
}
insertFixup(newNode);
size_++;
return {iterator(newNode), true};
}
4. map的完整实现
4.1 类定义与迭代器
map与set的主要区别在于存储的是键值对:
cpp复制template <typename Key, typename Value, typename Compare = std::less<Key>>
class Map {
public:
using iterator = RBTreeIterator<std::pair<const Key, Value>>;
// 元素访问
Value& operator[](const Key& key);
Value& at(const Key& key);
// 其他接口与set类似
// ...
};
4.2 operator[]的实现
这个操作符是map的特色功能,当key不存在时会自动插入:
cpp复制Value& operator[](const Key& key) {
auto it = find(key);
if (it == end()) {
it = insert({key, Value()}).first;
}
return it->second;
}
5. 红黑树的平衡维护
5.1 插入修复情况分析
插入后的修复操作需要考虑四种主要情况:
- 叔叔节点是红色
- 当前节点是父节点的右孩子,且叔叔是黑色
- 当前节点是父节点的左孩子,且叔叔是黑色
- 当前节点是根节点
修复代码框架:
cpp复制void insertFixup(Node* z) {
while (z->parent != nullptr && z->parent->color == RED) {
if (z->parent == z->parent->parent->left) {
Node* y = z->parent->parent->right;
if (y != nullptr && y->color == RED) {
// 情况1:叔叔是红色
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent;
} else {
// 情况2和3
if (z == z->parent->right) {
z = z->parent;
leftRotate(z);
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
rightRotate(z->parent->parent);
}
} else {
// 对称情况处理
}
}
root->color = BLACK;
}
5.2 删除修复情况
删除操作更为复杂,需要考虑六种主要情况。核心思想是通过旋转和重新着色来保持红黑树的性质。
6. 迭代器实现
6.1 迭代器设计
迭代器需要支持前向遍历,核心是找到下一个节点的算法:
cpp复制template <typename T>
class RBTreeIterator {
public:
RBTreeIterator(Node* p = nullptr) : current(p) {}
T& operator*() { return current->data; }
RBTreeIterator& operator++() {
if (current->right != nullptr) {
current = current->right;
while (current->left != nullptr) {
current = current->left;
}
} else {
Node* p = current->parent;
while (p != nullptr && current == p->right) {
current = p;
p = p->parent;
}
current = p;
}
return *this;
}
private:
Node* current;
};
7. 性能优化与测试
7.1 内存管理优化
在实际项目中,我们可以通过以下方式优化:
- 使用内存池预分配节点
- 实现移动语义支持
- 提供hint版本的insert操作
7.2 测试用例设计
完整的测试应该包括:
cpp复制void testMap() {
Map<std::string, int> m;
m["apple"] = 5;
m["banana"] = 3;
assert(m.size() == 2);
assert(m["apple"] == 5);
auto it = m.find("banana");
assert(it != m.end());
assert(it->second == 3);
m.erase("apple");
assert(m.size() == 1);
}
8. 实际项目中的经验教训
-
边界条件处理:特别注意空树、根节点、叶子节点的处理。我曾遇到过因为忘记处理NIL节点导致的段错误。
-
调试技巧:实现一个可视化打印函数,可以直观地检查树结构:
cpp复制void printTree(Node* root, int indent = 0) {
if (root == nullptr) return;
printTree(root->right, indent + 4);
std::cout << std::string(indent, ' ')
<< (root->color == RED ? "R " : "B ")
<< root->data << "\n";
printTree(root->left, indent + 4);
}
-
性能考量:虽然红黑树的各项操作都是O(log n),但在数据量较小(<100)时,线性结构可能更快。实际项目中需要根据场景选择。
-
线程安全:标准库的实现通常不是线程安全的。如果需要在多线程环境中使用,需要额外加锁或者考虑其他并发数据结构。
