在 C++ 标准库中,map
和 set
是常用的容器类型,它们提供了基于键的高效查找、插入和删除操作。为了实现这些功能,标准库通常使用红黑树(Red-Black Tree)这一自平衡二叉搜索树来作为底层数据结构。在本文中,我们将详细探讨如何通过封装红黑树(RBTree
)来实现 map
和 set
,并深入剖析其内部实现细节。
1. 红黑树(RBTree)基础
1.1 红黑树简介
本文不着重讲解红黑树,上一篇博客里有关于红黑树的具体讲解,请若有需求请移步https://blog.csdn.net/2301_77754590/article/details/144001181?spm=1001.2014.3001.5501
红黑树是一种特殊的二叉搜索树,它在普通的二叉搜索树的基础上,加入了“颜色”这一属性来确保树的平衡性。红黑树的核心优势在于,它能在 O(log n)
的时间复杂度下进行查找、插入和删除操作。红黑树的性质包括:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色的。
- 所有叶子节点(
nullptr
)是黑色的。 - 如果一个节点是红色的,则它的子节点必须是黑色的(没有连续的红色节点)。
- 从任何节点到每个叶子节点的路径上,必须经过相同数量的黑色节点。
这些性质使得红黑树在操作过程中能够始终保持平衡,从而保证了它在各类动态数据结构中的高效性。
红黑树被广泛应用于实现像 set
和 map
这样的容器,它们都需要能够进行快速的查找、插入和删除操作,而这些操作都能通过红黑树在 O(log n)
的时间复杂度内完成。
为了将红黑树封装到 map
和 set
容器中,我们需要提供一些基础设施,包括:
- 红黑树的节点类型(
RBTreeNode
)和迭代器类型(RBTreeIterator
)。 - 支持插入、查找、遍历等功能的
map
和set
容器类。 - 设计良好的函数和构造函数,使得容器能够支持各种操作。
接下来我们将详细描述如何实现这些功能。
2. RBTree
类的构造、拷贝构造和析构
接下来我们详细讲解 RBTree
类的构造、拷贝构造和析构。
2.1 构造函数 (RBTree()
)
RBTree() = default;
这是一个默认构造函数。它使用了 = default
语法,表示编译器生成一个默认构造函数。默认构造函数会将 _root
初始化为 nullptr
,即初始化一个空的红黑树。
2.2 拷贝构造函数
RBTree(const RBTree<K, T, KeyOfT>& t) {
_root = Copy(t._root);
}
- 这是一个拷贝构造函数,用于通过另一个红黑树对象
t
来构造一个新的红黑树。 - 它调用了一个私有的
Copy
函数来复制传入树t
的根节点_root
。 Copy
函数会递归地复制红黑树的所有节点,并维护节点之间的关系。
2.3 拷贝函数
Node* Copy(Node* root) {
if (root == nullptr)
return nullptr;
Node* newroot = new Node(root->_data);
newroot->_col = root->_col;
newroot->_left = Copy(root->_left);
if (newroot->_left)
newroot->_left->_parent = newroot;
newroot->_right = Copy(root->_right);
if (newroot->_right)
newroot->_right->_parent = newroot;
return newroot;
}
Copy
函数是一个递归函数,用于复制红黑树的每一个节点。- 它递归地复制当前节点的左子树和右子树,并更新新节点的父指针
_parent
。
2.4 析构函数
~RBTree() {
Destroy(_root);
_root = nullptr;
}
- 析构函数用于销毁树中的所有节点并释放内存。
- 它调用了
Destroy
函数来递归地销毁红黑树的节点。 - 在销毁所有节点后,将
_root
设置为nullptr
,确保红黑树对象的根节点指针为空。
2.5 销毁函数
void Destroy(Node* root) {
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
2.6 交换操作符
RBTree<K, T, KeyOfT>& operator=(RBTree<K, T, KeyOfT> t) {
swap(_root, t._root);
return *this;
}
- 赋值操作符重载是一个重要的操作,它实现了对红黑树的深拷贝。
- 为了避免自赋值,首先通过值传递方式将
t
传入(这会触发拷贝构造)。然后通过swap
函数交换当前树和传入树的_root
。 - 最后返回当前对象的引用。
3. 红黑树实现细节
3.1 RBTreeNode
结构体
首先,红黑树的每个节点需要包含以下信息:
- 颜色:红色或黑色,用于确保树的平衡。
- 左右子节点:用于表示该节点在树中的位置。
- 父节点:指向当前节点的父节点,用于在插入和删除时维护树的结构。
- 数据:存储节点的实际数据。
template<class T> struct RBTreeNode { RBTreeNode<T>* _left; RBTreeNode<T>* _right; RBTreeNode<T>* _parent; T _data; Colour _col; RBTreeNode(const T& data) : _left(nullptr), _right(nullptr), _parent(nullptr), _data(data), _col(RED) {} };
有人可能会疑问了,这里怎么会有一个T类型的 _data呢?我们为什么要这么设计呢,其实通过前文我们可以清晰的知道map和set是两种不同的容器,一个是用来存放(key,value)的键值对pair,而我们的set只存放key,难道传参过来的时候我们要为map和set单独设计一份插入函数吗,单独设计一份查找函数吗,那我们还封装他干嘛呢,干脆各自实现各自的。因此,为了极大的提高效率,我们采用模板的方式,T可以是任何类型,它既可以是map里的pair类型,也可以是set里的key类型,这样我们接受参数的时候,只需要一个参数就能接受,同时,因为在处理map其他函数的同时或多或少都需要用到它的key和它的value,而我们的set只需要一个key,所以我们选择了将set的参数也设置成pair,只不过不是(key,value),而是(key,key),这样无论取哪个参数都是key,这样就满足了map和set同时使用一个数据结构。
3.2 RBTreeIterator
迭代器
为了支持 map
和 set
的遍历,我们需要定义一个迭代器类型。RBTreeIterator
需要支持以下操作:
- 解引用操作符(
operator*
)和箭头操作符(operator->
),让我们能够访问节点中的数据。 - 不等操作符(
operator!=
),用于比较两个迭代器是否指向同一个节点。 - 自增操作符(
operator++
),用于迭代到下一个节点。template<class T, class Ref, class Ptr> struct __RBTreeIterator { typedef RBTreeNode<T> Node; typedef __RBTreeIterator<T, Ref, Ptr> Self; Node* _node; __RBTreeIterator(Node* node) : _node(node) {} Ref operator*() { return _node->_data; } Ptr operator->() { return &_node->_data; } bool operator!=(const Self& s) { return _node != s._node; } Self& operator++() { if (_node->_right) { Node* leftMin = _node->_right; while (leftMin->_left) { leftMin = leftMin->_left; } _node = leftMin; } else { Node* cur = _node; Node* parent = cur->_parent; while (parent && cur == parent->_right) { cur = parent; parent = parent->_parent; } _node = parent; } return *this; } };
这里的operator++是如何操作的呢?
首先,我们在使用++操作遍历时,一定处于某个迭代器位置,在当前root下,我们需要完成中序遍历,既然要++,假设我当前位于这颗树的最小节点,最左侧,此时要找寻下一个节点也就是第二小的,是不是要先从我的右子树找起,从右子树里找最左侧也就是最小节点,如果此时右子树为空,代表我当前子树已经找完了,那么此时就需要一直向上遍历,找到唯一一个是父亲左侧节点的节点,那么这个节点才是真正的下一个节点。
4. 封装 map
和 set
4.1 封装 map
我们可以通过封装 RBTree
来实现 map
。map
需要存储 key-value
对,并提供插入、查找、访问等操作。红黑树中的每个节点存储的是 pair<key, value>
,并且根据 key
进行排序。
template<class K, class V>
class map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
typedef typename RBTree<K, const K, MapKeyOfT>::ConstIterator const_iterator;
const_iterator begin() const
{
return _t.Begin();
}
const_iterator end() const
{
return _t.End();
}
iterator begin()
{
return _t.Begin();
}
iterator end()
{
return _t.End();
}
iterator find(const K& key)
{
return _t.Find(key);
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _t.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _t.Insert(make_pair(key, V()));
return ret.first->second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
4.2 封装 set
set
与 map
相似,但只存储键值,因此我们将红黑树节点存储的类型修改为 K
,
template<class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;
const_iterator begin() const
{
return _t.Begin();
}
const_iterator end() const
{
return _t.End();
}
iterator begin()
{
return _t.Begin();
}
iterator end()
{
return _t.End();
}
iterator find(const K& key)
{
return _t.Find(key);
}
pair<iterator, bool> insert(const K& key)
{
return _t.Insert(key);
}
private:
RBTree<K, const K, SetKeyOfT> _t;
};
在本文中,我们详细讲解了如何通过封装红黑树来实现 map
和 set
。通过使用红黑树作为底层数据结构,我们能够确保这两种容器具有 O(log n)
时间复杂度的插入、查找和删除操作。我们还深入分析了红黑树的节点结构、旋转操作、插入修复和迭代器实现等细节。希望通过本篇博客,读者能够理解如何将复杂的数据结构如红黑树有效地封装到容器类中,从而实现高效且灵活的 map
和 set
。