1. 序列容器和关联容器
1.1 序列容器
前面我们学习了stl的一些容器如:string、vector、list、deque、array、forward_list等,这些被称为序列式容器,他们的逻辑性结构是线性的,以线性排列,但是他们值没有很大的关联关系,交换一下也不会影响他们的结构。
1.2 关联式容器
关联式容器顾名思义,就是有关联的即两个位置之间的元素有紧密的关联,关联式容器也是用来存储数据,但它的结构是非线性的,如果位置交换一下那他的存储结构就被破坏了,关联式容器是按关键字来保存和访问的。类似的有:map/ set/ multimap/ multiset还有unordered_map/ unordered_set系列。
等会要讲的是map和set系列的结构,这两个的底层是红黑树,这里只是提及一下红黑树,迟点会有特定的文章进行讲解红黑树的底层以及实现。红黑树是一个平衡的二叉搜索树。set对应前面的二叉搜索树是key的搜索场景,而map是key/value的搜索场景。
ok,大概念容器已经介绍完了,我们现在开始进入正题。
2. set系列的使用
2.1 set和multiset参考文档:
很多时候中文网站的翻译都是通过软件等进行翻译的,有些意思用中文翻译过来意思就有点改变了,所以如果条件允许的话我们可以i尝试看一下英文的文档。
2.2 set类的介绍
首先我们上set的stl底层代码,然后来对这些代码进行解析一下,我们从底层了解一下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;
1、T就是set的底层关键字也就是我们前面二叉搜索树所说的key。
2、class Compare = less<T>就是接收仿函数的参数,这个给的默认是less,这个就是按从小到大进行排序,如果我们需要一个从大到小的话在实例化对象的时候加一个greater<int>就可以;如下代码所示:
//仿函数
#include<iostream>
#include<set>
using namespace std;
int main()
{
//从小到大进行排序
set<int> MySet = { 9,10,44,25,76,1,23,4 };
auto it1 = MySet.begin();
while (it1 != MySet.end())
{
cout << *it1 << " ";
it1++;
}
cout << endl;
//从大到小进行排序
set<int, greater<int>> MySet2 = { 9,10,44,25,76,1,23,4 };
for (auto e : MySet2)
{
cout << e << " ";
}
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
3、set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数,但这个语法是在后面学到,如果想要了解的话可以去看一下C++的文档。
4、set的实现和我们前面实现的二叉搜索树有点不同的是,set底层是使用红黑树进行实现(平衡树),增删查改的效率是O(logN),后面我们也会讲到红黑树的实现。
5、set的迭代器走的是中序遍历,所以用迭代器打印数据是有序的。
2.3 set的构造和迭代器
2.3.1 set的构造
1、无参构造,首先我们看一下底层源码,给了个缺省参数key_compare()而这个缺省参数给的是一个空的值,无参构造的作用就是当你不知道要放什么值进去的时候你就可以先用无参构造,等未来你确定需要插入什么值的时候再调用。所以下面的代码打印不出任何东西。
#include<iostream>
#include<set>
using namespace std;
int main()
{
//int无参构造
set<int> MySet;
for (auto e : MySet)
{
cout << e << " ";
}
set<string> MySet2;
for (auto e : MySet2)
{
cout << e << " ";
}
return 0;
}
2、 迭代器范围构造
迭代器范围构造就是给定两个迭代器区间然后把迭代器区间的数据都放到set里面。
使用代码如下:
//迭代器区间构造
#include<vector>
#include<set>
#include<iostream>
using namespace std;
int main()
{
vector<int> vec = { 5, 3, 4, 1, 2 };
set<int> MySet(vec.begin(), vec.end());
for (auto e : MySet)
{
cout << e << " ";
}
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
3、 拷贝构造,和前面的容器一样,就是拷贝一个容器的值给另一个容器;
使用代码如下:
//拷贝构造
#include<iostream>
#include<set>
using namespace std;
int main()
{
set<string> MySet = { "hahaha", "hohohoho","hihihihi" };
set<string> s1(MySet);
for (auto e : s1)
{
cout << e << " ";
}
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
4、initializer_list构造,就是我们上面一直使用的,用一个大括号括起来进行初始化,但是这个其实实质上是调用了重载赋值运算符来实现构造,我们下面两个都会展示出来;
//拷贝构造
#include<iostream>
#include<set>
using namespace std;
int main()
{
//使用initializer_list初始化
//第一种
// 第一种是构造使用initializer_list初始化
set<int> s({ 5,2,7,3,8 });
//第二种
//第二种是使用赋值构造进行传入initializer_list初始化
/*set<int> s = { 5,2,7,3,8 };*/
//while遍历
set<int>::iterator it1 = s.begin();
while (it1 != s.end())
{
cout << *it1 << " ";
it1++;
}
cout << endl;
//范围for遍历
for (auto e : s)
{
cout << e << " ";
}
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
2.3.2 set的迭代器
set的迭代器是一个双向迭代器,双向迭代器就意味着可以“--”或者“++”的,而set也还实现了反向迭代器和正向迭代器,和前面的vector的使用方法类似,所以我们就直接上代码了;
#include<iostream>
#include<set>
using namespace std;
int main()
{
set<int> s({ 5,2,7,3,8 });
auto it1 = s.begin();
while (it1 != s.end())
{
cout << *it1 << " ";
it1++;
}
cout << endl;
auto rit1 = s.rbegin();
while (rit1 != s.rend())
{
cout << *rit1 << " ";
rit1++;
}
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
2.4 set一些基本接口的使用
2.4.1 insert的使用
插入单值的接口:
后面那个pair<iterator, bool>类型其实就是封装了另外一个结构体,这样他就可以返回两个返回值,单值插入,如果这个值存在的话那么就会给bool类型返回false,就不会进行插入,这里我们先大概了解一下,主要还是使用方法,如下代码所示:
#include<iostream>
#include<set>
using namespace std;
int main()
{
// 去重+升序排序
//set<int> s;
set<int, greater<int>> s;
s.insert(5);
s.insert(2);
s.insert(7);
s.insert(5);
s.insert(7);
s.insert(3);
//set<int>::iterator it = s.begin();
auto it = s.begin();
while (it != s.end())
{
// error C3892: “it”: 不能给常量赋值
//*it = 1;
cout << *it << " ";
++it;
}
cout << endl;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
插入initializer_list:
其实和用initializer_list构造初始化很像,直接上代码:
#include<iostream>
#include<set>
using namespace std;
int main()
{
set<string > s;
s.insert("hahaha");
s.insert("hohoho");
s.insert("hihihi");
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
迭代器区间插入:
也是和迭代器区间构造很相似,所以也是直接上代码吧哈哈哈哈:
#include<iostream>
#include<set>
#include<vector>
using namespace std;
int main()
{
vector<int>v1 = { 1,6,8,4,2,5 };
set<int> s;
s.insert(v1.begin(), v1.end());
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
2.4.2 find的使用
find的底层源码,find的返回值是iterator,如果找到val就返回迭代器,如果找不到就返回一个无效的迭代器,那么可能有人注意到之前在算法库里面也有一个find,为什么stl set里面还要实现一个find,因为算法库里的find是从头遍历到尾,时间复杂度为O(N),但set里实现的find的时间复杂度是O(logN)和我们前面二叉搜索树里实现的一样。
#include<iostream>
#include<set>
#include<vector>
using namespace std;
int main()
{
set<int> s({ 1,6,3,4,7,9,10 });
int x;
cin >> x;
//库里的find
//auto pos1 = find(s.begin(), s.end(), x);
//
//set自己实现的find
auto pos2 = s.find(x);
cout << *pos2 << " ";
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
如果说我们输入的值里面没有的话,就返回一个无效的迭代器,那么我们cout<<*pos2就会报错,因为对空指针进行解引用。
2.4.3 count的使用
count的底层源码,我门看他的功能,是返回val的个数,但是又因为set是不支持重复的值,那么这个count对于set来说只能起到查找的作用,如果找到返回的值为1,如果找不到返回的值为0,因为是返回查找val的个数,没有重复值那么就只有两个答案要么1要么0。
#include<iostream>
#include<set>
#include<vector>
using namespace std;
int main()
{
// set::count是查找树里面是否存在x值,存在则返回个数
set<int> s({ 1,3,5,2,7,8 });
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
int x;
cin >> x;
if (s.count(x))
{
cout << x << "在!" << endl;
cout << "count is:> " << s.count(x) << endl;
}
else
{
cout << x << "不在!" << endl;
}
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
2.4.4 erase的使用
erase的底层源码
首先是第一个,删除迭代器,这个和我们前面的vector和list里面的迭代器一样,都有一个共病,那就是迭代器失效,如果说我们传入了一个pos进行删除,虽然他会返回下一个值的迭代器,如果我们不用pos接收的话那一样是迭代器失效,但实际上就算是用pos接收了,也算迭代器失效,因为他的值的意义变了,如果你不知道树中的数据是什么的话删除了一个指向下一个的直接就不知道是什么了,我们这里测试一下,但不要这么用。
#include<iostream>
#include<set>
#include<vector>
using namespace std;
int main()
{
// set::count是查找树里面是否存在x值,存在则返回个数
set<int> s({ 1,3,5,2,7,8 });
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
auto pos = s.find(7);
pos = s.erase(pos);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
cout << *pos << endl;
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
然后是第二个删除val,不存在返回0,存在返回1,但是它和count一样,因为set里面不能存放相同的值,所以返回的要么是1要么是0。如下代码所示:
#include<iostream>
#include<set>
#include<string>
using namespace std;
int main()
{
set<int> MySet({ 1,5,6,4,9,2 });
//打印Set的数据
for (auto e : MySet)
{
cout << e << " ";
}
cout << endl;
int temp = 0;
//删除存在的值看看返回多少
temp = MySet.erase(1);
cout << "删除存在的值返回:> " << temp << endl;
temp = MySet.erase(100);
cout << "删除不存在的值返回:> " << temp << endl;
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
第三个erase就是删除一个迭代器区间,返回的值是last位置的值,因为last是闭区间比如说我要删除[0, 4)的数据,那么第四个数据就不会被删除,而是作为返回值进行返回如下图和代码所示:
#include<iostream>
#include<set>
#include<string>
using namespace std;
int main()
{
set<int> MySet({ 1,5,6,4,9,2 });
//打印Set的数据
for (auto e : MySet)
{
cout << e << " ";
}
cout << endl;
auto it1 = MySet.begin();
it1++;
it1++;
auto tmp = MySet.erase(MySet.begin(), it1);
for (auto e : MySet)
{
cout << e << " ";
}
cout<<endl;
cout << *tmp << endl;
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
2.4.5 lower_bound和upper_bound的使用
首先是lower_bound,看一下他的底层的返回值和参数,他就是传入一个k的值,然后返回一个不小于k的值的迭代器,也可以说返回一个大于或者等于k值的迭代器。
然后是upper_bound,他的返回值也是一个迭代器,他和上面不同的是,upper_bound返回的是一个大于value值的迭代器。
为什么要先把这两个介绍完再来看代码,原因是把这两个结合起来我们就可以实现删除一个区间的值,如下代码所示:
#include<iostream>
#include<set>
#include<string>
using namespace std;
int main()
{
std::set<int> myset;
for (int i = 1; i < 10; i++)
myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
//返回>=25
auto itlow = myset.lower_bound(25);
//返回>55
auto itup = myset.upper_bound(55);
myset.erase(itlow, itup);
for (auto e : myset)
{
cout << e << " ";
}
return 0;
}
这里是实现一个删除[25, 55)的值的操作;
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
3. map系列的使用(略讲)
map就对应了我们前面实现的key_value结构,就是给一个值对应另一个值,让key和value的值扯上关系,在map里面可以修改value的值,但不可以修改key的值,因为修改key关键字的值的话就会破坏树的结构,因为这棵树是按key的值来进行排序的。
因为map的set的大体结构类似,这里就不过多讲述他的构造的使用,我们这里略略讲一下map的insert、还有访问数据、还有operator[]的使用。
3.1 insert的使用(重点)
由于map的插入和set的插入不同,map需要插入两个值,那么为了方便,底层实现了一个名为pair的键值对进行存储两个值,如果要传的话直接用该类传入特定的模板然后传入即可;如下图可见
由此可见insert的参数是value_type,而value_type被typedef过的,实际上是pair<const key_type, mapped_type>,而key_type就是我们树里的key关键字,mapped_type就是value,如下单句代码所示:
dict.insert(pair<string, string>("last", "最后"));
参数说明白了,这里我们来讲一下他的返回值,等会下面的operator[]也会用到。
首先他的返回值也是pair类的,一个是迭代器,还有一个是bool类型,我们去看一眼文档
发现全是英文,不过不要紧,我直接一个总结分为两点:
1、如果key已经在map中,插入失败,返回pair<iterator, false>,这个iterator是key在map中的迭代器,false代表插入失败。
2、如果key不在map中,插入成功,返回一个pair<iterator, true>,这个iterator是key在map中的迭代器,true代表插入成功。
也就是说无论插入失败或者成功pair的first都会返回key所在的迭代器,那就意味着如果插入失败了,就会返回key的地址,这就充当了查找的作用,这个在接下来的operator[]也会用到。
可能有人不知道first和second是什么意思,这里直接一条代码说明:pair<first, second>,对应上面first就是返回迭代器的参数,second就是接收bool类型的参数。
如下代码是insert的使用:
//map的使用
#include<map>
#include<string>
#include<iostream>
using namespace std;
int main()
{
map<string, string> dict = { {"left", "左边"},{"right", "右边 "}, {"insert", "插入"}, {"string", "字符串"} };
//如果我们要插入数据的话和之前的都不太同
//第一种插入数据,直接实例化出pair对象然后插入
pair<string, string> kv1("first", "第一个");
dict.insert(kv1);
//第二种就是通过匿名对象
dict.insert(pair<string, string>("last", "最后"));
//第三种就是成员函数make_pair进行
dict.insert(make_pair("sort", "排序"));
//第四种就是通过initializer_list
dict.insert({ "auto", "自动的" });
map<string, string>::iterator it1 = dict.begin();
//还有一种
//这种其实就是重载了->运算符实际上的样子是
//it1.operator->()->first;
//重载->运算符 {it1.operator->()}大括号内实际上是返回了*it1其实和上面是一样的
while (it1 != dict.end())
{
//key不可以被修改
//it1->first += 'x';
//val可以被修改,我们可以理解为比如说tomato是马铃薯,value就是马铃薯
//key就是tomato,那么tomato也可以是土豆,value就改为了土豆;
it1->second += 'x';
cout << it1->first << ":" << it1->second << endl;
++it1;
}
return 0;
}
上面的几串insert是使用不同的方式插入
1、第一条insert因为pair是一个类,那我们就可以使用pair实例化出一个对象然后通过对象进行insert。
2、第二条insert是通过匿名对象进行传参,我们知道匿名对象的生命周期就在那一行代码,但实际上匿名对象作值传入不是传匿名对象的值作参数,其实传入的是一个临时值对匿名对象的值进行拷贝然后再传给insert。
3、第三条insert就是通过一个函数的返回值进行传参,实际上make_pair的返回值是pair<V1, V2>一个模板。如下图所示:
4、第四条insert就是通过initializer_list进行传值(重点推荐!!)方便好使。
3.2 map的迭代器
map打印数值和我们前面打印的都不太一样,因为里面有pair键值对,所以我们需要用it1->first或者second进行访问,具体的底层逻辑在后面红黑树实现会有讲到,因为这里我也不太知道哈哈哈哈哈,还有一种反问方式就是(*it1).first和second。
map<string, string>::iterator it1 = dict.begin();
//这是第一种打印的方法
//这里解引用it1返回的是pair的地址,那么我们就可以通过pair类找到first和second进行打印
//while (it1 != dict.end())
//{
// cout << (*it1).first << ":" << (*it1).second << endl;
// ++it1;
//}
//cout << endl;
//还有一种
//这种其实就是重载了->运算符实际上的样子是
//it1.operator->()->first;
//重载->运算符 {it1.operator->()}大括号内实际上是返回了*it1其实和上面是一样的
while (it1 != dict.end())
{
//key不可以被修改
//it1->first += 'x';
//val可以被修改,我们可以理解为比如说tomato是马铃薯,value就是马铃薯
//key就是tomato,那么tomato也可以是土豆,value就改为了土豆;
it1->second += 'x';
cout << it1->first << ":" << it1->second << endl;
++it1;
3.3 operator[]的介绍(重点)
这里面的方括号和我们以往在别的地方的方括号大有不同,以往的vector呀、string呀之类的方括号都是通过下标进行访问这一个功能。
而map的[]厉害得多,它有“插入”、“修改”、“查找”三个功能,这也是为什么我们前面要先讲完insert再来讲方括号,干说没用,我们看一下它实际上是怎么个走法;
但看这个也太那啥了,我们就来看一下大概的底层代码:
k就是key关键字,mapped_type()就是我们前面说的value,对应上key_value结构。首先插入k,就和前面insert说的,先判断k是否在map里如果在,那么first就接收到k的迭代器,然后second为true,反之。又因为返回的是mapped_type& 引用,那么这也是实现了修改的功能。
总结"[]"的“插入和查找”是insert给的,“修改”是自己的返回值是引用。
使用代码如下:
#include<iostream>
using namespace std;
// accessing mapped values
#include <iostream>
#include <map>
#include <string>
int main()
{
std::map<char, std::string> mymap;
//operator[]首先是查找‘a'位置是否被插入数据了,如果没有则直接insert一个进去
//如果‘a’中有数据了,就返回一个解引用,然后我们可以通过解引用进行修改
mymap['a'] = "an element";
mymap['a'] = "666";
mymap['b'] = "another element";
mymap['c'] = mymap['b'];
std::cout << "mymap['a'] is " << mymap['a'] << '\n';
std::cout << "mymap['b'] is " << mymap['b'] << '\n';
std::cout << "mymap['c'] is " << mymap['c'] << '\n';
//这里这个d需要注意一下,就是‘d’中是没有数据,那么就会调用里面的默认构造生成一个值
std::cout << "mymap['d'] is " << mymap['d'] << '\n';
std::cout << "mymap now contains " << mymap.size() << " elements.\n";
return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
4. multiset、multimap和map的对比
1、multiset和multimap一样,不需要包对应的头文件,只需要包set和map的头文件就可以用了。
2、有multi的都是可以存放相同的值,不会进行检查。
3、multimap和map使用点基本类似,主要区别点在于multimap支持关键值key冗余,insert/find/count/erase都围绕着支持冗余,如果调用erase的话就会把相同的值全部删掉,find就是找第一个。
4、这里set和multiset完全一样,比如find时有多个同样的key,返回中序遍历的第一个。
5、multimap的[]不支持一些功能例如修改,因为支持key冗余,[]就只支持插入不支持修改了。
5.OJ题map的使用
class Solution {
public:
Node* copyRandomList(Node* head)
{
//复制结点
map<Node*, Node*>mymap;
Node* CopyHead = nullptr;
Node* CopyTail = nullptr;
Node* cur = head;
while(cur)
{
if(CopyTail==nullptr)
{
CopyHead = CopyTail = new Node(cur->val);
}
else
{
CopyTail->next = new Node(cur->val);
CopyTail = CopyTail->next;
}
//cur和CopyTail扯上关系
mymap[cur] = CopyTail;
cur = cur->next;
}
//完成复制后
//开始放置random
cur = head;
Node* CopyCur = CopyHead;
while(cur)
{
if(cur->random==nullptr)
{
CopyCur->random = nullptr;
}
else
{
CopyCur->random = mymap[cur->random];
}
CopyCur = CopyCur->next;
cur = cur->next;
}
return CopyHead;
}
};
重点是mymap[cur] = CopyTail;和CopyCur->random = mymap[cur->random];之前我们是构建一个新链表然后连接进行实现的,而这个就可以让cur和CopyCur连上关系,mymap[cur->random]找到CopyCur的7, 然后再用13的random脸上mymap[cur->random]
这里只是简单说个过程方便我回忆。只需要看代码即可。
END!
后面就开始上强度了。