目录
- 前言
- 一、序列式容器和关联式容器
- 二、set的使用
-
- 1、构造函数
- 2、赋值运算符重载
- [3、迭代器的使用begin、end 以及范围for](#3、迭代器的使用begin、end 以及范围for)
- 4、empty/size
- 5、insert
- 6、erase
- 7、swap/clear
- 8、find
- 9、count
- 10、lower_bound/upper_bound
- 11、equal_range
- 三、multiset和set的差异
- 四、map的使用
- 五、multimap与map的差异
前言
接着【C++】二叉搜索树详情请点击,今天来介绍【C++】map和set的使用
一、序列式容器和关联式容器
- 我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,比如交换一下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
- 关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构 ,两个位置有紧密的关联关系,交换一下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。
- 今天介绍的map和set底层是红黑树,红黑树是⼀颗平衡二叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。
二、set的使用

- T就是set底层关键字的类型
- set默认要求T支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数
- set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数(⼀般情况下,我们都不需要传后两个模版参数)
- set底层是用红黑树实现,增删查效率是O(logN),迭代器遍历是走的搜索树的中序,所以是有序的
- 在set中,value就是key,类型都是T,在set中key的值是唯一的且不可以修改,一旦修改,那么二叉搜索树的结构就被破坏了,所以key不能修改,虽然key不可以修改,但是key可以删除或插入,但是当插入的时候如果这个key已经有了,由于key是唯一的,所以默认将插入失败
- 根据这个性质,我们可以使用set对数据去重
1、构造函数

cpp
int main()
{
set<int> s1;
int a[] = { 1, 2, 3 };
set<int> s2(a, a + sizeof(a) / sizeof(a[0]));
set<int> s3(s2);
return 0;
}

2、赋值运算符重载

cpp
int main()
{
set<int> s1;
int a[] = { 1, 2, 3 };
set<int> s2(a, a + sizeof(a) / sizeof(a[0]));
s1 = s2;
return 0;
}

3、迭代器的使用begin、end 以及范围for

cpp
int main()
{
int a[] = { 1, 2, 3 };
set<int> s(a, a + sizeof(a) / sizeof(a[0]));
set<int> ::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
for (auto& e : s)
cout << e << " ";
cout << endl;
return 0;
}

4、empty/size

- empty:判断set容器中是否为空
- size:返回容器中有多少数据

5、insert

- 插入一个key(val)(这里的value_type(value)和key_type(key)一样都是T类型,T:set中存储的数据类型),观察其返回的是一个键值对pair类型,其中第一个数据first是插入位置的迭代器,第二个位置是是否插入成功,由于set中的key值不能重复且只能有一个,如果key值已经有了,那么返回的是原key位置的迭代器以及false,如果key值没有,那么返回的是新插入key位置的迭代器以及true,由于return不能返回两个值,所以如果想返回多个值,必须放在一个结构中进行返回,这里是放在了键值对pair中进行返回
- 在set的某一个迭代器位置插入val,迭代器位置并不是强制插入到该迭代器位置,我们传入的这个迭代器位置对于编译器来说只是一个建议位置,实际数据插入后还是根据其原有的二叉搜索树的逻辑进行调整
- 插入某个迭代器区间的数据
- 插入initializer_list类型数据
cpp
int main()
{
set<int> s;
int a[] = { 10, 7, 8 };
s.insert(1);
s.insert(s.begin(), 16);
s.insert(a, a + sizeof(a) / sizeof(a[0]));
s.insert({ 7, 10, 4 });
for (auto& e : s)
cout << e << " ";
cout << endl;
return 0;
}
运行结果如下图所示:
从调试窗口可以看到,16并没有插入到我们给的s.begin()位置上,而是根据二叉搜索树的插入逻辑来进行的插入。而且对于重复插入的值,并没有多次插入到set结构中
6、erase

- 第一个删除某个迭代器位置
- 删除某个key值,观察其返回值是size_t类型,返回的是删除key值的个数,明明在set中key值只有一个或没有,这里要是想要返回的话直接返回bool值即可,何必返回删除这个key值的个数呢?这是为了后面的multiset做准备,因为multiset可以允许有多个相同的key值,而set和mulitset的关联性很大,所以为了接口的统一性将这里的删除值为key的erase的操作的返回值设置为了返回删除key值的个数,在set中当有这个key值的时候删除对应的节点成功返回1,当没有这个key值的时候删除对应节点失败返回0
- 删除某个迭代器区间
cpp
int main()
{
set<int> s({ 10, 7, 12, 20, 6, 4 });
for (auto& e : s)
cout << e << " ";
cout << endl;
s.erase(s.begin());
for (auto& e : s)
cout << e << " ";
cout << endl;
s.erase(7);
for (auto& e : s)
cout << e << " ";
cout << endl;
s.erase(s.begin(), s.end());
for (auto& e : s)
cout << e << " ";
cout << endl;
return 0;
}

7、swap/clear

- swap:交换两个set对象内容
- clear:清空set中数据
cpp
int main()
{
set<int> s1({ 10, 7, 12, 20, 6, 4 });
set<int> s2;
s2.swap(s1);
s2.clear();
return 0;
}

8、find

- find是获取val元素的迭代器,如果没有查找到那么返回end()迭代器,所以我们在进行使用find查找的迭代器的时候,应该使用if语句判断一下是否不等于end()之后在进行后序对迭代器的操作,那么find就可以与erase或insert进行搭配使用
- 库里也有find,为什么set还要提供一个find函数呢?因为库里的是给出区间遍历查找,时间复杂度为O(N),set底层是一个平衡二叉搜索树,所以成员函数find的查找效率高为O(logN)
cpp
int main()
{
set<int> s({ 10, 7, 12, 20, 6, 4 });
auto it = s.find(7);
if (it != s.end())
cout << "找到了:" << *it << endl;
else
cout << "没有该元素,请继续后续操作" << endl;
return 0;
}

9、count

- count:返回key值对应的个数,返回值是size_t类型的,在set中key要么是1个要么是0个,这里的设计是为了和multiset保持接口的一致性,multiset允许多个key值存在,所以当myltiset调用count的时候可能key值对应的个数会有多个,为了set和multiset接口的统一性,我们给set也提供一个count接口,count接口在set中可以用于判空
- count不常用,因为set有一个find就足够了
10、lower_bound/upper_bound

- lower_bound是下界(>=)的意思,即寻找比给定key值大于等于的值的位置的迭代器,有等于优先是等于,其次是大于,如果找不到那么返回end()
- upper_bound是上界(>)的意思,即寻找比给定key值大于的值的位置的迭代器,如果找不到那么返回end()
- lower_bound和upper_bound一般结合删除一起使用
cpp
int main()
{
set<int> s({ 10, 7, 12, 20, 6, 4 });
// 返回大于等于10
auto it1 = s.lower_bound(10);
//返回大于20
auto it2 = s.upper_bound(20);
//删除的是[10, 20]之间的数据
s.erase(it1, it2);
for (auto& e : s)
cout << e << " ";
cout << endl;
}

11、equal_range

- equal_range:去寻找和key值相等序列的开头和结尾位置的迭代器,在set中key值只有一个或0个,怎么还会有和key值相等的序列呢?其实这里同样是为了和multiset保持接口的一致性,即multiset中可以有多个相同的key值,即使用equal_range去找key值会找到的是一段序列
三、multiset和set的差异
multiset和set的使用基本完全类似,主要区别点在于multiset支持值冗余,那么
insert/find/count/erase都围绕着支持值冗余有所差异
1、insert
cpp
int main()
{
multiset<int> s;
s.insert(4);
s.insert(1);
s.insert(4);
s.insert(3);
for (auto e : s)
cout << e << " ";
cout << endl;
return 0;
}
2、find
cpp
int main()
{
multiset<int> s({4, 1, 4, 3});
int x;
cin >> x;
auto pos = s.find(x);
while (pos != s.end() && *pos == x)
{
cout << *pos << " ";
pos++;
}
cout << endl;
return 0;
}
3、count
cpp
int main()
{
multiset<int> s({4, 1, 4, 3});
int x;
cin >> x;
auto pos = s.find(x);
while (pos != s.end() && *pos == x)
{
cout << *pos << " ";
pos++;
}
cout << endl;
cout << s.count(x) << endl;
return 0;
}
4、erase
cpp
int main()
{
multiset<int> s({4, 1, 4, 3});
int x;
cin >> x;
auto pos = s.find(x);
while (pos != s.end() && *pos == x)
{
cout << *pos << " ";
pos++;
}
cout << endl;
cout << s.count(x) << endl;
s.erase(x);
for (auto& e : s)
cout << e << " ";
cout << endl;
return 0;
}
- 相比set,multiset是排序,不会去重
- 相比set,multiset会存在多个x值,find函数找的是中序的第一个x
- multiset的count函数会返回x的个数
- multiset的erase函数会删除所有的x值
四、map的使用
1、map介绍
- map的Key 就是map底层关键字的类型 ,T 是map底层value的类型,map默认要求Key支持小于比较,如果不支持或者需要的话可以自行实现仿函数传给第二个模版参数,map底层存储数据的内存是从空间配置器申请的。
- ⼀般情况下,我们都不需要传后两个模版参数。map底层是用红黑树实现,增删查改效率是O(logN),迭代器遍历是走的中序,所以是按key有序顺序遍历的
2、pair类型介绍
- map底层的红黑树节点中的数据,使用pair<Key, T>存储键值对数据
- 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)
{}
};
- make_pair是一个函数模板,传入两个类型的值,自动推导pair类型,在进行map插入等操作时不需要声明对象,且该模板是inline
cpp
template <class T1,class T2>
inline pair<T1,T2> make_pair (T1 x, T2 y)
{
return ( pair<T1,T2>(x,y) );
}
3、构造函数

