容器的分类
前面我们已经接触过 STL 中的部分容器如:string、vector、list、deque 等,这些容器统称为序列式容器。因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,比如交换⼀下,它依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置进行顺序保存和访问的。
关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置有紧密的关联关系,交换⼀下,它的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有 map/set 系列unordered_map/unordered_set 系列。
序列式容器和关联式容器有点像数据结构中的线性结构和链式结构。
set
set 的声明为:
template < class T, // set::key_type/value_type
class Compare = less<T>, // set::key_compare/value_compare
class Alloc = allocator<T> // set::allocator_type
> class set;
set 的特点
**
•
**set 类不支持数据冗余**
•
**使用 set 时,需要包含头文件<set>**
•
**T 就是 set 底层关键字的类型**
•
**set 中没有 key 和 value ,只有 key**
•
**set 默认要求 T 支持小于比较,仿函数 compare 用于在外部控制 set 的比较大小逻辑/实现自己的需求**
• 一
**般情况下,都不需要传后两个模版参数**
•
**set底层是用红黑树实现的,增删查效率是 O(logN)**
•
**迭代器遍历是走的二叉搜索树的中序排序,所以遍历的结果是有序的
STL容器接口设计高度相似,set 的某些接口的功能与其它 STL 容器的差不多,掌握了前面 vector/list 等容器的接口的使用,再来掌握 set 容器的接口就不是问题了,所以就不逐一了解,只需了解重要的接口。
set 支持迭代器区间初始化和花括号初始化。如下代码所示:
cpp
set<int> s = { 3, 5, 2, 9, 8, 1, 3, 7, 2 };
vector<int> v = { 3, 5, 2, 9, 8, 1, 3, 7, 2 };
set<int> s2(v.begin(), v.end());
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:

由结果可知,遍历时将重复元素去除了,说明 set 的遍历是排序加上去重。
set 的迭代器类型为双向迭代器,set 的迭代器的使用方式与我们之前使用的迭代器一致。前面的STL 容器的迭代器都是普通迭代器支持修改,const 迭代器不支持修改,而 set 容器,普通迭代器和 const 迭代器都不能修改。
insert 函数的函数原型为:iterator insert (iterator position, const value_type& val),功能:指定位置 pos 处插入结点。单个数据插入,如果数据存在,则插入失败。
erase 函数的函数原型为:size_type erase (const value_type& val),功能:删除key 值为 val 的结点, 返回被删除的元素数量。由于 set 容器不支持数据冗余,所以 erase 的返回值只有 1 和 0 ,val 不存在返回 0 ,存在返回 1。erase 函数之所以返回值类型为 size_t ,是为了和 multiset 容器保持接口一致性。
使用 earse 函数:
cpp
set<int> s = { 3, 5, 2, 9, 8, 1, 3, 7, 2 };
cout << "删除前:";
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
cout << s.erase(3) << endl;
cout << "删除后:";
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:

若要删除 set 对象中最小的元素,删除迭代器 begin 指向的元素即可。因为中序遍历,第一个元素就是最小的元素。
find 函数的函数原型为:iterator find (const value_type& val) const,查找到了就返回查找到的元素位置的迭代器,未查找到就返回迭代器 end。
使用 find 函数:
cpp
set<int> s = { 3, 5, 2, 9, 8, 1, 3, 7, 2 };
auto it1 = s.find(2);
if (it1 != s.end())
{
cout << "2找到了" << endl;
}
else
{
cout << "2未找到" << endl;
}
auto it2 = s.find(6);
if(it2 != s.end())
{
cout << "6找到了" << endl;
}
else
{
cout << "6未找到" << endl;
}
运行结果:

set 容器中的 find 函数的查找时间复杂度为 O(logN) ;算法库中的 find 函数的查找时间复杂度为O(N)。查找 set 对象的元素时,使用 set 中实现的 find 函数。
count 函数的函数原型:size_type count (const value_type& val) const,功能:在容器中搜索与val 相等的元素,并返回匹配次数。因为 set 容器中的所有元素都是唯一的,所以该函数只能返回1(找到了指定元素)或0(未找到指定元素)。因此,可以使用 count 函数间接查找 set 对象中值为 val 的元素。
cpp
set<int> s = { 3, 5, 2, 9, 8, 1, 3, 7, 2 };
if (s.count(2))
{
cout << "2找到了" << endl;
}
else
{
cout << "2未找到" << endl;
}
if (s.count(6))
{
cout << "6找到了" << endl;
}
else
{
cout << "6未找到" << endl;
}
运行结果:

