C++
手写红黑树内核
使用左倾红黑树实现旋转、变色、插入与删除,为 set/map 系列建立共享有序索引。
发布于 2026年7月23日
手写红黑树内核
使用左倾红黑树实现旋转、变色、插入与删除,为 set/map 系列建立共享有序索引。
本系列代码使用 C++20 和
oc::handmade命名空间,目标是解释实现机制、复杂度和工程边界,不是替代标准库。普通容器与缓存核心不内置互斥锁;这不代表 lock-free。
一、学习目标
- 理解红黑树五条性质
- 实现局部旋转与颜色修复
- 用黑高约束证明 O(log n)
二、前置条件
熟悉二叉搜索树、递归、unique_ptr 和比较器。
Linux/macOS:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure
Windows PowerShell:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --config Debug
ctest --test-dir build -C Debug --output-on-failure
三、问题与设计选择
采用左倾红黑树表达 2-3 树:红链接向左,任何路径黑高相同。递归修改返回新的子树根,根节点最终染黑。
这里刻意保留一条边界:教学实现覆盖构造、复制移动、核心修改、查找和迭代契约,但不复刻标准库全部重载、ABI、constexpr、异构查找或节点句柄。
四、内存布局与核心不变量
根为黑色;空链接视为黑色;没有连续红节点;红链接左倾;任一节点到空叶子的黑节点数量相同。
每个修改操作都按“准备资源 → 构造新状态 → 提交连接或指针 → 清理旧状态”的顺序设计。提交点之前发生异常,应保持原对象可继续使用;无法提供强保证时,会在接口说明中明确基本保证。
五、核心实现
node* fix_up(node* h) noexcept {
if (is_red(h->right) && !is_red(h->left)) h = rotate_left(h);
if (is_red(h->left) && is_red(h->left->left)) h = rotate_right(h);
if (is_red(h->left) && is_red(h->right)) flip_colors(h);
return h;
}
上面先聚焦最容易写错的核心步骤;若本篇对应一个独立组件,下一节给出统一工程中的完整教学实现。代码没有放入 std 命名空间,避免未定义行为和名称冲突。
六、完整教学实现
下面是统一工程中经过 GCC、Clang、GoogleTest 和 Sanitizer 验证的完整组件。它依赖前序文章已经实现的公共类型以及头文件中的标准库 #include。
namespace oc::handmade {
template<class Key, class Mapped, class Compare = std::less<Key>>
class rb_tree {
struct node {
Key key;
Mapped mapped;
bool red{true};
std::unique_ptr<node> left;
std::unique_ptr<node> right;
node(Key key_value, Mapped mapped_value)
: key(std::move(key_value)), mapped(std::move(mapped_value)) {}
};
std::unique_ptr<node> root_;
std::size_t size_{};
Compare compare_{};
static bool is_red(const std::unique_ptr<node>& value) noexcept {
return value && value->red;
}
static std::unique_ptr<node> rotate_left(std::unique_ptr<node> h) {
auto x = std::move(h->right);
h->right = std::move(x->left);
x->left = std::move(h);
x->red = x->left->red;
x->left->red = true;
return x;
}
static std::unique_ptr<node> rotate_right(std::unique_ptr<node> h) {
auto x = std::move(h->left);
h->left = std::move(x->right);
x->right = std::move(h);
x->red = x->right->red;
x->right->red = true;
return x;
}
static void flip_colors(node* h) noexcept {
h->red = !h->red;
if (h->left) h->left->red = !h->left->red;
if (h->right) h->right->red = !h->right->red;
}
static std::unique_ptr<node> fix_up(std::unique_ptr<node> h) {
if (is_red(h->right) && !is_red(h->left)) h = rotate_left(std::move(h));
if (is_red(h->left) && is_red(h->left->left)) h = rotate_right(std::move(h));
if (is_red(h->left) && is_red(h->right)) flip_colors(h.get());
return h;
}
static std::unique_ptr<node> move_red_left(std::unique_ptr<node> h) {
flip_colors(h.get());
if (h->right && is_red(h->right->left)) {
h->right = rotate_right(std::move(h->right));
h = rotate_left(std::move(h));
flip_colors(h.get());
}
return h;
}
static std::unique_ptr<node> move_red_right(std::unique_ptr<node> h) {
flip_colors(h.get());
if (h->left && is_red(h->left->left)) {
h = rotate_right(std::move(h));
flip_colors(h.get());
}
return h;
}
std::unique_ptr<node> insert_impl(
std::unique_ptr<node> h,
Key key,
Mapped mapped,
node*& result,
bool& inserted
) {
if (!h) {
inserted = true;
++size_;
auto fresh = std::make_unique<node>(std::move(key), std::move(mapped));
result = fresh.get();
return fresh;
}
if (compare_(key, h->key)) {
h->left = insert_impl(
std::move(h->left), std::move(key), std::move(mapped), result, inserted);
} else if (compare_(h->key, key)) {
h->right = insert_impl(
std::move(h->right), std::move(key), std::move(mapped), result, inserted);
} else {
result = h.get();
}
return fix_up(std::move(h));
}
static node* minimum(node* h) noexcept {
while (h->left) h = h->left.get();
return h;
}
static std::unique_ptr<node> erase_min(std::unique_ptr<node> h) {
if (!h->left) return nullptr;
if (!is_red(h->left) && !is_red(h->left->left))
h = move_red_left(std::move(h));
h->left = erase_min(std::move(h->left));
return fix_up(std::move(h));
}
std::unique_ptr<node> erase_impl(std::unique_ptr<node> h, const Key& key) {
if (compare_(key, h->key)) {
if (h->left) {
if (!is_red(h->left) && !is_red(h->left->left))
h = move_red_left(std::move(h));
h->left = erase_impl(std::move(h->left), key);
}
} else {
if (is_red(h->left)) h = rotate_right(std::move(h));
const bool equal = !compare_(key, h->key) && !compare_(h->key, key);
if (equal && !h->right) return nullptr;
if (h->right) {
if (!is_red(h->right) && !is_red(h->right->left))
h = move_red_right(std::move(h));
const bool now_equal =
!compare_(key, h->key) && !compare_(h->key, key);
if (now_equal) {
node* successor = minimum(h->right.get());
h->key = successor->key;
h->mapped = successor->mapped;
h->right = erase_min(std::move(h->right));
} else {
h->right = erase_impl(std::move(h->right), key);
}
}
}
return fix_up(std::move(h));
}
template<class F>
static void inorder_impl(const node* current, F& callback) {
if (!current) return;
inorder_impl(current->left.get(), callback);
callback(current->key, current->mapped);
inorder_impl(current->right.get(), callback);
}
static int validate_black_height(const node* current) {
if (!current) return 1;
if (current->right && current->right->red) return -1;
if (current->red &&
((current->left && current->left->red) ||
(current->right && current->right->red))) return -1;
int left = validate_black_height(current->left.get());
int right = validate_black_height(current->right.get());
if (left < 0 || right < 0 || left != right) return -1;
return left + (current->red ? 0 : 1);
}
public:
struct entry {
const Key* key;
Mapped* mapped;
};
std::pair<entry, bool> insert(Key key, Mapped mapped) {
node* result = nullptr;
bool inserted = false;
root_ = insert_impl(
std::move(root_), std::move(key), std::move(mapped), result, inserted);
root_->red = false;
return {{&result->key, &result->mapped}, inserted};
}
Mapped* find(const Key& key) {
node* current = root_.get();
while (current) {
if (compare_(key, current->key)) current = current->left.get();
else if (compare_(current->key, key)) current = current->right.get();
else return ¤t->mapped;
}
return nullptr;
}
const Mapped* find(const Key& key) const {
return const_cast<rb_tree*>(this)->find(key);
}
bool contains(const Key& key) const { return find(key) != nullptr; }
bool erase(const Key& key) {
if (!contains(key)) return false;
if (!is_red(root_->left) && !is_red(root_->right)) root_->red = true;
root_ = erase_impl(std::move(root_), key);
if (root_) root_->red = false;
--size_;
return true;
}
std::size_t size() const noexcept { return size_; }
bool empty() const noexcept { return size_ == 0; }
template<class F>
void inorder(F&& callback) const {
inorder_impl(root_.get(), callback);
}
bool valid() const {
return (!root_ || !root_->red) && validate_black_height(root_.get()) > 0;
}
};
} // namespace oc::handmade
生产级标准库还要处理完整 allocator 传播、全部重载、ABI、调试迭代器和平台特化;这里保留的是能够独立推导核心数据结构的教学边界。
七、使用示例与输出
预期输出或状态:
插入 7 3 9 1 5 后中序为 1 3 5 7 9,校验器报告根黑、无连续红节点且黑高一致。
示例必须在文章对应的测试目标中实际编译。涉及顺序的输出只依赖接口明确承诺的顺序;无序容器不会把某次桶顺序写成稳定结果。
八、复杂度与失效规则
| 操作 | 复杂度 | 说明 |
|---|---|---|
| find/lower_bound | O(log n) | 按比较器下降 |
| insert | O(log n) | 旋转和变色 |
| erase | O(log n) | 向下移动红链接 |
| 遍历 | O(n) | 中序有序 |
复杂度中的 O(1) 若标记为“平均”或“摊还”,不能在面试中省略限定词。任何重新分配、节点删除、rehash 或缓存淘汰都必须单独说明迭代器、引用与指针是否失效。
九、异常安全与资源管理
- 获取资源后立即交给 RAII 对象或明确记录已构造数量。
- 用户类型构造、复制、移动、比较器和哈希器都可能抛异常。
- 只有在所有后续步骤不会失败时才修改不可回滚的链接。
- 析构、释放和关闭路径不得抛异常。
- 并发包装通过回调在锁内访问,避免返回保护对象的裸引用。
十、常见错误
1. 旋转时漏掉颜色转移
旋转时漏掉颜色转移会破坏本篇建立的契约。调试时先检查核心不变量,再缩小到触发该状态的最短操作序列。
2. 只验证搜索树顺序而不验证黑高
只验证搜索树顺序而不验证黑高会破坏本篇建立的契约。调试时先检查核心不变量,再缩小到触发该状态的最短操作序列。
3. 删除前没有保证下降方向存在红链接
删除前没有保证下降方向存在红链接会破坏本篇建立的契约。调试时先检查核心不变量,再缩小到触发该状态的最短操作序列。
十一、面试追问
- 红黑树最长路径为何不超过最短路径两倍?
- AVL 与红黑树如何取舍?
- 为什么旋转不破坏中序顺序?
回答时先说数据结构不变量,再给复杂度,最后说明异常、迭代器或并发边界,通常比背诵结论更有说服力。
十二、练习与自测
- 实现 invariant checker
- 输出 Graphviz 结构
- 统计随机插入时的旋转次数
自测标准:能够不看代码画出内存或节点关系,解释一次成功操作和一次失败回滚,并写出至少一个会击穿错误实现的测试。
十三、官方资料与延伸阅读
上一篇:手写 std::priority_queue | 下一篇:手写 std::set