cpp
#include <map>
int main()
{
map<string, string> dict1;
pair<string, string> arr[] = {make_pair("left", "左边"), make_pair("right", "右边"), make_pair("string", "字符串") };
map<string, string> dict2(arr, arr + sizeof(arr) / sizeof(arr[0]));
map<string, string> dict3(dict2);
// initializer_list构造(外面的{}是initializer_list)
// 里面的{ "pair", "一对" }是多参数的构造,隐式类型转换成pair
map<string, string>dict4 = {{ "pair", "一对" }, {"right", "右边"}};
return 0;
}

4、operator=

- 赋值运算符重载,传入一个map类型或者传入initializer_list类型
cpp
int main()
{
map<string, string> dict1;
map<string, string> dict3;
pair<string, string> arr[] = { make_pair("left", "左边"), make_pair("right", "右边"), make_pair("string", "字符串") };
//pair<string, string> arr[] = { {"left", "左边"}, {"right", "右边"}, {"string", "字符串"} };
map<string, string> dict2(arr, arr + sizeof(arr) / sizeof(arr[0]));
dict1 = dict2;
// initializer_list赋值(外面的{}是initializer_list)
// 里面的{ "pair", "一对" }是多参数的构造,隐式类型转换成pair
dict3 = {{ "pair", "一对" }};
return 0;
}
- 首先初始化dict1、dict2、dict3
- 将dict2赋值给dict1,dict1的size变成3,在监视窗口可以看出dict1中有了{"left", "左边"}, {"right", "右边"}, {"string", "字符串"}
- { "pair", "一对" }多参数的构造----隐式类型转换成pair,然后将pair类型通过外面的{}(initializer_list)赋值给dict3
5、迭代器begin、end以及范围for
- map的迭代器与set不太一样,map中存储的是一个pair类型,迭代器it进行解引用拿到的是pair这个对象,那么我们本质想要访问的是pair这个结构对象中存储的first(key),second(value),我们可以使用 . 和->的方式访问结构的成员变量,这样就可以访问到pair这个结构对象中存储的first(key),second(value)了,我们一般使用->方式访问
- 支持迭代器就支持范围for,因为map中存储的pair类型数据,拷贝消耗大,因此我们使用&,且使用范围for不修改pair,因此加入const修饰
cpp
int main()
{
// initializer_list构造(外面的{}是initializer_list)
// 里面的{ "pair", "一对" }是多参数的构造,隐式类型转换成pair
map<string, string>dict = { { "pair", "一对" }, {"right", "右边"} };
//map<string, string> ::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << " " << it->second << endl;
it++;
}
cout << endl;
for (const auto& kv : dict)
cout << kv.first << " " << kv.second << endl;
return 0;
}

