简介
关于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