前言:
在本篇文章中,我们将为大家讲述map set这两种容器,map和set是STL容器中比较常见的两个,他们的核心在于使用了KV模型,这样更进一步让我们可以对数据进行存储,同时由于底层使用了红黑树进行封装,因此在搜索效率上得到了极大的保障,但是两者各有各的特点,下面就让我们来看一看。
关联式容器:
在STL库中,我们对于容器一般分为两类:1.序列式容器 2.关联式容器
序列式容器:
比如list,vector,string,queue,stack,deque等等,这是因为他们底层为线性序列的数据结构,里面存储的是元素本身,注意这里我要强调的东西,他们存储的是元素本身,也就是说,你存储的如果是整形那就是整形,是其他类型就是其他类型。
那什么是关联式容器呢?
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,我们也称为KV模型,在数据检索时比序列式容器效率更高.
既然谈到<key,value>键值对,那这个所谓的键值对是什么呢?
键值对:
键值对的定义:
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息
其官方定义如下:
template <class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair(): first(T1()), second(T2())
{}
pair(const T1& a, const T2& b): first(a), second(b)
{}
};
我们从上述的定义可以看出来,这里的pair就是键值对<first,second>的标志,同时键值对的本质是一个类,因此我们访问键值对的数据的时候,就可以按照访问类的成员那样访问即可。
比如:我们去超市买水果的时候,每一种水果都有其特定的标价,作为管理者来说,我们想要快速获取任意一种水果对应的标价,我们就可以利用一个键值对,将水果的名称作为检索对象key,而对应的价格就是数据value。因此通过这种存储结构,我们只需要找到对应的水果名称pair.first,就可以访问到它对应的价格pair.second。
set容器:
在STL中,数据按照管理的大体结构主要分为两种,即哈希结构和树形结构,而包括set在内的下面的容器都是树形结构。
1.set的介绍:
1. set是按照一定次序存储元素的容器
2. 在set中,元素的value也标识它(value就是key,类型为T),set的pair结构为pair<key,key>,并且每个value必须是唯一的。
set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行
排序。
4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对
子集进行直接迭代。
5. set在底层是用二叉搜索树(红黑树)实现的。
我们需要注意的问题如下:
1. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。
2. set中插入元素时,只需要插入value即可,不需要构造键值对。
3. set中的元素不可以重复(因此可以使用set进行去重)。!!!!
4. 使用set的迭代器遍历set中的元素,可以得到有序序列,这得益于红黑树是一种平衡版本的BS树
5. set中的元素默认按照小于来比较,我们从set的仿函数模板参数也可以看到,其默认给的都是Less< T >
6. set中查找某个元素,时间复杂度为:
l
o
g
2
n
log_2 n
log2n,树形结构的效率都是如此,而且这里还进行了平衡
7. set中的元素不允许修改(为什么?),而set的元素不允许修改的原因在于,set的value就是key,因此一旦对value修改很可能导致有重复的情况出现,因此set不允许修改。
8. set中的底层使用二叉搜索树(红黑树)来实现
2.set的使用:
第一部分:
首先是第一部分,我认为这部分没什么值得说的东西,都是作为STL容器比较基本的东西,我们在后面的模拟实现,在实现迭代器的时候再重点说这部分。
第二部分:
在这一部分中,我们想要谈一谈的是lower_bound和upper_bound以及equal_range.
1.lower_bound是用来返回我们对应的参数的距离最近的小于key元素的迭代器的,而upper_bound则反之为返回距离最近的大于参数的key元素的迭代器的。他们的组合用法如下:
#include <iostream>
#include <set>
int main ()
{
std::set<int> myset;
std::set<int>::iterator itlow,itup;
for (int i=1; i<10; i++) myset.insert(i*10); // 10 20 30 40 50 60 70 80 90
itlow=myset.lower_bound (30); // ^
itup=myset.upper_bound (60); // ^
myset.erase(itlow,itup); // 10 20 70 80 90
std::cout << "myset contains:";
for (std::set<int>::iterator it=myset.begin(); it!=myset.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
return 0;
}
//myset contains: 10 20 70 80 90
2.而对于equal_range来说:
它则是返回一个键值对,其first即lower_bound,而其second即upper_bound,用法如下:
#include <iostream>
#include <set>
int main ()
{
std::set<int> myset;
for (int i=1; i<=5; i++) myset.insert(i*10); // myset: 10 20 30 40 50
std::pair<std::set<int>::const_iterator,std::set<int>::const_iterator> ret;
ret = myset.equal_range(30);
std::cout << "the lower bound points to: " << *ret.first << '\n';
std::cout << "the upper bound points to: " << *ret.second << '\n';
return 0;
}
//the lower bound points to: 30
//the upper bound points to: 40
注意,我们使用set是可以去重的,比如如下:
#include <set>
void TestSet()
{
// 用数组array中的元素构造set
int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4,
6, 8, 0 };
set<int> s(array, array+sizeof(array)/sizeof(array));
cout << s.size() << endl;
// 正向打印set中的元素,从打印结果中可以看出:set可去重
for (auto& e : s)
cout << e << " ";
cout << endl;
// 使用迭代器逆向打印set中的元素
for (auto it = s.rbegin(); it != s.rend(); ++it)
cout << *it << " ";
cout << endl;
// set中值为3的元素出现了几次
cout << s.count(3) << endl; }
这样我们就可以将数组中重复的元素全部去掉,同时得到一个排列顺序好的数据结构。
map容器:
map的容器底层也是红黑树,在大体的结构上它和set有很大的相似之处。
1.map的介绍:
1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
2. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair: typedef pair<const key, T> value_type;
3. 在内部,map中的元素总是按照键值key进行比较排序的。
4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
5. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。这也是因为得益于key唯一且不可修改的特点,unordered_map就不能实现这个功能。
6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。
2.map的使用:
在对于map的使用中,我们不需要进行过多的赘述,很多函数对应的用法和set是一样的,但注意map是真正的KV模型,key用来检索和排序,而value则存储真正的数据。
在这些函数接口中,我注意到[]运算符重载和at:
【】和at实际上是同一个意思,用法相同,只不过[]是将at的方法通过运算符重载变成我们熟悉的下表访问的形式,其用法如下:
#include <iostream>
#include <map>
#include <string>
int main ()
{
std::map<char,std::string> mymap;
mymap['a']="an element";
mymap['b']="another element";
mymap['c']=mymap['b'];
std::cout << "mymap['a'] is " << mymap['a'] << '\n';
std::cout << "mymap['b'] is " << mymap['b'] << '\n';
std::cout << "mymap['c'] is " << mymap['c'] << '\n';
std::cout << "mymap['d'] is " << mymap['d'] << '\n';
std::cout << "mymap now contains " << mymap.size() << " elements.\n";
return 0;
}
//mymap['a'] is an element
//mymap['b'] is another element
//mymap['c'] is another element
//mymap['d'] is
//mymap now contains 4 elements.
#include <iostream>
#include <string>
#include <map>
int main ()
{
std::map<std::string,int> mymap = {
{ "alpha", 0 },
{ "beta", 0 },
{ "gamma", 0 } };
mymap.at("alpha") = 10;
mymap.at("beta") = 20;
mymap.at("gamma") = 30;
for (auto& x: mymap) {
std::cout << x.first << ": " << x.second << '\n';
}
return 0;
}
//alpha: 10
//beta: 20
//gamma: 30
但是在这里,我们要搞清楚operator[]函数的返回值以及功能实现:
operator[]的原理是:
用<key, T()>构造一个键值对,然后调用insert()函数将该键值对插入到map中 如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器 如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器,operator[]函数最后将insert返回值键值对中的value返回
因此,对于operator[]的返回值,我们可以使用起来,它是会返回一个迭代器的,同时也有插入的功能,我们在一会模拟实现map的时候,模拟实现operator[]利用的就是这个关键的原理。
set map容器的模拟实现:
在完善了上述的功能之后,接下来让我们通过模拟实现set map来进一步体会使用,通过模拟实现,我们会对模板的使用封装产生更深的理解。
1.底层红黑树的构建:
这一部分我不做过多的赘述,直接上代码,有不懂的地方可以参考我数据结构专栏的链接: 红黑树。
基本的红黑树结构在那里有详解,代码如下:
enum Colour//颜色枚举体
{
BLACK,
RED
};
template<class T>
struct RBTreeNode//红黑树节点类
{
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
Colour _col;
T _data;
RBTreeNode(const T& data)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _data(data)
, _col(RED)//控制节点颜色的成员变量
{}
};
template<class K, class T,class KeyOf>//利用一个仿函数来处理key的获取问题
class RBTree//红黑树类
{
public:
typedef struct RBTreeNode<T> Node;
typedef struct _Treeiterator<T,T&,T*> iterator;//迭代器
typedef struct _Treeiterator<T,const T&,const T*> const_iterator;//const 迭代器
iterator begin()//头位置的迭代器,即找到最左边的节点,但是注意不要找过头了,因此我们是cur&&cur->_left
{
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);//返回迭代器类的匿名构造
}
pair<Node*,bool> Insert(const T& data)//插入
{
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;
return make_pair(_root, true);
}
Node* cur = _root;
Node* parent = nullptr;
KeyOf key;//设置一个通用的仿函数来将对应的map和set中对应的key取出来,因为pair自己的first没法取,因此利用分别封装的类来返回取接收值
while (cur)
{
if (key(cur->_data) > key(data))
{
parent = cur;
cur = cur->_left;
}
else if (key(cur->_data) < key(data))
{
parent = cur;
cur = cur->_right;
}
else
{
return make_pair(cur, false);
}
}
//新增节点给红色,默认构造出来了
cur = new Node(data);
Node* newnode = cur;//由于存在更新的问题需要向上调整,因此cur不再是新插入的位置了,因此在调整之前我们先将其保存下来以防止找不到这个节点
if (key(parent->_data) > key(data))
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//插入完毕,开始红黑节点调整
while (parent && parent->_col == RED)//这里要加上parent的判断,否则会出现空指针访问报错,先检验parent是否指向空
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)//父亲在左边,即叔叔在右边的情况
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)//uncle不为空且为红色的情况下
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔节点为空或者黑色的情况
{
if (cur == parent->_left)//对cur的左右位置进行讨论,可能单旋,也可能双旋,这里为单旋
{
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->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;//这里如果不停止,则父子关系后续是全乱的,后续在循环就会出现大问题,而且由于改为黑色节点已经没必要再向上修改了,及时退出即可
}
}
}
//有可能最后的节点为grandfather,此时根结点为红色,但要求根节点为黑色,因此不管是否为根节点,我们都最后将root改为黑色,这样就处理了所有情况
_root->_col = BLACK;
return make_pair(newnode, true);
}
void RotateL(Node* parent)//左单旋旋转调整
{
Node* sub1 = parent->_right;
Node* sub2 = sub1->_left;
Node* PPparent = parent->_parent;
parent->_right = sub2;
if (sub2)//注意这里,可能涉及到sub2为空的情况,比如h==0,此时要对sub2是否为空进行一次判断
{
sub2->_parent = parent;
}
sub1->_left = parent;
parent->_parent = sub1;
if (_root == parent)
{
_root = sub1;
sub1->_parent = nullptr;
}
else
{
if (PPparent->_right == parent)
{
PPparent->_right = sub1;
}
else if (PPparent->_left == parent)
{
PPparent->_left = sub1;
}
sub1->_parent = PPparent;
}
}
void RotateR(Node* parent)//右单旋旋转调整
{
Node* sub1 = parent->_left;
Node* sub2 = sub1->_right;
Node* PPparent = parent->_parent;
parent->_left = sub2;
if (sub2)
{
sub2->_parent = parent;
}
sub1->_right = parent;
parent->_parent = sub1;
if (_root == parent)
{
_root = sub1;
sub1->_parent = nullptr;
}
else
{
if (PPparent->_right == parent)
{
PPparent->_right = sub1;
}
else if (PPparent->_left == parent)
{
PPparent->_left = sub1;
}
sub1->_parent = PPparent;
}
}
void InOrder()//中序遍历接口
{
_InOrder(_root);
cout << endl;
}
size_t size()//求节点个数
{
return _size(_root);
}
bool Check(Node* root, int blacknum, const int refVal)//节点检查函数,注意这里要传值返回而不是传地址,这是因为保证每一层的递归时blacknum的数据不变,保持原来的值,而是把之前的值传到下一个递归去,这样都相互不影响
{
if (root == nullptr)
{
if (blacknum != refVal)
{
cout << "存在黑色节点数量不相等的路径" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)//由于我们向下检查子节点的时候,子节点的情况太多不好检查,我们要检查两个节点是否为红,非常麻烦,所以我们采取向上检查的方式来检查子和父节点是否都为红色即可,因此我们反其道而行之向上检查,即检查当前节点和父节点是否都为红色,若都为红色则这个树不是红黑树
{
cout << "有连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
blacknum++;
}
return Check(root->_left, blacknum, refVal) && Check(root->_right, blacknum, refVal);
}
bool IsBalance()//判断是否为红黑树
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)
{
return false;
}
int refVal = 0;//黑色节点参考值,倘若不同就说明红黑树出现问题了,黑色节点不相同
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
refVal++;
}
cur = cur->_left;
}
int blacknum = 0;
return Check(_root, blacknum, refVal);
}
private:
Node* _root = nullptr;
void _InOrder(Node* root)//中序遍历
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
size_t _size(Node* root)//求节点个数
{
if (root == nullptr)
{
return 0;
}
return _size(root->_left) + _size(root->_right) + 1;
}
};
红黑树的基本结构不变,但我们为此做出了一些修改,由于红黑树同时满足了set 和 map的底层,因此我们统一将数据为class T,同时我们还增加了迭代器的内容:
typedef struct _Treeiterator<T,T&,T*> iterator;//迭代器
typedef struct _Treeiterator<T,const T&,const T*> const_iterator;//const 迭代器
这种常见的迭代器类,我们之前进行容器的模拟实现的时候都是采用这种方式,将T T& T拆分开三个部分传参,这样传参的目的是让我们的const_iterator和iterator两个迭代器共用一份代码,不需要重复写。另一个只要传入T const T& const T即可。
2.迭代器:
下面就是迭代器了,对于迭代器,我们最主要的就是对前置加加和前置减减进行处理。
前置++:
对于前置++,我们的代码如下:
Self& operator++()//前置++
{
if (_node->_right)
{
Node* cur = _node->_right;
while (cur->_left)
{
cur = cur->_left;
}
_node = cur;
}
else
{
Node* cur = _node;
Node* parent = _node->_parent;
while (parent && parent->_right == cur)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;//当parent为空时,说明整棵树走完了,直接返回空与end一同判定即可
}
return *this;
}
首先,我们首先要判断我们当前节点的右边有没有节点,如果有,那就向右向下走,直接走到右边节点的最左边,否则说明下面的节点都访问结束了,后续需要执行的便是向上走即可。
前置–:
前置–就是前置++的镜像,因此我不做过多赘述:
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 && parent->_left == cur)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
3.对于插入的改造:
我们不妨思考一下,由于模板的存在,我们如何统一让set返回first而map返回second呢?
按理来说这时不可能的,此时我们就要做进一步的封装,也就是模板套模板的形式,同时利用仿函数封装一个针对set和map分别处理的仿函数类来处理插入中数值的访问问题,如下:
struct SetKeyOf//仿函数类
{
const K& operator()(const K& key)
{
return key;
}
};
struct MapKeyOf//仿函数类,其实这个仿函数类对于set意义不大,因为set本身就可以返回key,它的主要作用是帮助map的pair来返回key使用的.
{
const K& operator()(const pair<K, T>& kv)
{
return kv.first;
}
};
有了这两个分别处理set和map的仿函数,我们就可以在模板中传入一个仿函数模板,如下:
template<class K, class T,class KeyOf>//利用一个仿函数来处理key的获取问题
这样在后续处理的过程中,我们在插入的时候就需要一个仿函数对象处理返回值,如下代码:
KeyOf key;//设置一个通用的仿函数来将对应的map和set中对应的key取出来,因为pair自己的first没法取,因此利用分别封装的类来返回取接收值
while (cur)
{
if (key(cur->_data) > key(data))
{
parent = cur;
cur = cur->_left;
}
else if (key(cur->_data) < key(data))
{
parent = cur;
cur = cur->_right;
}
else
{
return make_pair(cur, false);
}
}
比如在这里,我们将节点放入到对应的位置的循环中就先创建了一个仿函数对象处理数据访问的问题,这样不管是set还是map,都会根据模板选择对应的仿函数类,从而选择对应的数据返回方式,这是一种模板套模板,利用仿函数实现一种类似多态的方式。
同时,针对insert的返回方式,我们也要遵守STL文档给的,返回一个T数据类型和bool值的键值对,后续我们根据我们的set和map传入迭代器iterator即可。
插入代码如下:
pair<Node*,bool> Insert(const T& data)//插入
{
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;
return make_pair(_root, true);
}
Node* cur = _root;
Node* parent = nullptr;
KeyOf key;//设置一个通用的仿函数来将对应的map和set中对应的key取出来,因为pair自己的first没法取,因此利用分别封装的类来返回取接收值
while (cur)
{
if (key(cur->_data) > key(data))
{
parent = cur;
cur = cur->_left;
}
else if (key(cur->_data) < key(data))
{
parent = cur;
cur = cur->_right;
}
else
{
return make_pair(cur, false);
}
}
//新增节点给红色,默认构造出来了
cur = new Node(data);
Node* newnode = cur;//由于存在更新的问题需要向上调整,因此cur不再是新插入的位置了,因此在调整之前我们先将其保存下来以防止找不到这个节点
if (key(parent->_data) > key(data))
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//插入完毕,开始红黑节点调整
while (parent && parent->_col == RED)//这里要加上parent的判断,否则会出现空指针访问报错,先检验parent是否指向空
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)//父亲在左边,即叔叔在右边的情况
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)//uncle不为空且为红色的情况下
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔节点为空或者黑色的情况
{
if (cur == parent->_left)//对cur的左右位置进行讨论,可能单旋,也可能双旋,这里为单旋
{
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->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;//这里如果不停止,则父子关系后续是全乱的,后续在循环就会出现大问题,而且由于改为黑色节点已经没必要再向上修改了,及时退出即可
}
}
}
//有可能最后的节点为grandfather,此时根结点为红色,但要求根节点为黑色,因此不管是否为根节点,我们都最后将root改为黑色,这样就处理了所有情况
_root->_col = BLACK;
return make_pair(newnode, true);
}
4.底层之上的set和map类,套用红黑树接口函数:
代码如下:
#pragma once
#include"RBTree.h"
namespace hbw
{
template<class K>
class set
{
public:
struct SetKeyOf//仿函数类
{
const K& operator()(const K& key)
{
return key;
}
};
typedef typename RBTree<K, K, SetKeyOf>::const_iterator iterator;
typedef typename RBTree<K, K, SetKeyOf>::const_iterator const_iterator;//由于set就要求不能修改,因此不论是iterator还是const_iterator统一都使用const_iterator进行封装
//typedef struct _Treeiterator<K> iterator;
iterator begin() const
{
return _t.begin();
}
iterator end() const
{
return _t.end();
}
pair<iterator,bool> Insert(const K& key)//set插入
{
return _t.Insert(key);
}
private:
RBTree<K,K,SetKeyOf> _t;
};
}
#pragma once
#include"RBTree.h"
namespace hbw
{
template<class K, class T>
class map
{
public:
struct MapKeyOf//仿函数类,其实这个仿函数类对于set意义不大,因为set本身就可以返回key,它的主要作用是帮助map的pair来返回key使用的.
{
const K& operator()(const pair<K, T>& kv)
{
return kv.first;
}
};
//加typedef告诉编译器这里是类型
typedef typename RBTree<K, pair<const K, T>, MapKeyOf>::iterator iterator;//注意这里要加typename,告诉编译器这是一个类,否则编译器由于没有实例化,无法判断这个模板的类是什么,因此没法确定这个iterator到底是类型还是静态变量,因此对于这种内嵌类型的typedef外部封装,要加typename
typedef typename RBTree<K, pair<const K, T>, MapKeyOf>::const_iterator const_iterator;
//typedef struct _Treeiterator<pair<K,T>> iterator;//不过上面的写法容易出错,因此我直接独立访问_Treeiterator,如果是写在这个RBTtree里面的内部类就只能通过访问限定符去访问,但在这里可以直接访问
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, T>& kv)//map插入
{
return _t.Insert(kv);
}
T& operator[](const K&key)//map方括号下标访问形式,利用insert的返回值来实现
{
pair<iterator, bool> it = _t.Insert(make_pair(key, T()));//这里value给缺省值,因为我们查找的就是value,因此此时的value根本确定不了
return it.first->second;
}
const T& operator[](const K& key) const//map方括号下标访问形式,利用insert的返回值来实现
{
pair<iterator, bool> it = _t.Insert(make_pair(key, T()));//这里value给缺省值,因为我们查找的就是value,因此此时的value根本确定不了
return it.first->second;
}
private:
RBTree<K, pair<const K, T>,MapKeyOf> _t;
};
}
这便是我们实现set和map的精髓之处,模板嵌套模板,在底层的红黑树之上,将set和map在封装一层模板,这种方法很常见,我们要仔细品味体会一下。你会发现,我们的set和map类的参数基本都使用了底层的红黑树的接口。
同时注意map的operator[],我们就遵循了我刚才说过的那个operator[]使用了insert的原理。
同时针对对应的仿函数类,只要将其作为一个模板传入到底层的红黑树类即可针对不同的类调用不同的仿函数类,从而针对不同的数据进行返回对应的数据,这样就避免了map和set数据返回不同的问题。
multimap容器:
1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key, value>,其中多个键值对之间的key是可以重复的。
2. 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,value_type是组合key和value的键值对:typedef pair<const Key, T> value_type;
3. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对key进行排序的。
4. multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于key有序的序列。
5. multimap在底层用二叉搜索树(红黑树)来实现。
注意:multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以重复的。
注意:
1. multimap中的key是可以重复的。
6. multimap中的元素默认将key按照小于来比较
7. multimap中没有重载operator[]操作(同学们可思考下为什么?)。
8. 使用时与map包含的头文件相同:
总结:
在本篇中我们详细介绍了set map容器的使用,模拟实现,这其中最关键的除了熟练掌握容器的各种函数使用之外,对于多层封装以及巧妙运用仿函数进行数据获取也是一个很关键的一点。