count 函数的返回值为 size_t 类型是为了和 multiset 保持一致性,multiset 中也实现了 count 接口,而 multiset 支持元素冗余,multiset 对象中的元素不是唯一的,这样 count 的返回值就不只是1和0了。
lower_bound函数的函数原型为:iterator lower_bound (const value_type& val) const,功能:返回指向容器中第一个大于等于 val 的元素的迭代器。
upper_bound 函数的函数原型为:iterator upper_bound (const value_type& val) const,功能:返回指向容器中第一个大于val的元素的迭代器。
如果要删除对象中3~9之间的值(包括3和9),应该怎么做?函数使用的迭代器区间都是左闭右开区间,如何获取到左闭右开区间,况且对象中不一定就存在3或9,想要在对象中单独找到3或9的办法就不可行了。这种情况下,我们可以使用set容器中的 lower_bound 和 upper_bound 接口。
如下所示:
cpp
set<int> s = { 4, 9, 5, 7, 1, 2, 4, 6, 10 };
cout << "删除前:";
for (auto& e : s)
{
cout << e << " ";
}
cout << endl;
// 删除[3, 9] 区间的元素
// 使用 lower_bound 函数,获取第一个大于等于3位置的迭代器
auto it1 = s.lower_bound(3);
// 使用 upper_bound 函数,获取第一个大于9位置的迭代器
// 若最后一个元素为9,返回的是end
auto it2 = s.upper_bound(9);
// 删除
s.erase(it1, it2);
cout << "删除后:";
for (auto& e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:

multiset
multiset 和 set 的使用基本完全类似,主要区别点在于 multiset 支持数据冗余,insert/find/count/erase 这几个接口都围绕着支持数据冗余有所差异。
迭代器遍历对象
cpp
multiset<int> s = { 4, 9, 3, 5, 7, 3, 1, 4, 6 };
multiset<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
相比 set 不同的是,multiset 是排序,但是不去重。
multiset 支持使用 insert 函数插入 multiset 对象中已经存在的元素,insert 的函数原型为:iterator insert (const value_type& val)
cpp
multiset<int> s = { 4, 9, 3, 5, 7, 3, 1, 4, 6 };
cout << "插入前:";
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
s.insert(6);
cout << "插入后:";
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:

erase 函数的函数原型为:size_type erase (const value_type& val),功能:将所有 val 值都删除,返回删除了几个val值。
cpp
multiset<int> s = { 4, 9, 3, 5, 7, 3, 1, 4, 6 };
cout << "删除前:";
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
cout << "删除了" << s.erase(4) << "个值为4的元素" << endl;
cout << "删除后:";
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
运行结果:

如果只想删除某个值,可以使用迭代器删除,函数原型为:void erase (iterator position)。
count 函数的函数原型为:size_type count (const value_type& val) const,功能:查找multiset 对象中有几个 key 值为 val 的元素。
cpp
multiset<int> s = { 4, 9, 3, 5, 7, 3, 1, 4, 6 };
cout << "元素4的个数:" << s.count(4) << endl;
cout << "元素1的个数:" << s.count(1) << endl;
cout << "元素8的个数:" << s.count(8) << endl;
运行结果:

find 函数的原型为:iterator find (const value_type& val) const,功能:查找对象中值为 key 值为 val 的元素,它返回的是中序遍历的第一个值为 val 的元素的迭代器。

对于该二叉搜索树,find(5) 的结果是指向粉色结点5的迭代器。
map
map 的声明:
template < class Key, // map::key_type
class T, // map::mapped_type
class Compare = less<Key>, // map::key_compare
class Alloc = allocator<pair<const Key,T> > // map::allocator_type
> class map;
map 的特点:
**
•
**使用 map 容器时,需要包含头文件 <map>**
•
**key就是map底层关键字的类型,T 是 map 底层 value 的类型**
•
**set 默认要求 key 支持小于比较,仿函数 compare 用于控制 key 的比较逻辑/自己实现需求
•
⼀般情况下,都不需要传后两个模版参数**
•
**map底层是用红黑树实现,增删查改效率是 O(logN)**
•
**迭代器遍历是走的中序,所以是按 key 有序顺序遍历的**
•
**map 不支持数据冗余
•
在 map 中,关键值 key 通常用于对元素进行排序并为其赋予唯一标识,而映射值 value则存储与该关键值 key 相关联的内容。关键值 key 和映射值 value 的类型可能不同,并且它们被组合在一起形成成员类型 value_type,这是一个结合了两者特征的元组类型: typepedef pair<const Key,T> value_type
•
map底层的红黑树节点中的数据,就是使用 pair 存储键值对数据
•
map 支持修改 value 数据,不支持修改 key 数据,修改关键字数据,会破坏底层搜索树的结构
•
map 中存储的都是 pair 类型的数据
pair 的声明:template <class T1, class T2> struct pair,pair 是一个类,类中有两个成员变量,first 和 second ,first 就是 key 关键值,second 就是映射值 value。
pair 的内部实现:
cpp
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)
{}
// 拷贝构造函数
template<class U, class V>
pair(const pair<U, V>& pr) : first(pr.first), second(pr.second)
{}
template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y)
{
return (pair<T1, T2>(x, y));
}
};
map 支持迭代器区间初始化,花括号初始化
如下代码所示:
cpp
// 花括号中的{"int", "整型"}是多参数的隐式类型转换
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
vector<pair<string, string>> v = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
map<string, string> map1(v.begin(), v.end());
在讲解 map 的接口之前,需要了解参数的类型代表的是什么
key_type ------ The first template parameter (Key)
mapped_type ------ The second template parameter (T)
value_type ------ pair<const key_type,mapped_type>
insert 函数的函数原型为:pair<iterator,bool> insert (const value_type& val),insert 的参数有三种写法:
cpp
map.insert(pair<string, string>("map", "地图"));
map.insert(make_pair("set", "集合"));
map.insert({ "insert", "插入" });
•
pair<string, string> 调用的是 pair 类中的pair(const T1& a, const T2& b) : first(a), second(b) {} ;
•
make_pair 调用的是 pair 类中的template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y) { return (pair<T1, T2>(x, y)); }
•
最简洁的方式为:mp.insert({ "int", "整型" }),{ "int", "整型" }通过隐式类型转换为 pair 类型
遍历
使用迭代器,找起始位置,map<string, string>::iterator it = mp.begin(),it 的参数类型似乎过长,可以使用 aut o代替,让编译器推理 it 的类型 ------ auto it = mp.begin()。使用 auto 会有缺点,自己不知道 it 的类型,导致不了解 it 的使用。
使用迭代器遍历 map 对象
cpp
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
auto it = map.begin();
while (it != map.end())
{
cout << *it << " ";
++it;
}
迭代器使用得正确吗?不正确。it 解引用得到的数据类型是 pair 类型,pair 类型不支持流提取运算符,需要将 pair 中的成员一个个打印。改进后的代码如下所示:
cpp
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
auto it = map.begin();
while (it != map.end())
{
cout << it->first << ":" <<it->second << endl;
++it;
}
也可以使用 * 和 . 操作符
cpp
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
auto it = map.begin();
while (it != map.end())
{
cout << (*it).first << ":" << (*it).second << endl;
++it;
}
但是推荐使用 -> ,it->first 和 it->second 的本质是 it.operator->()->first 和 it.operator->()->second,在 list 容器中有详细讲解。
运行结果:

