红黑树,这种平衡二叉搜索树以其独特的颜色标记和平衡机制,成为实现 map 和 set 等容器的核心。它在保证有序性和高效性之间取得了微妙的平衡,为C++标准库带来了无与伦比的查找效率。本篇博客将带你走进红黑树的世界,从原理到实现,揭开其在 map 和 set 中的应用奥秘。
本篇文章将深入探讨如何使用红黑树实现自定义的 Map
和 Set
数据结构,目的是帮助读者理解标准库中 map
和 set
容器的底层机制。通过实现这些容器,我们可以在掌握红黑树等底层数据结构的同时,设计和构建出符合特定需求的容器。本文将逐步介绍如何改造红黑树、设计红黑树迭代器,并基于此实现 Set
和 Map
两个容器。
红黑树是一种平衡二叉树,主要用于实现关联数据结构(如 map
和 set
)。它满足二叉搜索树的特性,并通过节点的颜色属性及一系列平衡操作,确保树的平衡性。标准的红黑树需要满足以下规则:
nullptr
)是黑色。在红黑树的实现中,每个节点包含一个 Color
属性用于指示节点颜色,且需要在插入和删除节点时进行颜色检查和旋转操作以保持树的平衡。具体实现如下:
enum Color {
RED,
BLACK
};
template <class T>
class RBTreeNode {
public:
RBTreeNode<T>* left;
RBTreeNode<T>* right;
RBTreeNode<T>* _parent;
T _data;
Color _col;
RBTreeNode(const T& data)
: left(nullptr), right(nullptr), _parent(nullptr), _data(data), _col(RED) {}
};
红黑树的插入过程比普通二叉搜索树复杂,需要保持平衡。在插入节点后,根据节点颜色和结构调整,具体调整规则如下:
pair<Iterator, bool> insert(const T& data) {
if (!_root) {
_root = new Node(data);
_root->_col = BLACK;
return make_pair(Iterator(_root), true);
}
// 定义当前节点cur
Node* cur = _root;
Node* parent = nullptr;
// 仿函数用来索引key
KeyOfT kot;
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); // 不可能出现相等的情况插入
}
// 创建新的Node实例cur,并将它插入到适当的位置
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;
// 更新结点颜色,控制平衡
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
// 父亲是祖父的左孩子
if (grandfather && 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 if (grandfather && parent == grandfather->right) {
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->right) {
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else {
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return make_pair(Iterator(newnode), true);
}
红黑树的迭代器设计与其他容器的迭代器类似,需要实现 operator++
和 operator--
操作符,以支持顺序访问节点。迭代器在容器的遍历、修改等操作中扮演重要角色。在红黑树的 Map
和 Set
容器中,迭代器不仅用于遍历节点,还用于检查元素是否存在,因此设计合理的迭代器十分关键。
迭代器设计的核心是封装对节点的操作,使用户可以通过迭代器透明地访问节点数据。红黑树的节点以二叉树结构相连,因此迭代器可以通过节点之间的相对关系进行遍历。对于红黑树,我们需要设计如下迭代器类:
template<class T, class Ref, class Ptr>
class __TreeIterator {
public:
typedef RBTreeNode<T> Node;
typedef __TreeIterator<T, Ref, Ptr> Self;
typedef __TreeIterator<T, T&, T*> Iterator;
Node* node;
// 如果迭代器是const,则为构造函数,否则为拷贝构造函数
__TreeIterator(const Iterator& it) : node(it._node) {}
__TreeIterator(Node* node = nullptr) : node(node) {}
Ref operator*() { return node->data; }
Ptr operator->() { return &(node->data); }
bool operator!=(const Self& other) const { return node != other.node; }
Self& operator++();
Self& operator--();
};
以上迭代器类提供了基本的访问接口,包括解引用(*
)、成员访问(->
)以及不等比较操作符(!=
),方便用户直接使用迭代器访问数据。此外,++
和 --
操作符实现了顺序访问和逆序访问功能。
begin()
与 end()
的实现为了在 Set
和 Map
中使用迭代器遍历红黑树,我们需要实现 begin()
和 end()
。begin()
返回红黑树的最左节点,即序列中的第一个节点;end()
则返回空迭代器,表示遍历结束。
以下迭代器需要在 RBTree
类中实现,方便map&set的使用。
typedef __TreeIterator<T, T&, T*> Iterator;
typedef __TreeIterator<T, const T&, const T*> const_Iterator;
Iterator begin() {
Node* cur = root;
while (cur && cur->left) {
cur = cur->left;
}
return Iterator(cur);
}
Iterator end() {
return Iterator(nullptr);
}
const_Iterator begin() const {
Node* cur = _root;
while (cur && cur->left) {
cur = cur->left;
}
return const_Iterator(cur);
}
const_Iterator end() const {
return const_Iterator(nullptr);
}
begin()
和 end()
的实现确保用户可以通过常见的迭代器接口遍历红黑树中的所有节点。
operator++()
与 operator--()
的实现operator++
和 operator--
是迭代器设计的关键,它们分别用于遍历红黑树的后继节点和前驱节点。operator++
的实现逻辑如下:
Self& operator++() {
if (node->right) {
node = node->right;
while (node->left) {
node = node->left;
}
} else {
Node* parent = node->parent;
while (parent && node == parent->right) {
node = parent;
parent = parent->parent;
}
node = parent;
}
return *this;
}
operator--
的实现与 operator++
类似,只是方向相反:
Self& operator--() {
if (_node->left) {
Node* cur = _node->left;
while (cur->right) {
cur = cur->right;
}
_node = cur;
}
else {
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->left) {
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
通过实现 operator++
和 operator--
,我们可以通过迭代器在红黑树中自由前后遍历。
Set
的模拟实现在 MySet.h
文件中,Set
使用红黑树 RBTree
作为底层数据结构来实现集合。红黑树的自平衡特性保证了集合元素的有序性,且插入、删除、查找操作的时间复杂度为
。Set
的实现不允许重复元素,因此当插入新元素时,红黑树会判断该元素是否已存在,若存在则不插入。
首先定义了一个 SetKeyOfT
仿函数类,它的作用是从集合中的元素提取出键值。在 Set
中,每个元素即为集合的键,因此 SetKeyOfT
直接返回元素本身。
class SetKeyOfT {
public:
const K& operator()(const K& key){
return key;
}
};
然后,我们通过 RBTree
实现了 Set
容器的主要功能,包括 begin()
、end()
和 insert()
方法:
template<class K>
class set{
public:
// typename告诉编译器这是类型,
// 因为set不能够进行修改,所以我们用const迭代器来初始化普通迭代器,来达到不能修改的目的
typedef typename RBTree<K, K, SetKeyOfT>::const_Iterator Iterator;
typedef typename RBTree<K, K, SetKeyOfT>::const_Iterator const_Iterator;
Iterator begin() const {
return _t.begin();
}
Iterator end() const {
return _t.end();
}
pair<Iterator, bool> insert(const K& key) {
pair<typename RBTree<K, K, SetKeyOfT>::Iterator, bool> ret = _t.insert(key);
return pair<Iterator, bool>(ret.first, ret.second);
}
private:
RBTree<K, K, SetKeyOfT> _t;
};
Set
的 insert()
方法在 Set
的 insert()
方法中,调用了 _t.insert(key)
来向红黑树 _t
插入一个元素。RBTree
的 insert()
方法会自动处理元素是否重复的问题,因此若尝试插入已存在的元素,红黑树会拒绝插入。插入方法返回一个 pair
,包含:
Iterator
:指向插入位置的迭代器。bool
:表示是否成功插入,若插入成功为 true
,否则为 false
(即元素已存在)。Map
的模拟实现首先,定义了一个 MapKeyOfT
仿函数类,用于从 Map
的元素(即键值对 pair<K, V>
)中提取键 K
。RBTree
使用该仿函数来确定键值对在树中的位置。
class MapKeyOfT {
public:
const K& operator()(const pair<K, V>& kv) {
return kv.first;
}
};
接下来,我们使用 RBTree
实现 Map
的主要功能,包括 begin()
、end()
、insert()
和 operator[]
方法:
template<class K,class V>
class map {
public:
// map是一个key不能修改,value能够修改的结构
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator Iterator;
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::const_Iterator const_Iterator;
Iterator begin() {
return _t.begin();
}
Iterator end() {
return _t.end();
}
const_Iterator begin() const{
return _t.begin();
}
const_Iterator end() const{
return _t.end();
}
pair<Iterator, bool> insert(const pair<K, V>& kv) {
return _t.insert(kv);
}
// 重载map中的operatorp[]
// operatorp[] 当原数据中没有时,插入并初始化,有则修改second
V& operator[](const K& key) {
pair<Iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
insert()
方法Map
的 insert()
方法用于将键值对插入到 Map
中。其调用 _t.insert(kv)
来将 kv
插入到红黑树 _t
中。insert()
方法返回一个 pair
,包含:
Iterator
:指向插入位置的迭代器。bool
:表示是否成功插入,若插入成功为 true
,否则为 false
(即键已存在)。operator[]
重载operator[]
提供了通过键访问值的方式。如果键不存在,operator[]
会将键值对插入 Map
,并将值初始化为默认值 V()
;如果键已存在,则直接返回对应值的引用。
V& operator[](const K& key) {
pair<Iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
该实现方式确保了 Map
的键值对在访问时自动插入,并支持直接通过键来访问和修改值。
红黑树的美在于平衡,数据在红与黑的交替间流转,让查找、插入、删除都化繁为简。掌握了红黑树,你将不再畏惧复杂的数据结构,而是能够在数据的森林中自如穿行。愿这篇博客为你的算法之路增添一份优雅与从容。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!