简介
关于map和set的介绍和红黑树,之前博客都有介绍。
- map和set:C++ STL -->set和map的使用
- 红黑树:红黑树(RB-Tree)
这篇文章要用一棵红黑树同时封装出set和map,主要利用泛型编程的思想来完成。 之前博客实现的红黑树是K,V模型的,而set是K模型,map是K,V模型。(K模型和KV模型,是二叉搜索树的两个主要应用的两个大模型。关于K和KV模型,在二叉搜索树的最后应用场景有介绍:二叉搜索树(BST))
要用同一棵红黑树来封装map和set,使用模板参数来确定树中存放的是K还是KV模型。 对之前的红黑树进行修改如下:
(删除了验证红黑树相关成员函数,添加了析构和查找函数)
红黑树源码
cpp
enum Color
{
RED,
BLACK
};
template <class T>
struct RBTreeNode
{
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
T _data;//存储元素
Color _color; //使用枚举值定义结点的颜色
RBTreeNode(const T& data)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_data(data)
,_color(RED)
{}
};
template <class K, class T>
class RBTree
{
public:
typedef RBTreeNode<T> Node;
bool insert(const T& data)
{
//空树直接做为根结点
if (_root == nullptr)
{
_root = new Node(data);
_root->_color = BLACK;
return true;
}
//1、 确定插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (data < cur->_data)
{
parent = cur;
cur = cur->_left;
}
else if (data > cur->_data)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;//键值冗余不允许插入
}
}
//2、进行链接
cur = new Node(data);
if (data < parent->_data)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//3、若插入结点的父结点是红色的,则需要对红黑树进行调整
while (parent != nullptr && parent->_color == RED)
{
Node* grandfahter = parent->_parent; //parent为红色,grandfahter一定存在
if (grandfahter->_left == parent) //parent是grandfather左孩子的情况
{
Node* uncle = grandfahter->_right;//uncle若存在,一定是其右孩子
if (uncle != nullptr && uncle->_color == RED)//情况一:u存在且为红
{
//颜色调整
parent->_color = BLACK;
uncle->_color = BLACK;
grandfahter->_color = RED;
//继续向上调整
cur = grandfahter;
parent = cur->_parent;
}
else //情况2+3(u不存在/u存在且为黑)
{
//cur是parent的左
/* g
* p u
* c
*/
if (cur == parent->_left)
{
//右旋
RotateR(grandfahter);
//更新颜色
parent->_color = BLACK;
grandfahter->_color = RED;
}
else//cur是parent的右
{
/* g
* p u
* c
*/
//左右双旋(先以p为旋点左旋,在以g为旋点右旋)
RotateL(parent);
RotateR(grandfahter);
// cur变黑,g变红
cur->_color = BLACK;
grandfahter->_color = RED;
}
break;
}
}
else //parent是grandfather的右孩子
{
Node* uncle = grandfahter->_left; //uncle若存在一定是其左孩子
if (uncle != nullptr && uncle->_color == RED)//u存在且为红
{
//颜色调整
parent->_color = BLACK;
uncle->_color = BLACK;
grandfahter->_color = RED;
//继续向上调整
cur = grandfahter;
parent = cur->_parent;
}
else//u不存在/u存在为黑
{
//cur是parent的右
/* g
* u p
* c
*/
if (cur == parent->_right)
{
//左旋
RotateL(grandfahter);
// p变黑,g变红
parent->_color = BLACK;
grandfahter->_color = RED;
}
else
{
//cur是parent的左
/* g
* u p
* c
*/
//右左双旋(先以p为轴点右旋,再以g为轴点左旋)
RotateR(parent);
RotateL(grandfahter);
// cur变黑,g变红
cur->_color = BLACK;
grandfahter->_color = RED;
}
break;
}
}
}
//根节点一定为黑
_root->_color = BLACK;
return true;
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parent_parent = parent->_parent;
//让subRL结点作为parent结点的右子树 更新完之后处理subRL_parent;
parent->_right = subRL;
if (subRL != nullptr)
{
subRL->_parent = parent;
}
//让parnet做为subR的左子树 更新完之后处理parent的_parent
subR->_left = parent;
parent->_parent = subR;
//subR做为这颗最小不平衡子树的根节点
if (parent_parent == nullptr)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subR;
}
else
{
parent_parent->_right = subR;
}
subR->_parent = parent_parent;
}
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parent_parent = parent->_parent;
//让subLR节点做为parent节点的左子树 更新完之后处理subLR的_parent;
parent->_left = subLR;
if (subLR != nullptr)
{
subLR->_parent = parent;
}
//让parent节点做为subL的右子树 更新完之后处理parent的_parent
subL->_right = parent;
parent->_parent = subL;
//让这颗最小不平衡子树的parent节点做为subL的右子树
if (parent_parent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent_parent->_left == parent)
{
parent_parent->_left = subL;
}
else
{
parent_parent->_right = subL;
}
subL->_parent = parent_parent;
}
}
private:
Node* _root = nullptr;
};
利用模板参数用一棵红黑树同时封装出map和set
将红黑树的第二个模板参数修改为T,通过set和map实例化时确认,如果是set则实例化为Key,如果是map则实例化为pair
- set
cpp
namespace ding
{
template<class K>
class set
{
public:
//set提供的方法...
private:
RBTree<K, K> _tree;实例化为K模型
};
}
- map
cpp
namespace ding
{
template<class K, class V>
class map
{
public:
//map提供的方法...
private:
RBTree<K, pair<const K,V>> _tree;//实例化KV模型(即pair键值对)
};
}
具体实例化如下: 然后红黑树中的节点类,根据模板参数T来确定节点中存放K还是pair
cpp
template <class T>
struct RBTreeNode
{
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
T _data;//存储元素
Color _color; //使用枚举值定义结点的颜色
RBTreeNode(const T& data)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_data(data)
,_color(RED)
{}
};
红黑树中插入元素时,需要通过比较逻辑来确定最终插入位置,而map中的第二个模板参数pair的比较规则不符合map的比较规则
pair的比较规则单拿小于来说,first或second中有一个小就小。这很明显不符合map的比较规则。
- map的比较规则
map是用pair中的first进行比较的。所以,在封装时还要考虑比较的规则
利用仿函数进行比较
对于set来说,无所谓,模板参数都是K,直接用来比较即可,但是对于map来说,需要键值对pair中的first来进行比较,而pair的比较规则又不满足比较规则。所以这里利用仿函数和模板参数来解决这一问题
- map
map仿函数主要返回pair的first用来进行比较
cpp
namespace ding
{
template<class K, class V>
class map
{
//仿函数
class MapKeyofT
{
public:
const K& operator()(const pair<const K, V>& kv )
{
return kv.first;
}
};
public:
//map提供的方法...
private:
RBTree<K, pair<const K,V>, MapKeyofT> _tree;
};
}
- set
set的仿函数可有可无,但是为了和map使用同一棵红黑树,也要提供这个仿函数做为红黑树的第三个模板参数。
cpp
namespace ding
{
template<class K>
class set
{
//仿函数
class SetKeyofT
{
public:
const K& operator()(const K& key)
{
return key;
}
};
public:
//set提供的方法...
private:
RBTree<K, K, SetKeyofT> _tree;//实例化为K模型
};
}
红黑树此时就需要第三个模板参数KeyofT,就是为了拿到map中pair的first进行比较。 这里以find为例子(需要比较的地方都需要用仿函数对象来进行比较):
cpp
template <class K, class T, class KeyofT>
class RBTree
{
public:
typedef RBTreeNode<T> Node;
Node* Find(const K& key)
{
Node* cur = _root;
while (cur != nullptr)
{
if (_kot(cur->_data) > key)
{
cur = cur->_left;
}
else if (_kot(cur->_data) < key)
{
cur = cur->_right;
}
else
{
return cur;
}
}
return nullptr;
}
Node* _root = nullptr;
KeyofT _kot;//仿函数对象
};
具体实例化过程如下: 以上就是map和set的基本框架了。
迭代器
map和set的迭代器封装了红黑树的迭代器,而红黑树的迭代器是对红黑树节点指针的封装。
红黑树的迭代器设计和list的迭代器设计基本一样,list的迭代器在这前博客中有详细的介绍:C++ STL -->list模拟实现
- 第一个模板参数T:数据类型(int,char,string等)
- 第二个模板参数Ref:数据类型的引用,即T&
- 第三个模板参数Ptr:数据类型的指针,即T*
cpp
template<class T, class Ref,class Ptr>
struct RBTreeIterator
{
typedef RBTreeNode<T> Node;
typedef RBTreeIterator< T, Ref, Ptr> Self;
//构造函数
RBTreeIterator(Node* node);
Ref operator*();
Ptr operator->();
bool operator!=(const Self& s);
Self operator++();
Self operator++(int);//后置++重载
Self operator--();
Self operator--(int);//后置--重载
Node* _node;
};
构造函数
迭代器就是对结点指针进行封装,这里只需要一个结点指针成员变量初始化即可。
cpp
RBTreeIterator(Node* node)
:_node(node)
{}
*运算符重载
解引用操作符,是想拿到地址的内容,直接返回当前结点的数据内容引用即可。 返回值是Ref即T&。
cpp
Ref operator*()
{
return _node->_data;
}
->运算符重载
->返回值是指针类型Ptr即T*,这里直接返回对应结点数据的指针即可。
cpp
Ptr operator->()
{
return &(_node->_data);
}
前置++运算符重载
红黑树的迭代器++,根据红黑树中序遍历序列找到当前结点的下一个结点。 比如下面红黑树: 如果迭代器it的位置在1处,经过++it后,下一个结点是6,在经过++it后,就是8。 具体的逻辑如下:
-
如果当前结点的右子树不为空,++后的结点是右子树最左结点
-
如果当前结点的右子树为空,++后的结点是其父节点的父结点,并且孩子节点是父结点的右孩子。比如下面这颗红黑树,迭代器位置在11时,++it后,下一个节点就是13。
cpp
Self operator++()
{
Node* cur = _node;
if (cur->_right != nullptr)
{
//找最右子树的最左节点
Node* subRight = cur->_right;
while (subRight != nullptr && subRight->_left != nullptr)
{
subRight = subRight->_left;
}
_node = subRight;
}
else
{
//如果当前结点的右子树为空,++后的结点是其父节点,并且孩子节点是父结点的左孩子
//如果
Node* parent = cur->_parent;
while (parent != nullptr && cur == parent->_right )
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
前置--运算符重载
一个正向迭代器进行--操作时,应该根据红黑树中序遍历的序列找到当前结点的前一个结点。 比如下面这棵红黑树: 如果迭代器it的位置在27,经过--之后,前一个节点是25,在经过--it之后,前一个结点是22,在经过--it后,前一个结点是17。
- 如果当前结点的左子树不为空,则
--
操作后应该找到其左子树当中的最右结点。 - 如果当前结点的左子树为空,则
--
操作后应该在该结点的祖先结点中,找到孩子不在父亲左的祖先。
cpp
Self operator--()
{
Node* cur = _node;
if (cur->_left != nullptr)
{
Node* subLeft = cur->_left;
while (subLeft != nullptr && subLeft->_right != nullptr)
{
subLeft = subLeft->_right;
}
_node = subLeft;
}
else
{
Node* parent = cur->_parent;
while (parent != nullptr && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
!=运算符重载
双目运算符,两个迭代器类型对象进行比较。
cpp
bool operator!=(const Self& s)
{
return _node != s._node;
}
具体实例化过程:
map的封装
map的[ ]运算符重载
STL源码中,map提供了[ ]运算符,函数原型为:
cpp
mapped_type& operator[] (const key_type& k);
[ ]的参数就是一个键值。 [ ]的返回值是maaped_type的引用。 这里的maaped_type就是上面封装map的第二个模板参数V。
[ ]运算符重载主要依靠insert函数。
operator[]的原理是: 用构造一个键值对,然后调用insert()函数将该键值对插入到map中 如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器 如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器 operator[]函数最后将insert返回值键值对中的value返回
这里还要修改红黑树insert的返回值如下:
cpp
pair<itertaor, bool> insert(const T& data)
{
//空树直接做为根结点
if (_root == nullptr)
{
_root = new Node(data);
_root->_color = BLACK;
return make_pair(iterator(_root),true);
}
//1、 确定插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (_kot(data) < _kot(cur->_data))
{
parent = cur;
cur = cur->_left;
}
else if (_kot(data) > _kot(cur->_data))
{
parent = cur;
cur = cur->_right;
}
else
{
return make_pair(iterator(cur), false);;//键值冗余不允许插入
}
}
//2、进行链接
cur = new Node(data);
Node* newnode = cur;
if (_kot(data) < _kot(parent->_data))
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//3、若插入结点的父结点是红色的,则需要对红黑树进行调整
//....
return make_pair(iterator(newnode),true);
}
operator[]的实现如下:
cpp
pair<iterator,bool> insert(const pair<K, V>& kv)
{
return _tree.insert(kv);
}
V& operator[](const K& key)
{
//1、调用insert函数插入键值对 ret键值对接收insert返回值
pair<iterator, bool> ret = insert(make_pair(key, V());
//2.拿到插入元素的迭代器
iterator it = ret.first;
//3.返回迭代器位置的实值的引用
return it->second;
}
封装后的map
cpp
namespace ding
{
template<class K, class V>
class map
{
//仿函数
class MapKeyofT
{
public:
const K& operator()(const pair<const K, V>& kv )
{
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::iterator iterator;
typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::const_iterator const_iterator;
pair<iterator,bool> insert(const pair<K, V>& kv)
{
return _tree.insert(kv);
}
V& operator[](const K& key)
{
//1、调用insert函数插入键值对 ret键值对接收insert返回值
pair<iterator, bool> ret = insert(make_pair(key, V()));
//2.拿到插入元素的迭代器
iterator it = ret.first;
//3.返回迭代器位置的实值的引用
return it->second;
}
iterator find(const K& key)
{
return _tree.Find(key);
}
iterator begin()
{
return _tree.begin();
}
iterator end()
{
return _tree.end();
}
private:
RBTree<K, pair<const K, V>, MapKeyofT> _tree;
};
}
set的封装
cpp
namespace ding
{
template<class K>
class set
{
//仿函数
class SetKeyofT
{
public:
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator;
typedef typename RBTree<K, K, SetKeyofT>::const_iterator const_iterator;
//set提供的方法...
iterator begin()
{
return _tree.begin();
}
iterator end()
{
return _tree.end();
}
pair<iterator,bool> insert(const K& key)
{
return _tree.insert(key);
}
iterator find(const K& key)
{
return _tree.Find(key);
}
private:
RBTree<K, K, SetKeyofT> _tree;//实例化为K模型
};
}
set的规则
STL库中不能通过set的迭代器改变set元素的值,因为set的元素值就是其键值。如果修改set元素的值,会破坏set的排序规则。
但是上面封装的set是可以通过迭代器修改set元素的值,STL源码在设计时,将普通迭代器也设计成为了cosnt迭代器。这样就不能通过set的迭代器来改变set的值。
上面set的封装第16行修改为:
cpp
typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator;
这样修改完后,还是会有问题对于下面代码:
cpp
void test_set()
{
set<int> s1;
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto& e : a)
{
s1.insert(e);
}
auto it = s1.begin();//拿到set第一个元素的迭代器位置
cout << *it << endl;
}
出现了这样的报错信息:
这里因为set里面的红黑树对象是非const对象,让非const对象去调用红黑树类的成员函数,会调用非const版本的beign,红黑树类非const版本的begin和end函数返回值是iterator,而set中的iterator本质是一个const_iterator。所以会导致无法从非const转换为const的错误。
这种情况第一时间想到的是将红黑树类中的非const版本的begin和end删除,只留下const版本的begin和end。 这种做法针对set是完全可以的,但是map是可以修改键值对中的V的。而set和map共用一棵红黑树,这样做就会导致map无法修改。
只能另寻他路。 STL源码大佬的做法是在迭代器类中提供一个构造函数,如下:
cpp
template<class T, class Ref,class Ptr>
struct RBTreeIterator
{
typedef RBTreeIterator<T, T&, T*> iterator;
typedef RBTreeIterator<T, const T&, const T*> const_iterator;
RBTreeIterator(const iterator& it)
:_node(it._node)
{}
//...
}
- 这个构造函数如果是非cosnt版本的迭代器就是拷贝构造函数
类模板实例化为iterator,他就是一个拷贝构造函数
函数原型如下:
cpp
RBTreeIterator(const RBTreeIterator<T, T&, T*>& it)
:_node(it._node)
{}
完全符合拷贝构造函数的性质。
- 如果是const版本的迭代器就是一个支持用iterator构造初始化为const_iterator的构造函数。(
这里还涉及到单参数的构造函数支持隐式类型转化,可以将非cosnt的iterator隐式转换为cosnt_iterator
)
函数原型如下:
cpp
RBTreeIterator(const RBTreeIterator<T, const T&, const T*>& it)
:_node(it._node)
{}
不符合拷贝构造函数的要求,可以当作是将iterator类型初始化为const_iterator类型的构造函数。
这样就可在红黑树类中的begin或end函数return时调用这个构造函数,将iterator转换为const_iterator类型。从而限制了通过迭代器修改set中的元素。
参考源码
完整源码在gitee上。
- gitee 用一颗红黑树封装出map和set
- 菜鸟一枚,写的不好的地方请各位大佬多多包涵,手下留情。