使用范围 for 遍历 map 对象
cpp
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
for (auto& kv : map)
{
cout << kv.first << ":" << kv.second << endl;
}
运行结果:

map 可以使用结构化绑定,结构为:auto [k, v] = kv,取结构体中的值,这 是C++14/17 新增的语法,auto[x, y] = kv 是一种拷贝行为,auto& [x, y] = kv 避免了拷贝 。范围 for 可以和结构化绑定搭配使用,简化了范围 for ,在使用 map/multimap 的范围 for 时,最好使用结构化绑定。
cpp
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
for (auto& [k, v] : map)
{
cout << k << ":" << v << endl;
}
map 中的 lower_bound 和 upper_bound 的功能与 set 中的一致。
equal_range 函数的函数原型为:
pair<const_iterator,const_iterator> equal_range (const key_type& k) const;
pair<iterator,iterator> equal_range (const key_type& k);
成员变量是两个iterator,返回值为 pair 类型,返回一个元素 k 的左闭右开区间。
map 支持修改 value 值,第一种修改方式是通过迭代器。
cpp
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
cout << "修改前:" << endl;
for (auto& [k, v] : map)
{
cout << k << ":" << v << endl;
}
cout << endl;
cout << "修改后:" << endl;
for(auto& [k, v] : map)
{
// key 值不能修改
//k += 'x'
// value 值可以修改
v += '2';
cout << k << ":" << v << endl;
}
运行结果:

第二种修改方法通过 find 返回 key 所在的 iterator 修改。
find 函数的函数原型为:iterator find (const key_type& k),只需要传key值。功能:查找 k ,返回 k 所在的迭代器,没有找到返回 end() ,如果找到了通过 iterator 可以修改 key 对应的 mapped_type 。
cpp
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
for (auto& [k, v] : map)
{
cout << k << ":" << v << endl;
}
cout << endl;
auto ret = map.find("int");
ret->second += '2';
for (auto& [k, v] : map)
{
cout << k << ":" << v << endl;
}
运行结果:

