在C++标准模板库(STL)中,set和map作为关联式容器,其底层实现依赖于一种高效的自平衡二叉搜索树——红黑树。红黑树之所以被选为底层数据结构,主要基于以下几个关键特性:
平衡性保证:红黑树通过颜色标记和旋转操作,确保树的高度始终保持在O(log n)级别,这使得查找、插入和删除操作的时间复杂度都能稳定在O(log n)。
严格的排序规则:红黑树维护了严格的有序性,这对于需要有序遍历的set和map至关重要。
高效的插入删除:相比AVL树,红黑树的平衡条件相对宽松,减少了旋转操作的频率,在实际应用中往往表现更优。
红黑树必须满足以下五个基本性质:
这些规则共同保证了红黑树的平衡性。其中第四条规则限制了红色节点的连续出现,第五条规则确保了任意路径不会比其他路径长出两倍以上。
虽然set和map都基于红黑树实现,但它们在数据存储方式上有本质区别:
这种差异导致了它们在红黑树模板参数上的不同处理方式。在STL实现中,通过引入"从value中提取key"的仿函数(KeyOfValue)来统一处理这两种情况:
cpp复制// set使用的identity仿函数
struct identity {
const T& operator()(const T& x) const { return x; }
};
// map使用的select1st仿函数
struct select1st {
const K& operator()(const pair<K,V>& x) const { return x.first; }
};
这种设计体现了泛型编程的思想,使得同一套红黑树实现可以同时支持set和map的不同需求。
红黑树节点的设计采用了继承体系,将通用属性与具体数据分离:
cpp复制// 节点基类(处理颜色和指针关系)
struct __rb_tree_node_base {
typedef __rb_tree_color_type color_type;
color_type color; // 节点颜色
base_ptr parent; // 父节点指针
base_ptr left; // 左孩子指针
base_ptr right; // 右孩子指针
};
// 具体节点类(存储实际数据)
template <class Value>
struct __rb_tree_node : public __rb_tree_node_base {
typedef Value value_type;
value_type value_field; // 存储的实际数据
};
这种设计有以下优势:
红黑树的核心类需要处理多种模板参数以适应不同容器的需求:
cpp复制template <class Key, class Value, class KeyOfValue,
class Compare, class Alloc = alloc>
class rb_tree {
protected:
typedef rb_tree_node<Value> node_type;
typedef node_type* link_type;
public:
// 核心接口
iterator insert_unique(const value_type& x);
size_type erase(const key_type& x);
iterator find(const key_type& x);
private:
link_type header; // 特殊头节点
size_type node_count; // 节点计数
// ... 其他成员函数
};
关键模板参数说明:
Key: 用于查找和排序的键类型Value: 实际存储的数据类型(set为Key,map为pair<const Key,T>)KeyOfValue: 从Value中提取Key的仿函数Compare: 键比较函数对象,默认为lessset的实现相对简单,因为它只需要存储键值:
cpp复制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>, Compare, Alloc> rep_type;
rep_type t; // 红黑树实例
public:
// 接口转发
pair<iterator,bool> insert(const value_type& x) {
return t.insert_unique(x);
}
// ... 其他接口
};
set的关键点:
map的实现需要考虑键值对的存储:
cpp复制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>, Compare, Alloc> rep_type;
rep_type t;
public:
// 接口转发
pair<iterator,bool> insert(const value_type& x) {
return t.insert_unique(x);
}
// ... 其他接口
};
map的特殊之处:
红黑树迭代器的核心在于实现中序遍历的顺序访问。中序遍历的顺序是:左子树 → 根节点 → 右子树。迭代器的++操作需要按照这个顺序移动。
cpp复制template <class T>
struct RBTreeIterator {
typedef RBTreeNode<T> Node;
Node* _node;
// 前置++操作
Self& operator++() {
if (_node->_right) {
// 情况1:有右子树 → 找右子树的最左节点
_node = _node->_right;
while (_node->_left) _node = _node->_left;
} else {
// 情况2:无右子树 → 向上找第一个是父节点左孩子的祖先
Node* parent = _node->_parent;
while (parent && _node == parent->_right) {
_node = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
// 其他操作...
};
cpp复制iterator begin() {
// 返回最左节点(中序第一个)
Node* leftMost = _root;
while (leftMost && leftMost->_left) {
leftMost = leftMost->_left;
}
return iterator(leftMost);
}
iterator end() {
// 返回nullptr作为结束标记
return iterator(nullptr);
}
为了支持const迭代器,我们需要定义const版本的迭代器类型:
cpp复制typedef RBTreeIterator<T, T&, T*> iterator;
typedef RBTreeIterator<T, const T&, const T*> const_iterator;
红黑树的插入操作分为两个主要阶段:
cpp复制pair<iterator, bool> insert(const T& data) {
// 1. 普通BST插入
if (_root == nullptr) {
_root = new Node(data);
_root->_col = BLACK;
return make_pair(iterator(_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), 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;
// 2. 红黑树平衡调整
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
if (parent == grandfather->_left) {
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) {
// 情况1:叔叔节点为红色 → 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
if (cur == parent->_right) {
// 情况2:LR型 → 先左旋再右旋
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
} else {
// 情况3:LL型 → 右旋
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
break;
}
} else {
// 对称处理右子树情况
// ...
}
}
_root->_col = BLACK;
return make_pair(iterator(newnode), true);
}
旋转操作是红黑树保持平衡的关键:
cpp复制void RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 处理subRL与parent的关系
parent->_right = subRL;
if (subRL) subRL->_parent = parent;
// 处理subR与祖父节点的关系
Node* pparent = parent->_parent;
subR->_parent = pparent;
if (pparent == nullptr) {
_root = subR;
} else if (parent == pparent->_left) {
pparent->_left = subR;
} else {
pparent->_right = subR;
}
// 处理parent与subR的关系
subR->_left = parent;
parent->_parent = subR;
}
void RotateR(Node* parent) {
// 对称实现右旋
// ...
}
map的operator[]是其重要特性之一,实现原理如下:
cpp复制V& operator[](const K& key) {
// 尝试插入键值对,value使用默认构造
pair<iterator, bool> ret = insert(make_pair(key, V()));
// 返回value的引用
return ret.first->second;
}
这种实现方式使得我们可以像使用数组一样使用map:
cpp复制map<string, int> word_count;
word_count["hello"] = 1; // 如果"hello"不存在会自动插入
红黑树需要比较键值来维护排序,但对于map来说,存储的是pair,而比较只需要基于key。KeyOfValue仿函数提供了从存储值中提取键的统一方式:
这种设计避免了为set和map分别实现不同的红黑树版本,提高了代码复用性。
红黑树的迭代器在以下情况下会失效:
与vector不同,红黑树的插入操作通常不会导致其他迭代器失效(除非发生rebalance)。这是关联式容器的一大优势。
哨兵节点:STL实现中常使用一个额外的header节点,其left指向最小节点,right指向最大节点,parent指向根节点。这可以简化边界条件处理。
节点缓存:可以实现一个节点缓存池,减少频繁的内存分配释放。
颜色存储优化:可以利用指针的低位来存储颜色信息(因为节点地址通常对齐,低位为0),节省内存。
完整的红黑树实现需要经过严格测试,特别是对于边界条件的处理:
cpp复制void TestRBTree() {
RBTree<int, int, Identity<int>> tree;
// 测试插入
for (int i = 0; i < 100; ++i) {
tree.insert(i);
}
// 验证大小
assert(tree.size() == 100);
// 验证排序
int prev = -1;
for (auto it = tree.begin(); it != tree.end(); ++it) {
assert(*it > prev);
prev = *it;
}
// 测试删除
for (int i = 0; i < 100; i += 2) {
tree.erase(i);
}
assert(tree.size() == 50);
// 验证红黑树性质
assert(tree.verify_properties());
}
在实际项目中使用自定义实现的map/set时,需要考虑以下因素:
内存管理:与STL实现相比,我们的分配器支持可能不够完善
异常安全:需要确保在异常发生时资源不会泄漏
调试支持:可以添加调试接口,如可视化树结构、验证红黑树性质等
性能分析:与标准库实现进行性能对比,找出优化点
红黑树的这种设计模式可以推广到其他数据结构的实现中:
多键map:可以通过修改KeyOfValue仿函数支持多个键的组合比较
自定义排序:通过替换Compare模板参数,可以实现不同的排序方式
内存数据库:基于红黑树可以实现简单的内存键值存储
理解红黑树的实现不仅有助于深入理解STL,也为设计其他复杂数据结构提供了思路和模式。