6、empty/size
map的empty和size与set的一样都是分别返回容器是否为空,以及容器大小
cpp
int main()
{
map<string, string>dict = { { "pair", "一对" }, {"right", "右边"} };
cout << dict.empty() << endl;
cout << dict.size() << endl;
return 0;
}
7、insert


- insert插入一个pair类型的值<key,value>,如果插入成功,那么返回一个pair类型<iterator,bool>,其中第一个数据first是插入位置的迭代器,第二个位置是是否插入成功返回bool类型,由于map中的key值不能重复,如果key值已经有了,那么返回的是原key对应位置的迭代器以及false;如果key值没有,那么返回的是新插入key对应位置的迭代器以及true。
- return不能返回两个值,所以如果想返回多个值,必须放在一个结构中进行返回,这里是放在pair中进行返回
- 在迭代器位置(这个迭代器位置对于编译器来说只是一个建议位置,具体插入在哪里还是要看原map的插入规则)插入一个pair类型的值<key,value>,如果插入成功,返回插入的迭代器,插入失败,返回原key对应位置的迭代器
- 插入一段迭代器区间
cpp
int main()
{
map<string, string>dict = { { "pair", "一对" }, {"right", "右边"} };
dict.insert({ "left", "左边" });
dict.insert(dict.begin(), { "z", "字母z" });
return 0;
}
在dict.begin()插入一个pair类型数据,但是实际并不是插入在begin()位置,而是根据map插入规则插入到对应的地方
8、find