map 还有⼀个非常重要的修改接口 operator[ ],但是 operator[ ] 不仅支持修改,还支持插入数据和查找数据,所以它是一个多功能复合接口。
统计水果出现的次数
传统写法:
cpp
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果",
"西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto& str : arr)
{
map<string, int>::iterator it = countMap.find(str);
if (it != countMap.end())
{
it->second++;
}
else
{
countMap.insert({ str, 1 });
}
}
for (auto& [k, v] : countMap)
{
cout << k << ":" << v << endl;
}
运行结果:

使用 operator[ ]:
cpp
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果",
"西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto& str : countMap)
{
countMap[str]++;
}
for (auto& [k, v] : countMap)
{
cout << k << ":" << v << endl;
}
运行结果:

map 和 set 最大的不同是 map 重载了 [ ] 操作符。
operator [ ] 的原型:mapped_type& operator[ ] (const key_type& k),operator[ ] 等价于 (*((this->insert(make_pair(k,mapped_type()))).first)).second,operator[ ] 是基于 insert 函数实现的,它用到了 insert 函数的返回值 pair<iterator, bool> 。
insert 插入⼀个 pair 对象
1 、如果 key 已经在 map 中,插⼊失败,则返回⼀个 pair 对象,返回 pair 对象 first 是 key 所在结点的迭代器, second 是 false
2 、如果 key 不在 map 中,插⼊成功,则返回⼀个 pair 对象,返回 pair 对象 first 是新插入 key 所在结点的迭代器, second 是 true
也就是说无论插入成功还是失败,返回 pair 对象的 first 都会指向 key 所在的迭代器 那么也就意味着 insert 插入失败时充当了查找的功能,正是因为这⼀点,insert 可以用来实现 operator [ ]
我们一层一层的分析:(*((this->insert(make_pair(k,mapped_type()))).first)).second
**
•
**insert(make_pair(k,mapped_type())) 返回值类型为 pair<iterator,bool>
•
(this->insert(make_pair(k,mapped_type())),通过 this 指针指向insert 的返回值 pair<iterator,bool>
•
((this->insert(make_pair(k,mapped_type()))).first) 取到 pair<iterator,bool> 中的 first ,也就是 iterator 迭代器,迭代器是 map 容器中的迭代器,指向的数据是 pair<key_type, mapped_type> 类型
•
*((this->insert(make_pair(k,mapped_type()))).first) 对 iterato r解引用,解引用得到的值的类型为 pair<key_type, mapped_type> 一个是key的类型,一个是value的类型
•
(*((this->insert(make_pair(k,mapped_type()))).first)).second 再取 pair<key_type, mapped_type>中的second,也就是 value**
•
**所以 opeartor[ ] 的本质是返回 k 中的 value
注意:
这里有两个pair类型,一个是insert的返回值pair<iterator,bool>;一个map容器的数据类型pair<key_type, mapped_type>
operator[ ] 根据 k 是否存在分成了不同的功能:
1 、如果 k 不在 map 中, insert 会插⼊ k 和 mapped_type 默认值,同时 [ ] 返回结点中存储mapped_type 值的引用,那么我们可以通过引用修改返映射值。所以 [ ] 具备了插入 + 修改功能
2 、如果 k 在 map 中, insert 会插入失败,但是 insert 返回 pair 对象的 first 是指向 key 结点的迭代器,返回值同时 [ ] 返回结点中存储 mapped_type(value) 值的引用,所以 [ ] 具备了查找 + 修改的功能
下面来展示 [ ] 的各种功能:
cpp
map<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
map["map"]; // 插入
cout << map["map"] << endl;
map["map"] = "地图"; // 修改
cout << map["map"] << endl;
map["set"] = "集合"; // 插入+修改
cout << map["set"] << endl;
cout << map["int"] << endl; // 查找
运行结果:

multimap
multimap 和 map 的使用基本完全类似,主要区别点在于 multimap 支持关键值 key 冗余,那么 insert/find/count/erase 都围绕着支持关键值 key 冗余有所差异,这里跟 set 和 multiset 完全⼀样,比如使用 find 函数查找 key 值为 val 的元素,若有多个 key,返回中序第⼀个。其次就是 multimap 不支持 [ ] ,因为支持 key 冗余,[ ] 就只能支持插入了,不能支持修改。
支持 key 冗余如下所示:
cpp
multimap<string, string> map = { {"int", "整型"}, {"short", "短整型"},
{"long", "长整型"}, {"char", "字符型"} };
map.insert({ "string", "你好" });
map.insert({ "string", "世界" });
for (auto& [k, v] : map)
{
cout << k << ":" << v << endl;
}
运行结果:
