文章目录
(一)set系列的使用
1.set的介绍
set是一个关联式容器,关联式容器逻辑结构通常是非线性结构,两个位置之间右紧密的联系,交换一下,它的存储结构就被破坏。set的底层是红黑树,红黑是是一棵平衡的二叉搜索树,set是key搜索场景的结构
set声明的结构如下图:
可以看到它有三个模板参数,它们各自的作用为:
- 第一个模板参数为关键字的类型,也就是key的类型
- 第二个模板参数是仿函数,用来控制关键字的比较方式,这里默认支持小于的比较方式,若不满足自己的需求,可自己实现一个仿函数
- 第三个模板参数是一个空间配置器,用来控制申请结点,增加效率,同样地,若不满足自己的需求,可以自己实现一个内存池
set增删查的效率为O(logN),迭代器遍历是走的搜索树的中序,所以获取到的数据是有序的。一般情况下,最后不需要传后面两个模板参数
2.set的构造和迭代器
set的构造有好几种,例如,无参的默认构造,迭代器构造,拷贝构造,initializer list构造(也就是用一段数组进行构造)
代码举例如下:
cpp
#include <iostream>
#include <set>
#include <vector>
using namespace std;
int main()
{
set<int> s1; //无参的默认构造,int就是我们传的第一个模板参数,后面两个模板参数不需要传
//再对s1进行数据的插入即可
s1.insert(2);
s1.insert(5);
s1.insert(7);
//initializer list构造
set<int> s2 = {8,3,10,2,6,5,13,14};
//拷贝构造
set<int> s3 = s2;
//用迭代器构造
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
set<int> s4(v1.begin(),v1.end());
return 0;
}
set支持正向和反向迭代器,下面代码是正向迭代器的举例:
cpp
#include<iostream>
#include<set>
using namespace std;
int main()
{
set<int> s1;
s1.insert(8);
s1.insert(5);
s1.insert(10);
//用迭代器对s1进行遍历
set<int>::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
结果:
从代码及结果可以看到,set默认按升序的顺序进行遍历的,因为底层结构是搜索二叉树,所以迭代器的遍历是走的中序遍历,set的iterator和const_iterator都不支持迭代器修改数据,修改了关键字的数据,就破坏了二叉搜索树的结构
若想输出结果变为倒序,在构造时修改一下第二个模板参数即可,如下代码:
cpp
set<int,greater<int>> s1;//使数据的存储规则变为大的在左,小的在右,迭代器中序遍历时就是倒序了
支持迭代器就说明支持范围for,使用如下:
cpp
int main()
{
set<int> s1;
s1.insert(8);
s1.insert(5);
s1.insert(10);
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
return 0;
}
set容器还有一个功能就是去重,set是不支持值冗余的,也就是说如果要插入的值,已经存在,那么就会插入失败,证明代码如下:
cpp
int main()
{
set<int> s;
s.insert(3);
s.insert(2);
s.insert(5);
s.insert(5);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
return 0;
}
输出结果如下:
图中只出现了一个5,说明还有一个5插入失败,证明了set容器不支持值冗余,若想要支持值冗余可以用multiset这个容器,该容器与set容器类似,区别就在于是否支持值冗余
3.set的增删查接口
- 先来看增这个接口如何使用,来看下面这张图片:
insert函数就是set增的接口,也就是插入数据的接口,红色圈起来的部分就是比较常用的插入方式,用法说明如下:
1.第一个红圈的插入方式是,一个数据一个数据的插入
2.第二个红圈的插入方式是,插入一段迭代器区间
3.第一个红圈的插入方式是,插入一组被花括号括起来的数
4.蓝色圈的插入方式是,在某个位置进行插入,但是若插入的位置破坏了二叉搜索树的规则,编译器就会按照二叉搜索树的规则来进行插入数据,所以一般不会去使用
以上就是比较常用的插入方式,代码举例如下:
cpp
#include<iostream>
#include<vector>
#include<set>
int main()
{
//插入一个个数据
set<int> s1;
s1.insert(8);
s1.insert(3);
s1.insert(9);
s1.insert(6);
//插入一段迭代器区间
vector<int> v1 = { 3,4,2,1,7,0 };
set<int> s2;
s2.insert(v1.begin(), v1.end());
//插入initializer list,也就是插入一组用花括号括起来的数据
set<string> s3
s3.insert({"set","insert","left"});//插入时,同样也会去重
return 0;
}
- 再来看删这个接口,使用该接口的方式如下图片:

1.第一个方式支持删除某个位置的值
2.第二个方式支持给一个值,直接进行删除操作,返回值是删除的元素的个数,也就是说,若删除成功就返回1,不成功就返回0
3.第三个方式支持删除一段迭代区间的值
代码举例如下:
cpp
int main()
{
set<int> s1 = { 8,3,10,2,6,9 };
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
//第一个用法:删除某个位置(position)的值,例如删除最小值
s1.erase(s1.begin());//迭代器走的中序遍历,所以中序的第一个就是最小值
cout << "删除最小值:";
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
//第二个用法:给一个值直接进行删除
int x = 0;
cin >> x;
if (s1.erase(x) == 0)
{
cout << x << "不存在" << endl;
}
else
{
cout << "删除" << x << "值:";
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
}
cout << endl;
//第三个用法:删除一段迭代区间的值
set<int> s2 = { 3,1,6,0,7,2 };
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
cout << "删除一段区间的值:";
s2.erase(s2.begin(), s2.end());
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
return 0;
}
输出结果:

- 最后来看查这个使用接口,对于查找这个接口来说,其实算法库中也实现了一个查找的接口,那为什么set还要自己实现一个查找的接口呢?
这个是算法库中的find接口:
这个是set的find接口:
我们知道算法库中的find接口,不止可用于set这个容器,它还可以用于vector、list等等其他序列式容器,所以它是一个通用型接口,为了兼容其他容器,它的查找方式是一个个数据遍历去查找,时间复杂度为O(N),效率较低;而set容器中的查找方式是根据二叉搜索树的规则来进行查找的,最差的情况下是查找的树的高度次,时间复杂度为O(logN),查找的效率较高,这就是set自己实现查找接口的原因。
算法库和set的查找接口的使用方式如下:
cpp
#include <algorithm>
int main()
{
set<int> s1 = { 8,3,10,3,2,14 };
//算法库中的find,时间复杂度为O(N)
int x;
cin >> x;
set<int>::iterator ret1 = find(s1.begin(), s1.end(), x);
cout << *ret1 << endl;
//set中的find,时间复杂度为O(logN)
set<int>::iterator ret2 = s1.find(14);
//若找到返回值指向找到的元素,否则指向std::end
return 0;
}
还有另一种快速查找的方法,就是利用set容器中count这个接口
功能:计算某个值在树中出现的次数,返回值就是出现的次数。若该值不在树中就会返回0,否则返回非0,即可以利用它来进行快速查找,代码举例如下:
cpp
int main()
{
//利用count来进行快速查找
set<int> s = { 8,3,10,6,2,14 };
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
int x = 0;
cin >> x;
if (s.count(x))
{
cout << x << "存在!" << endl;
}
else
{
cout << x << "不存在!" << endl;
}
return 0;
}
4.lower_bound和upper_bound接口的使用
这两个接口的功能就是配合起来找一段区间,两个接口的声明如下:
lower_bound接口的声明:
upper_bound接口的声明:
这两个函数就是找某个值的边界。例如,在某一段有序的数中,查找某一段区间[x,y],lower_bound函数负责找左区间x的边界,upper_bound函数负责找右区间y值的边界。那么它们查找的规则是如何的呢?来看下面的一段代码:
cpp
int main()
{
set<int> s = { 10,20,30,40,50,60,70,80,90 };
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
//实现查找[30,60]区间的值
auto lower = s.lower_bound(25);
//lower_bound会查找>=30的值,返回结果30
auto upper = s.upper_bound(65);
//upper_bound会查找>60的值,返回结果70
//找到区间之后进行删除
s.erase(lower, upper);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
return 0;
}
结果如下:
从上面的代码中,我们会有疑问,为什么lower_bound函数会找>=左区间的值呢,代码中的数组有30这个值,那如果没有30这个值,那在30到60区间的左区间的边界是不是就应该大于30,所以lower_bound函数会找>=左区间的值;而upper_bound函数找>右区间,而不是>=右区间的值,是因为我们知道,迭代器它是遵循左闭右开这个规则的,若找等于右区间的值,迭代器在遍历的过程中就不会包含右区间,那么60就不会包括在查找到的区间内,若找大于右区间的值,70的话,迭代器就会遍历到60这个数。
5.multiset和set的差异
前面已经提到过multiset了,它们之间最大的差异就是multiset支持值冗余,那么它的增删查接口都会围绕着值冗余的特点来进行实现
- multiset的insert功能,因为multiset支持值冗余,所以在插入数据时,就不会进行去重的操作,代码如下:
cpp
int main()
{
//支持值冗余,排序,但不进行去重操作
multiset<int> mset;
mset.insert(8);
mset.insert(3);
mset.insert(10);
mset.insert(2);
mset.insert(3);
multiset<int>::iterator it = mset.begin();
while (it != mset.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
结果如下:
2.multiset的find功能,multiset在查找的过程中,可能会出现多个相同的值,例如,如果要查找的值为3,可能就会有多个3,那么就会产生歧义。为解决这个问题,它查找的规则是找中序的第一个,也就是说,找中序遍历的第一个3,下图举例:
证明代码如下:
cpp
int main()
{
multiset<int> mset;
mset.insert({ 8,3,10,2,4,11,9,4,7,4,1,4 });
for (auto e : mset)
{
cout << e << " ";
}
cout << endl;
int x = 0;
cin >> x;
auto pos = mset.find(x); // 找到中序的第一个x
while (pos != mset.end() && *pos == x)
{
cout << *pos << " ";
++pos; //接着找后面的x,并打印出来
}
return 0;
}
输出结果:
- multiset的count接口
count在set中是判断某个值在或不在,但在multiset中是计算,某个值的个数,代码如下:
cpp
int main()
{
multiset<int> mset;
mset.insert({ 8,3,10,2,4,11,9,4,7,4,1,4 });
for (auto e : mset)
{
cout << e << " ";
}
cout << endl;
int x = 0;
cin >> x;
size_t ret = mset.count(x);
cout << "个数:" << ret << endl;
return 0;
}

- multiset的erase功能,是将要删除的值全部删完,不是只删除一个
代码如下:
cpp
int main()
{
multiset<int> mset = { 8,3,10,2,4,11,9,4,7,4,1,4 };
for (auto e : mset)
{
cout << e << " ";
}
cout << endl;
int x = 0;
cin >> x;
mset.erase(x);
for (auto e : mset)
{
cout << e << " ";
}
cout << endl;
return 0;
}

(二)map系列的使用
1.map的介绍
map也是一个关联式容器,底层也是一棵平衡的二叉搜索树,map是key/value搜索场景的结构
,map声明的结构如下:
cpp
template<class key,
class T,
class Compare = less<key>,
class Alloc = allocator<pair<const key, T>>
>class map;
key是map底层关键字的类型,T是底层value的类型,后面两个模板参数与set后面两个相同,这里不再做介绍,一般情况下,后面两个模板参数也是不需要传的。map增删查改的效率是O(logN),迭代器遍历走中序,所以是按key有序顺序来进行遍历的。
2.pair类型介绍
在map的底层中,存储key和value的数据不是分开存储的,而是用一个pair的类模板来进行存储的
cpp
template<class T1,class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 _first; //用来存放key的数据
T2 _second;//用来存放value的数据
pair()
:_first(T1())
,_second(T2())
{}
//....
};
所以这个key/value这个键值对都是存放在这个pair里面的
3.map的构造和迭代器
- map的构造,我们关注以下几个常用的接口即可:
无参构造:
cpp
map(const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
拷贝构造:
cpp
map(const map& x);
initializer list 列表构造:
cpp
map(initializer_list<value_type> il
,const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
迭代器区间构造:
cpp
template<class InputIterator>
map(InputIterator first,InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type& = allocator_type());
举例代码如下:
cpp
#include<iostream>
#include<map>
int main()
{
//无参构造
map<string, int> mymap1;
//拷贝构造
map<string, int> mymap2 = mymap1;
//initializer list构造
map<string, int> mymap3 = { {"left",1},{"sort",2},{"insert",3} };
//迭代器区间构造
map<string, int> mymap4(mymap3.begin(), mymap3.end());
return 0;
}
- map的迭代器,是一个双向迭代器
cpp
//正向迭代器:
iterator begin();
iterator end();
//反向迭代器:
reverse_iterator rbegin();
reverse_iterator rend();
迭代器的使用就不举例了,当然支持迭代器也会支持范围for
4.map的增删查改
- map的增
因为在map底层中存储key和value的是pair类模板,所以在使用插入的接口时,插入的是pair的键值对数据,跟set是有所不同的
map增接口也就是insert函数,使用方式如下:
cpp
pair<iterator,bool> insert (const value_type& val);//支持插入一个pair
template <class InputIterator>
void insert (InputIterator first, InputIterator last);//支持插入一段迭代区间
void insert (initializer_list<value_type> il);//插入initializer list
//还可以用一个函数模板来进行插入
template<class T1,class T2>
inline make_pair<T1,T2> make_pair(T1 x,T2 y)
{
return (pair<T1,T2>(x,y));
}
//make_pair是一个函数模板,只需要传两个参数,函数模板就会自己推导出参数的类型,推导出来之后,再在里面构造一个匿名对象,再返回,相当于又套了一层,本质上还是构造一个匿名对象
举例代码如下:
cpp
#include <iostream>
#include <map>
using namespace std;
int main()
{
//插入一个pair
pair<string, string> pv1 = { "left","左边" };
map<string, string> mymap1;
mymap1.insert(pv1);
mymap1.insert(pair<string,string>("right","右边"));
mymap1.insert(make_pair("boy", "男孩"));
//上面三种插入方式都是调用pair的构造来进行调用,区别就在于有名或者匿名而已
mymap1.insert({"right","右边"});//pair支持多参数的隐式类型转换,这里走了pair构造的转换,该插入方式是最简单的
//插入initializer list
map<string, string> dict;
dict.insert({ {"insert","插入"},{"sort","排序"},{"dog","狗"},{"girl","女孩"} });
//插入一段迭代区间
map<string, string> dict1;
dict1.insert(dict.begin(), dict.end());
//用迭代器遍历时要注意,因为pair没有对流输出进行重载,所以遍历时需要一个个打印它的成员变量
map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
//cout << (*it).first << ":" << (*it).second << endl;
cout << it->first << ":" << it->second << endl;
//上面这两种方式都可以
++it;
}
cout << endl;
//也可以用范围for进行遍历
/*for(const auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;*/
//范围for的另一种写法,C++17及以上才支持,这种用法叫"结构化绑定"
/*for (const auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
cout << endl;*/
return 0;
}
输出结果:

- map的查
该接口是find函数,使用的方式与set完全类似,声明定义如下:
cpp
iterator find(const key_type& k);
const_iterator find(const key_type& k) const;
与set的查找一样,都是通过关键字(key)来进行查找,不过map中的find返回的iterator,不仅可以确认要查找的key在不在,还可以找到key映射的value,同时通过返回的迭代器对value进行修改,但是关键字key还是一样不准修改
代码举例,经典的key/value场景,给一个单词,找出它的中文意思:
cpp
#include<iostream>
#include<string>
#include<map>
using namespace std;
int main()
{
map<string, string> dict = { {"left","左边"},{"right","右边"},
{"auto","自动的"},{"sort","排序"},
{"insert","插入"} };
string str;
while (cin >> str)
{
auto pos = dict.find(str);
if (pos != dict.end())
{
cout << "->" << pos->second << endl;
}
else
{
cout << "单词不存在,请重新输入!" << endl;
}
}
return 0;
}
输出结果:
3.map的删
与set完全类似,使用erase函数,也是通过传关键字或者迭代器的位置及迭代区间来进行单个/多个的删除,它的声明结构如下:
cpp
iterator erase(const_iterator position);
size_type erase(const key_type& k);
iterator erase(const_iterator first, const_iterator last);
代码举例如下:
cpp
int main()
{
map<string, string> dict = { {"left","左边"},{"right","右边"},
{"auto","自动的"},{"sort","排序"},
{"insert","插入"},{"girl","女孩"},
{"boy","男孩"}};
for (const auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
cout << endl;
cout << "删除第一个位置的关键字:" << endl;
dict.erase(dict.begin());
for (const auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
cout << endl;
cout << "删除right:右边" << endl;
dict.erase("right");
for (const auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
cout << endl;
cout << "全部删除:" << endl;
dict.erase(dict.begin(), dict.end());
for (const auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
cout << endl;
return 0;
}
结果:
4.map的该
在查找时已经提到,find函数返回的迭代器是可以对value进行修改的,或者迭代器在遍历的过程中,也可以对value进行修改,代码如下:
cpp
int main()
{
map<string, string> dict = { {"left","左边"},{"right","右边"},
{"auto","自动的"},{"sort","排序"},
{"insert","插入"} };
//对left进行
auto ret = dict.find("left");
if (ret != dict.end())
{
ret->second = "留下";
cout << ret->first << ":" << ret->second << endl;
}
cout << endl;
//用迭代器遍历进行修改
for (auto& e : dict)
{
e.second += 'x';
cout << e.first << ":" << e.second << endl;
}
return 0;;
}

5.map的operator[]
先来看该接口的声明结构:
cpp
mapped_type& operator[](const key_type& k);
从声明结构中得到,给一个key,返回对应的value的引用,所以该接口可以充当一个查找+修改的功能
实际上,重载方括号的底层其实是用insert实现的,它的实现等价于调用这段代码:
cpp
return (*this->insert(make_pair(k, mapped_type()))).second;
所以可以知道,重载的方括号还有插入的功能,上面这段代码有点复杂,涉及到了插入的返回值,我们可以再来重新看一遍插入函数的声明结构:
cpp
pair<iterator, bool> insert(const value_type& val);
分析:上面的这个声明结构,涉及到了两个pair,第一个pair就是要传的参数,value_type,相当于pair<k,v>,第二个pair就是返回值,返回值的pair值中,第一个是迭代器,第二个是bool值,需要返回bool值是因为插入可能会成功,也可能会失败,毕竟map容器是不允许值冗余的。当插入成功时,bool值是true,返回的迭代器是插入成功的新的key结点的迭代器;当插入失败时,bool值是false,返回的迭代器是已经存在的key结点的迭代器。
总结:无论插入成功还是失败,返回pair<iterator,bool>对象的迭代器都会指向key所在的结点,那么也就意味着insert插入失败时充当查找的功能,正是这一点,insert可以用来实现operator[]
operator[]内部实现的代码如下:
cpp
mapped_type& operator[](const key_type& val)
{
pair<iterator, bool> ret = insert({ k,mapped_type() });
iterator it = ret.first;
return it->second;
}
代码解析:
- 若key不在map中,insert会插入key和mapped_type默认值,同时[]返回结点中存储mapped_type值的引用,那么我们可以通过引用来修改反映射值。所以[]具备插入+修改的功能
- 若key在map中,insert会插入失败,但insert返回pair对象的first是指向已存在的key结点的迭代器,同时[]会返回pair迭代器中存储mapped_type值的引用,所以[]具备了查找+修改的功能
[]的使用样例如下代码:
cpp
int main()
{
map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
for (auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
cout << endl;
cout << "key不存在->插入:" << endl;
dict["left"]; //插入后value是string的默认值,所以是空
for (auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
cout << endl;
cout << "存在->修改:" << endl;
dict["left"] = "留下";
dict["left"]; //插入后value是string的默认值,所以是空
for (auto& [k, v] : dict)
{
cout << k << ":" << v << endl;
}
cout << endl;
cout << "查找:" << endl;
cout << dict["left"] << endl;
return 0;
}
输出结果:
6.multimap和map的差异
multimap和map的使用基本上是完全类似的,区别就在于multimap支持关键字key值冗余,那么它的insert/find/erase/count都会因为key冗余而有所差异,跟set和multiset完全一样,比如,find有多个key时,返回中序的第一个key,其次就是multimap不支持operator[],因为支持key冗余,operator[]就只能支持插入,不能支持值修改