- map中查找使用key值查找,与value值无关,在插入时,只要key值相同,即使value值不同也会视为相同的数据
- find传入key,如果key存在那么返回key对应位置的迭代器,如果key值不存在,那么返回end()
cpp
int main()
{
map<string, string>dict = { { "pair", "一对" }, {"right", "右边"} };
dict.insert({ "left", "左边" });
dict.insert(dict.begin(), { "z", "字母z" });
auto it = dict.find("left");
if (it != dict.end())
cout << it->second << endl;
else
cout << "没有对应的key值,请输入存在的key值" << endl;
return 0;
}
8、erase

- erase可以传入一个迭代器,也可以传入一个迭代器区间删除
- erase传入一个key值来删除key值对应数据,并返回删除了多少个key看,这和set设计思路是一样的为multimap接口做准备
cpp
int main()
{
map<string, string>dict = { { "pair", "一对" }, {"right", "右边"} };
dict.insert({ "left", "左边" });
dict.insert(dict.begin(), { "z", "字母z" });
size_t ret = dict.erase("z");
dict.erase(dict.begin());
cout << "ret = " << ret << endl;
for (const auto& kv : dict)
cout << kv.first << " " << kv.second << endl;
return 0;
}

9、swap/clear

cpp
int main()
{
map<string, string>dict1 = { { "pair", "一对" }, {"right", "右边"} };
map<string, string> dict2;
dict2.swap(dict1);
dict2.clear();
return 0;
}
- 交换dict1和dict2数据,dict2的size变为2,dict1的size变为0
- dict2使用clear函数,size变为0
10、operator[](重点理解)

- map的operator[]是传入一个key值,返回的是对应的value的引用,因为返回的是引用,因此可以修改value值
operator[]底层就是调用insert,countMap.insert({key,V()}),插入可能成功,可能失败。插入成功说明map结构中原来没有key,所以就会直接插入,返回一个pair类型键值对<iterator, bool>,iterator是新插入key的iterator;插入失败说明map结构中已经有了key,返回一个pair类型键值对<iterator, bool>,iterator是原key的iterator迭代器。- 所以
无论插入成功还是失败,最终都会返回一个pair的键值对<iterator, bool>,pair的first都是指向key的iterator

- 所以不管插入成功还是失败,我们都能拿到指向key的迭代器,因此operator[]的实现就像下面这段伪代码一样,通过拿到插入后的指向key的迭代器再访问second数据就是value值,将其引用返回
cpp
V& operator[](const K& key)
{
// 指向key的迭代器
pair<iterator, bool> ret = insert({ key, V() });
// 指向key的迭代器-> pair的second(key对应的value)
return ret.first->second;
}
map的功能:
- 插入(基本不会这样用):当map中没有"left",那么会插入"left"成功,value值调用对应类型的默认构造
插入+修改:"right"没有在map数据中,插入"right",value值调用对应类型的默认构造,operator[]返回value值的引用,修改为"右边"
修改:"left"已经存在,因此不会插入成功,但是会返回value值的引用,将""修改为"左边"
- 查找:dict["left"],left已经存在,插入失败,但是插入失败也能拿到指向"left"的迭代器,再返回value的引用,因此就能查找到"left"对应的value值
cpp
int main()
{
map<string, string> dict;
//插入
dict["left"];
//插入+修改
dict["right"] = "右边";
//修改
dict["left"] = "左边";
//查找
cout << dict["left"] << endl;
return 0;
}
五、multimap与map的差异
multimap和map的使用基本完全类似,主要区别点在于multimap支持关键值key冗余 ,那么insert/find/count/erase都围绕着支持关键值key冗余有所差异,这里跟set和multiset完全⼀样,比如find时,有多个key,返回中序第一个。其次就是multimap不支持[],因为支持key冗余,[]就只能支持插入了,不能支持修改。
cpp
int main()
{
map<string, string> dict;
dict.insert({ "pair", "一对" });
dict.insert({ "right", "右边" });
dict.insert({ "left", "左边" });
multimap<string, string> dict1;
dict1.insert({ "pair", "一对" });
dict1.insert({ "pair", "一对" });
dict1.insert({ "left", "左边" });
return 0;
}











