基本实现思路如下:

一、红黑树基础
1.1 什么是红黑树
红黑树是一种自平衡的二叉搜索树 ,每个节点增加一个存储位表示颜色(红或黑)。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是近似平衡的。
1.2 红黑树的五条性质
-
每个节点不是红色就是黑色
-
根节点是黑色
-
每个叶子节点(
NIL)是黑色(通常用空指针表示) -
如果一个节点是红色,则它的两个子节点都是黑色(不能有连续的红节点)
-
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(黑色高度相等)
1.3 与 AVL 树的对比
-
AVL 树:严格平衡(左右子树高度差不超过1),查询快,插入/删除旋转较多
-
红黑树:近似平衡(最长路径不超过最短路径的2倍),插入/删除旋转较少,整体性能更优
-
应用场景 :
map,set, Linux 内核的epoll红黑树
二、红黑树节点与迭代器
2.1 节点结构 RBTreeNode<T>
cpp
template<class T>
class RBTreeNode
{
T _data;
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
Color _col; // RED 、BLACK
};
-
三叉链表(左右 + 父节点)便于迭代器的前后移动
-
_data存储实际数据(map为pair<const K,V>,set为const K)
2.2 迭代器 Iterator<T, Ref, Ptr>
cpp
template <class T, class Ref, class Ptr>
class Iterator
{
Node* _node;
Node* _root; // 用于 --end()
};
-
operator++(前置):cppSelf& operator++() { assert(_node != nullptr); if (_node->_right)//右不为空 下一个节点就是 此右子树的 最左节点 { Node* leftMost = _node->_right; while (leftMost->_left) { leftMost = leftMost->_left;//找到左子树最小节点 } _node = leftMost; } else//右为空 检查上一个父亲节点是祖父节点的左还是右节点--左则_node = parent 右则往上一直走 直到是上一个节点的左节点或者是根节点 { Node* cur = _node; Node* parent = cur->_parent; while (parent && parent->_right == cur) { cur = parent; parent = cur->_parent; } _node = parent; } return *this; }-
有右孩子 → 右子树的最左节点
-
无右孩子 → 向上回溯,直到当前节点是父节点的左孩子
-
-
operator--(前置):cppSelf& operator--()// 左 中 右--》右 中 左 { if (_node == nullptr) { Node* cur = _root; while (cur && cur->_right)//加判空 防止空树 { cur = cur->_right; } _node = cur; return *this; } if (_node->_left)//左不为空 下一个节点就是 此左子树的 最右节点 { Node* rightMin = _node->_left; while (rightMin->_right) { rightMin = rightMin->_right;//找到右子树最大节点 } _node = rightMin; } else//左为空 检查上一个父亲节点是祖父节点的左还是右节点--右则_node = parent 右则往上一直走 直到是上一个节点的左节点或者是根节点 { Node* cur = _node; Node* parent = cur->_parent; while (parent && parent->_left == cur) { cur = parent; parent = cur->_parent; } _node = parent; } return *this; }-
有左孩子 → 左子树的最右节点
-
无左孩子 → 向上回溯,直到当前节点是父节点的右孩子
-
如果
_node == nullptr(即end()),回退到整棵树的最大节点
-
面试常问 :迭代器递增/递减的时间复杂度是多少?
平均 O(1),整体中序遍历整棵树为 O(N)
三、红黑树封装
3.1 模板参数设计
cpp
template<class K, class T, class KeyofT>
class RBTree
-
K:键类型 -
T:节点存储的数据类型(map为pair<const K,V>,set为const K) -
KeyofT:仿函数,用于从T中提取K
KeyofT 的作用 :统一比较逻辑,使红黑树无需关心 T 的具体结构
3.2 插入操作 Insert
-
返回类型:
pair<iterator, bool>-
.first:插入位置或已存在节点的迭代器 -
.second:true表示插入成功,false表示键已存在
-
-
插入后需要调整颜色和旋转,维持红黑树性质
-
成功插入后,
_size++维护节点计数
面试常问 :为什么
Insert要返回pair<iterator, bool>?为了支持
operator[]它需要知道插入是否成功,并能返回新插入或已存在元素的引用
cpp
pair<iterator,bool> Insert(const T& key)
{
if (_root == nullptr)
{
_root = new Node(key);
_root->_col = BLACK;
++_size;
return {iterator(_root,_root),true};
}
//空树插入情况单独考虑
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kot(cur->_data) < kot(key))
{
parent = cur;
cur = cur->_right;
}
else if (kot(cur->_data) > kot(key))
{
parent = cur;
cur = cur->_left;
}
else
return { iterator(cur,_root),false };
}
//循环结束 说明已经找到需要插入的位置
cur = new Node(key);
cur->_col = RED;
Node* newnode = cur;
if (kot(parent->_data) < kot(cur->_data))
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;//前面也只是 对于父亲的指针做了更改
//确定cur的是父亲节点左还是右
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔节点不存在或者节点颜色为黑的情况--这意味着红色节点连续且无法通过变色直接解决 需要旋转
{
if (cur == parent->_left)//从整体上来看 用特殊情况替代一般情况
{
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
RotateR(parent);
RotateL(grandfather);
cur->_col = RED;
grandfather->_col = BLACK;
}
else
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
++_size;
_root->_col = BLACK;//最后把_root 的颜色置为黑 肯定没问题
return { iterator(newnode,_root),true };
}
3.3 查找操作 Find
-
返回
iterator或const_iterator -
找不到返回
end()(即iterator(nullptr, _root))
面试常问 :为什么
find找不到要返回end()而不是nullptr?统一接口,所有 STL 容器都这么设计,用户统一用
if (it != container.end())判断
cpp
iterator _Find(Node* root,const K& key)
{
Node* cur = root;
while (cur)
{
if (key> kot(cur->_data))
{
cur = cur->_right;
}
else if (key < kot(cur->_data))
{
cur = cur->_left;
}
else
{
return iterator(cur, _root);//构造一个迭代器 需要传的参数 这里要自己去参考迭代器的构造函数
}
}
return iterator(nullptr,_root);
}
const_iterator _Find(Node* root,const K& key) const
{
Node* cur = root;
while (cur)
{
if (key> kot(cur->_data))
{
cur = cur->_right;
}
else if (key < kot(cur->_data))
{
cur = cur->_left;
}
else
{
return const_iterator(cur, _root);//构造一个迭代器 需要传的参数 这里要自己去参考迭代器的构造函数
}
}
return const_iterator(nullptr,_root);
}
3.4 旋转函数 RotateL / RotateR
-
左旋和右旋是红黑树调整的核心操作
-
注意更新父节点指针和子节点指针的双向链接
-
具体代码 参考AVL树的实现
3.5 资源管理(RAII)
cpp
// 强制使用默认构造
RBTree() = default;
// 析构函数
~RBTree() { _Destroy(_root); }
// 拷贝构造 --深拷贝
RBTree(const RBTree& other) : _root(nullptr), _size(other._size)
{
_root = _Copy(other._root);
}
// 赋值运算符
RBTree& operator=(const RBTree& other)
{
if (this != &other)
{
_Destroy(_root);
_root = _Copy(other._root);
_size = other._size;
}
return *this;
}
// 节点数量
size_t Size() const { return _size; }
bool Empty() const { return _root == nullptr; }
辅助函数:
-
_Destroy(Node* root):递归释放整棵树 -
_Copy(Node* root):递归深拷贝整棵树,注意拷贝时保留颜色和父指针
面试常问 :如果不实现拷贝构造和赋值,会发生什么?
默认浅拷贝会导致多个对象共享同一棵树,修改一个会影响另一个,引发内存泄漏或重复释放(双重释放)
四、容器封装 map 和 set
4.1 Map 封装
cpp
namespace JMP2
{
template <class K, class V >
class map
{
struct MapKeyofT
{
const K& operator()(const pair<const K,V>& kv) const
{
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::iterator iterator;//在类里面 取没有确定的类模板成员 要加 typename
typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::const_iterator const_iterator;
public:
pair<iterator,bool> insert(const pair<K, V>& kv)//具体插入什么根据具体容器决定
{
return _rb.Insert(kv);
}
iterator begin() { return _rb.begin(); }
const_iterator begin() const { return _rb.begin(); }
iterator end() { return _rb.end(); }
const_iterator end() const { return _rb.end(); }
iterator find(const K& kv)
{
return _rb.Find(kv);
}
const_iterator find(const K& kv) const
{
return _rb.Find(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
size_t Size() const { return _rb.Size(); }
bool Empty() const { return _rb.Empty(); }
private:
RBTree < K, pair<const K,V>, MapKeyofT> _rb;
};
}
4.2 Set 封装
cpp
namespace JMP1
{
template <class K>
class set
{
struct SetKeyofT
{
const K& operator()(const K& key) const
{
return key;
}
};
public: //要明白 typename的作用 编译器默认认为 依赖名::xxx 不是类型(而是变量或函数)
typedef typename RBTree<K, const K, SetKeyofT>::iterator iterator;//编译器猜不出来 标准就规定 如果这确实是一个类型,必须显式地用 typename 关键字告诉编译器
typedef typename RBTree<K, const K, SetKeyofT>::const_iterator const_iterator;
public:
pair<iterator, bool> Insert(const K& key)//具体插入什么根据具体容器决定
{
return _rb.Insert(key);
}
iterator begin() { return _rb.begin(); }
const_iterator begin() const { return _rb.begin(); }
iterator end() { return _rb.end(); }
const_iterator end() const { return _rb.end(); }
iterator find(const K& key)
{
return _rb.Find(key);
}
const_iterator find(const K& key) const
{
return _rb.Find(key);
}
size_t Size() const { return _rb.Size(); }
bool Empty() const { return _rb.Empty(); }
private:
RBTree < K, const K, SetKeyofT> _rb;//前面两个是重复的 之所以这么设计是因为 要兼顾map的实现
};
}
六、踩坑记录
坑1:MapKeyofT 参数类型不匹配
错误 :operator()(const pair<K,V>& kv),但树节点存储的是 pair<const K,V>
解决 :改为 operator()(const pair<const K,V>& kv)
坑2:kot(key) 错误用法
错误 :在 Find 中写 kot(key) < kot(cur->_data)
解决 :直接 key < kot(cur->_data),因为 key 已经是 K 类型,不需要再提取
坑3:拷贝构造忘记复制 _size
错误 :拷贝构造只复制了树结构,没有复制 _size
解决 :初始化列表加 _size(other._size)
坑4:赋值运算符未检查自赋值
解决 :加 if (this != &other)
七、总结
通过完整的手写红黑树及其封装,深入理解:
-
红黑树的核心性质与调整逻辑
-
迭代器的设计与实现
-
模板与仿函数在容器封装中的应用
-
RAII资源管理的必要性--内存关系
-
STL 容器的标准接口设计规范
八、面试高频问题汇总
Q1:红黑树插入有几种情况?
-
新节点为根 → 直接染黑
-
父节点为黑 → 直接插入,无需调整
-
父节点为红(需要调整):
-
叔叔为红 → 变色(父、叔变黑,祖父变红,继续往上检查)。
-
叔叔为黑(或不存在) → 旋转 + 变色:
-
LL型 → 右旋祖父
-
RR型 → 左旋祖父
-
LR型 → 左旋父 + 右旋祖父
-
RL型 → 右旋父 + 左旋祖父
-
-
Q2:迭代器失效问题?
-
插入操作:不导致任何已有迭代器失效(节点地址不变)
-
删除操作:指向被删除节点的迭代器失效
-
拷贝构造/赋值:与源对象无关,不影响已有迭代器
Q3:map 和 unordered_map 底层区别?
-
map:红黑树(有序,查询 O(log N)) -
unordered_map:哈希表(无序,查询 O(1) 平均,最坏 O(N))
Q4:为什么 map 的键是 const?
防止用户通过迭代器修改键,破坏红黑树的有序性。
Q5:find 返回 end() 而不是 nullptr 的好处?
统一接口,所有 STL 容器都这么设计,用户代码可以写成统一风格 if (it != container.end())。
Q6:深拷贝和浅拷贝的区别?
-
浅拷贝:默认拷贝构造/赋值,只复制指针,多个对象共享同一块内存
-
深拷贝:复制整个数据结构,每个对象独立管理自己的资源
-
我们的实现 :
_Copy递归复制整棵树,保证每个RBTree对象独立
我的gitee仓库