前面我们学习了vector、list等容器,其实他们都属于序列式容器,因为其底层为线性结构;今天我们学习使用的set与map是属于关联式容器,关联式容器更注重于数据检索访问的效率;本文所有的资料均查阅于文档,以下为文档链接;可自行查看;
文档链接
在C++中,它是一种集合,是我们前面二叉搜索树的Key模型;其所在的头文件是set;其实这个头文件下,还有一个兄弟容器 ------- multiset,在介绍set是我也会介绍multiset,因为二者其实用法区别并不大,差异我会一 一介绍出来;
关于multiset其实与set几乎差不多,唯一区别是,set会去重,而multiset不会;本文主要注重set的讲解,当然,你会使用set,multiset你也自然会使用了;
set的类模板如上所示;我们说过它是我们前面学过二叉搜索树的Key模型;因此它的第一个模板参数就是我们的Key的类型;第二个模板参数是比较的方式,需要传入一个仿函数类;第三个参数为内存池相关对象;传默认即可;
首先我们查看该类的类型重定义,我们发现该类有key_type与value_type都是第一个模板参数T类型;所以说set是Key模型;然后我们发现set的迭代器是双向迭代器;
这部分我们主要看其构造函数与赋值重载的接口;构造函数接口如下;我们发现有三种不同的构造方式,第一种是无参的默认构造;第二种是迭代器区间构造;第三种是拷贝构造;有了前面的基础,这里的接口几乎看一眼都会使用了;
void test_set3()
{
// 无参构造
set s1;
// 迭代器区间构造
vector v{ 1,2,3,4,5,6 }; // C++11初始化语法
set s2(v.begin(), v.end());
// 拷贝构造
set s3(s2);
}
赋值重载接口如下;赋值重载对于已存在的对象进行赋值;使用难度几乎没有;
迭代器真的不愧是STL中六大组件之一,无论是线性结构还是set与map中的树形结构,我们都可以通过迭代器接口来进行访问;使用起来几乎没有区别;这里迭代器为双向迭代器,因此不支持方括号的方式访问,但支持++与--;使用如下;
void test_set4()
{
set s1;
// 这里为了展示迭代器先使用插入接口了
s1.insert(2);
s1.insert(3);
s1.insert(1);
s1.insert(5);
s1.insert(2);
s1.insert(1);
s1.insert(4);
set::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 既然支持迭代器,就肯定有范围for了
for (auto& e : s1)
{
cout << e << " ";
}
cout << endl;
}
我们发现对set用迭代器遍历的时候,set对里面的数据完成了去重+排序;我们再试试我们之前说的multiset,代码与结果如下;
void test_set4()
{
cout << "set: " << endl;
set s1;
// 这里为了展示迭代器先使用插入接口了
s1.insert(2);
s1.insert(3);
s1.insert(1);
s1.insert(5);
s1.insert(2);
s1.insert(1);
s1.insert(4);
set::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 既然支持迭代器,就肯定有范围for了
for (auto& e : s1)
{
cout << e << " ";
}
cout << endl;
cout << "multiset: " << endl;
multiset ms1(s1.begin(), s1.end());
ms1.insert(2);
ms1.insert(3);
ms1.insert(1);
ms1.insert(5);
ms1.insert(2);
ms1.insert(1);
ms1.insert(4);
for (auto& e : ms1)
{
cout << e << " ";
}
cout << endl;
}
我们发现与set不同,multiset只会对数据进行排序,并不具有去重功能;
到了这一块就没有什么扩容的概念了;empty是检查set是否为空,size是查看set中元素个数,max_size是查看理论上最多可以放的元素个数,这个接口几乎没什么意义;
关于set的修改接口如下所示;以下展示比较经常使用的接口;
void test_set5()
{
set s1;
// 插入某个值
s1.insert(2);
s1.insert(5);
s1.insert(1);
s1.insert(2); // 插入失败,因为有2这个值了
s1.erase(1); // 删除某个值
set s2;
// 交换两个set
s1.swap(s2);
// 清空某个set
s2.clear();
}
关于set还有一些特殊化的操作,如下所示;
void test_set6()
{
set s1;
s1.insert(0);
s1.insert(1);
s1.insert(3);
s1.insert(3);
s1.insert(4);
// 查找某个key的位置,返回该位置迭代器,若没找到返回s1.end()
set::iterator it = s1.find(2);
if (it != s1.end())
{
cout << "找到了" << endl;
}
// count接口在set中就是查看该元素在不在set中
cout << s1.count(2) << endl;
}
这里的count在multiset中是返回指定元素个数;lower_bound函数与upper_bound函数是一个查找函数,一个是返回一个迭代器,该迭代器指向集合容器中的第一个元素,该元素不被认为在值之前。另一个是返回一个迭代器,该迭代器指向集合容器中的第一个元素,该元素被认为是在值之后。equal_range则是返回一个范围的边界,该范围包括集合容器中与 value 等效的所有元素。
set是一个Key模型,因此我们通常用来解决在不在的问题;例如还是查一篇文章中的单词是否拼写正确,我们可以检查这个单词是否存在字典中;如下代码所示;
void test_set2()
{
set dict;
dict.insert("left");
dict.insert("right");
dict.insert("area");
dict.insert("count");
dict.insert("return");
string str;
while (cin >> str)
{
set::iterator pos = dict.find(str);
if (pos != dict.end())
{
cout << "在" << endl;
}
else
{
cout << "不在" << endl;
}
}
}
map是我们前面所学二叉搜索树中的一种Key_Value模型,是一种键值对的方式,key与value一 一对应;与set类似,还有一个multimap接口;因为map也是去重的,multimap则是不去重版本;它们的接口使用几乎也无二异;故放在一起讲解;
首先我们看模板参数,第一个模板参数是Key的类型,第二个模板参数是Value的类型;第三个参数则是比较的方式;第四个模板参数是内存池相关类;
在看map类型的重定义时,我们发现这里与set有很大的差别;它的key_type是第一个模板参数;这里多了一个map_type,是第二个模板参数;而它的value_type则是一个二元组(pair),里面存的是一个键值对;而迭代器是一个双向迭代器;
在正式谈map接口之前,我们首先弄清关于pair,pair是一个二元组,具体定义如下;其中有两个成员分别为first与second;
template
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)
{}
};
同时,C++还为我们提供了一个函数来构建pair;我们可以通过这个函数实例化出一个pair对象;
map的构造函数无非也就是那三种构造方式,默认构造,迭代器区间构造,拷贝构造;
迭代器的使用也没什么不同,演示代码如下;
void test_map1()
{
map dict;
dict.insert(make_pair("left", "左边"));
dict.insert(make_pair("right", "右边"));
dict.insert(make_pair("count", "计数"));
map::iterator it = dict.begin();
while (it != dict.end())
{
// 迭代器本质实际上就是指向pair的指针,因此这里我们指定访问的某个具体成员
cout << it->first << ": " << it->second << endl;
it++;
}
cout << endl;
// 范围for
for (auto& e : dict)
{
cout << e.first << ": " << e.second << endl;
}
cout << endl;
}
容量接口与前set相同,此处就不做介绍了;
主要介绍这里的修改相关接口,这里的insert插入的是value_type类型的数据,也就是我们前面说的pair类型的数据;
注意,上面的插入数据的接口是一个二元组的pair类型的数据,且pair的第一个数据的类型是一个迭代器,第二个数据的类型是一个bool类型的数据;实际上,第二个pair的第二个成员是记录是否插入成功,若插入数据的key值已存在,则插入失败,第二个参数设置为false,第一个参数设置为对应key的迭代器;若插入数据不存在,则插入新值,然后第一个参数设置为新插入key值得迭代器的位置,第二个参数设置为true;
void test_map2()
{
map dict1;
dict1.insert(make_pair("left", "左边"));
dict1.insert(make_pair("right", "右边"));
dict1.insert(make_pair("count", "计数"));
dict1.insert(make_pair("right", "权力")); // 插入失败
// 删除指定键值对
dict1.erase("count");
// 交换两个map(两棵树的根节点)
map dict2;
dict1.swap(dict2);
// 清空
dict2.clear();
}
关于map的使用,最需要注意的就是这个方括号了;其次我们需要学会使用这个find接口;这里的count接口与set一样,查看一个键值对是否存在,在multimap可以统计某个key在容器中的个数;
方括号的返回值的声明如下所示,其实际上调用了insert函数而已;函数体也放在下面了;
函数体如下图,可能很多人看到下面的代码头都大了,小编一开始看到这样的代码也是一脸懵;
mapped_type& operator[] (const key_type& k)
{
return (*((this->insert(make_pair(k,mapped_type()))).first)).second;
}
这个知识不就跟我们之前学的插入的返回值串起来了吗?map中的方括号功能十分强大,如下所示;
void test_map3()
{
map mp1;
mp1.insert(make_pair("left", "左边"));
mp1.insert(make_pair("right", "右边"));
mp1.insert(make_pair("int", "整型"));
mp1.insert(make_pair("right", "权力")); // 插入失败
// 我们可以通过方括号代替insert
mp1["double"] = "浮点型"; // 插入
cout << mp1["int"] << endl; // 查找
mp1["right"] = "权力"; // 查找+修改
}
方括号语法确实也强大,具有多种功能;接下来我们来看看map的查找函数;
void test_map4()
{
map mp1;
mp1.insert(make_pair("left", "左边"));
mp1.insert(make_pair("right", "右边"));
mp1.insert(make_pair("int", "整型"));
map::iterator pos = mp1.find("int");
if (pos != mp1.end())
{
cout << pos->second << endl;
}
}
相比起来,还是我们的方括号似乎更加好用;
map是key_value模型,主要用于键值对的维护,是一种一 一对应的关系;我们可以用来统计某种物品的个数,这里我们在之前的二叉搜索树那里也实现过相同的代码,今天我们用map再实现一次;
void test_map5()
{
map fruits;
std::string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "火龙果", "苹果",
"西瓜","苹果", "香蕉", "苹果", "香蕉" , "火龙果"};
// 方法一
//for (auto& e : arr)
//{
// auto pos = fruits.find(e);
// if (pos != fruits.end())
// {
// pos->second++;
// }
// else
// {
// fruits.insert(make_pair(e, 1));
// }
//}
// 方法二
for (auto& e : arr)
{
fruits[e]++;
}
for (auto& e : fruits)
{
cout << e.first << ": " << e.second << endl;
}
cout << endl;
}
这里是不是又能体会到方括号功能的强大!同时这里也体现了key_value模型的作用;