我们学习过顺序容器如vecor,list等,它们中的元素是按照在容器中的位置来顺序保存和访问的。而接下来要学习的关联容器则有根本的不同,它们中的元素是按关键字来保存和访问的。
在《C++Primer》中列举了标准库中的8个关联容器,如下:
关联容器支持高效的关键字查找和访问,我们在这里介绍两个主要的关联容器set和map。
map里面存的是一些key-value对,其中key起到索引的作用, 而value则表示于索引相关联的数据。
比如字典就是一个很好使用map的例子,把单词当作key,解释当作value。其实map类型也常称做关联数组,它和一般的数组类似,可以认为它key就是数组的下标(只不过不必是整数),value则是数组存的值,还是上面的字典例子,比如有某个单词为right,它的其中一个解释为右边的,我们就可以使用类似数组的方式a[“right”]访问到“右边的”这个解释。
set里面每个元素只存有一个key,它支持高效的关键字查询操作,比如检查一个关键字是否在set中或者在
某些文本处理过程中可用set保存想要忽略的单词
在学习这两种关联容器之前,我们可以先看一两个如何使用这类容器的例子。
假设一个字符串数组,里面的每个字符串都是水果,比如苹果,梨子等,现在要统计每种水果出现的次数。
string fruit[] =
{
"apple", "pear", "watermelon", "peach", "banana",
"apple", "pear", "watermelon", "peach",
"apple", "pear", "watermelon",
"apple", "pear",
"apple",
};
size_t n = sizeof(fruit) / sizeof(fruit[0]);
map<string, size_t> countFruit;//定义map用来存取每种水果和它的次数
for (size_t i = 0; i < n; ++i)//增加每种水果的次数
{
++countFruit[fruit[i]];
}
for (const auto& element : countFruit)//打印每种水果以及它的次数
{
cout << element.first << " : " << element.second << endl;
}
对于上面的程序,首先定义了一个map来存取每种水果和它的次数,map也是模板,我们必须指定key和value,在这个程序中,key为string表示水果,value为size_t表示次数。
此时map里面的每个元素都是一个pair类型的对象,简单来说pair是个模版类型,保存两个public成员first和second分别对应key和value。
在上面我们说过,可以通过a[key]访问到value,因此在本例中,我们类似的也用countFruit[fruit[i]]访问到次数,但是需要注意的一点是,如果当前fruit[i]还不在map中,则创建新元素,且它的key为fruit[i],value为0。不管元素是否是新创建的,我们都将value加1。
最后的打印操作,我们使用了范围for语句,访问到map的每个元素(pair类型),然后打印他们的key和value。结果如下图:
上一个例子我们进行一个简单的扩展,比如可能某种水果太贵了比如pear,watermelon等等,我们忽视这些输入,我们则可以用set保存要想要忽略的单词,只对不在set集合的单词统计次数。
map<string, size_t> countFruit;//定义map用来存取每种水果的次数
set<string> exclude = {"watermelon", "banana"};
for (size_t i = 0; i < n; ++i)
{
if (exclude.find(fruit[i]) == exclude.end())
++countFruit[fruit[i]];
}
我们定义了一个set用来保存需要忽略的元素,类似的set也是一个模板,我们需要传入一个key类型。这个程序仅仅是多加了一句if,我们来看看是怎么回事。我们调用它的find函数,它返回一个迭代器,如果给定key在set中,则迭代器指向该key,不然返回尾后迭代器以表示没找到。这里面我们就完成了仅当每种水果不在忽略集合中再统计它的次数。结果如下图:
看完了上面的例子,我们对map和set有了一个大致的了解,下面我们仅对一些常用操作进行介绍,其余的可以参考C++文档查询map和set完整详细用法。
在上面,我们对map和set进行了一个说明,map是以key-value的形式存取,set以key形式存取,他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成,因此可以完成高效的插入删除。
在上面的统计水果次数例子中,对于重复水果,key = “apple”时对关键字没有任何影响,但是可以改变value的值,这样才实现了统计重复值的次数这一操作。另外注意我们用范围for对map打印结果依次是
这是按照字符ascii码的顺序升序得到的,因此有时候也可以让map/set排序。
但是,如果想要改变默认次序,我们可以对map/set的构造函数传入一个函数对象进行降序,下面我们来看看它的一些构造函数和拷贝构造函数。
bool fncomp (char lhs, char rhs) {return lhsstruct classcomp
{
bool operator() (const char& lhs, const char& rhs) const
{return lhsint main ()
{
map<char, int> first;//默认升序 以key abcd 的顺序存储
first['a'] = 10;
first['b'] = 30;
first['d'] = 50;
first['c'] = 70;
//注意:map的下标操作,其行为与vector很不相同:使用一个不在容器中关键字作为下标,会添加一个具有此关键字的元素到map中。一般使用find函数代替下标操作。
map<char, int> second(first.begin(), first.end());
map<char, int> third(second);
map<char, int, classcomp> fourth; // 降序 以key dcba的顺序存储
fourth['a'] = 10;
fourth['b'] = 30;
fourth['d'] = 50;
fourth['c'] = 70;
bool(*fn_pt)(char, char) = fncomp;
map<char, int, bool(*)(char, char)> fifth(fn_pt); // function pointer as Compare
}
了解map构造,我们来看下最常用的insert和find操作, 在之前的统计水果次数题,我们插入元素使用[]操作,现在我们换种做法:
void CountFruitTimes(map<string, size_t>& countFruit, string fruit[], size_t n)
{
for (size_t i = 0; i < n; ++i)
{
//auto ret = = countFruit.insert(make_pair(fruit[i], 1));
pair<map<string, size_t>::iterator, bool> ret = countFruit.insert(make_pair(fruit[i], 1));
if (!ret.second)
{
ret.first->second++;
}
//countFruit[fruit[i]]++;
}
}
我们写了个函数完成统计次数的功能,注意到insert返回的是pair类型,它的first为一个迭代器,second为bool类型,如果bool为true说明插入成功,此时它的key为fruit[i],value为1。如果bool为false说明插入失败,说明该元素已经存在,但是我们可以通过insert返回值ret得到指向该元素的迭代器。
记得之前我们讲过map里面每个元素都是一个pair类型,在上面的例子中第一个为string,第二个是size_t,因此ret.first为迭代器访问到size_t成员并对次数加1。
这样就完成了统计次数的操作。