【学习笔记】STL学习总结
前言
鉴于笔者的 STL 掌握情况实在是弱(指几乎人人 AK 的练习题只 A 了 \(20\%\)),所以就滚过来写总结了。
因为这是大坑,就慢慢填。
写作计划
-
STL 的常用容器
\(\text{(1) stack}\) 栈
\(\text{(2) queue}\) 队列
\(\text{(3) priority_queue}\) 优先队列
\(\text{(4) vector}\) 不定长数组
\(\text{(5) pair}\) 二元组模板
\(\text{(6) map}\)
\(\text{(7) set}\) 特殊的 \(\text{map}\)
\(\text{(8) deque}\) 双向队列
\(\text{(9) list}\) 双向链表
\(\text{(10) bitset}\) 特殊的 \(\text{set}\)
-
STL 的常用函数
-
如何访问容器里的元素?
-
重载运算符 & 迭代器
-
练习题
-
哪些情况适合用哪些容器?
-
STL 易错点
1. STL 的常用容器
(1) stack 栈
栈就类似于一个放羽毛球的桶,只能从最上面放取物品,而且先放进去的后拿出来,即 FILO(First In Last Out)。
因为这是 STL 专题,所以只讲 STL 的实现方法。
实现需要的头文件:#include<stack>
注意:所有 STL 的头文件都需要加 using namespace std;
!这个也不例外!
栈的所有操作时间复杂度都为 \(O(1)\)。
num1.定义
格式:stack<数据类型> 栈名;
注意这里的数据类型不仅可以填类似于
int
、char
、double
之类的,还可以填结构体,甚至你往里面再套一个 STL 的容器也可以(那岂不是可以无限套娃了嘛)。
num2.判空
格式:栈名.empty()
当这个栈为空的时候,返回
true
,反之返回false
。
需要特别注意的是,STL 容器的所有操作函数后面都有括号,不管需不需要传参。
num3.长度
格式:栈名.size()
返回元素(即数据)的个数。
num4.元素入栈
格式:栈名.push(入栈元素)
元素入栈后,会成为新的栈顶元素。如果您需要在元素入栈的同时求解原来的栈顶元素,请先执行返回栈顶元素的操作。
请注意入栈元素类型要和栈定义的类型一致,不然会 RE。其余容器同理。
num5.元素出栈
格式:栈名.pop()
栈顶元素出栈。
注意当栈为空的时候,执行此操作会 RE。注意后面所有容器的删除元素指令都同理。
num6.求栈顶元素的值
格式:栈名.top()
注意和出栈指令的区分。
某些求容器内元素的指令在容器为空的条件下会 RE,但是另一些不会。现在我们默认它会 RE,下面有特殊情况我会做特殊说明。
(2) queue 队列
队列是先进后出,简写为 FIFO(First In First Out)。就像我们要排队打饭一样,最后来的人只能站在最后面等,先来排队的人可以先吃到饭。我们要做优雅的宏帆人, 不能插队。
实现需要的头文件:#include<queue>
普通队列的所有操作时间复杂度都为 \(O(1)\)。
num1.定义
格式:queue<数据类型> 队名;
num2.判空
格式:队名.empty()
num3.长度
格式:队名.size()
num4.元素入队
格式:队名.push(入队元素)
类似于走到一个队然后站在最后排队等待打饭,元素入队后,会成为队列的最后一个元素。
num5.元素出队
格式:队名.pop()
类似于拿到了饭,要走出队列去吃饭,此时出队的元素是原来队里最前面的元素。
num6.求队首元素
格式:队名.front()
num7.求队尾元素
格式:队名.back()
(3) priority_queue 优先队列
优先队列相较于普通的队列,其优势就在于可以随时对元素排序,在某些条件下十分方便。当然,这么方便的排序肯定是要付出代价的,代价就在于时间复杂度,优先队列的某些操作时间复杂度是 \(O(logn)\)。
需要的头文件:#include<queue>
(和 #include<vector>
)
num1.定义
格式:priority_queue<数据类型,容器<容器数据类型>,排序方式<排序数据类型> /*这里有一个空格*/>
普通版
有三个部分,我们一个一个来看。
第一个部分是队列的数据类型,意义和普通队列是一样的,这里不作赘述。
第二个部分是优先队列的实现方式。这里建议使用vector
,后面的类型和前面填成一样的。
第三个部分是优先队列的排序方式,后面的类型填队列的类型即可。排序方式有两种:库函数和自定义排序函数。
系统自带的函数有less
和greater
两种常用的。less
为大顶堆,即最大的排前面,按照数字降序排列;greater
为小顶堆,与大顶堆相反。
如果定义优先队列的时候不定义排序方式,系统会默认为大顶堆的排序方式。你甚至可以不写第二个部分。即,下面两种写法是等价的。
1.priority_queue<int> q;
2.priority_queue<int,vector<int>,less<int> > q;
进阶版——自定义排序方式
我们知道sort
是可以自定义cmp
函数的,而优先队列也可以实现这个功能。不过优先队列实现有一些麻烦。
Step1——创立cmp
函数。这个应该没什么好说的吧,和sort
的那个是一样的,不过要注意cmp
的类型不是bool
,而是struct
。
Step2——重载运算符。在cmp
里重载括号:
template <typename T>
struct cmp{
//重载 () 运算符
bool operator()(T &a,T &b){
return abs(a)<abs(b);
}
};
最上面那一行是很重要的。
虽然我也不知道它有什么用,但是我知道只有这么写才能成功。
接着就是使用了。使用很简单,和less
等一样。
这里甩一个代码。
#include<cstdio>
#include<queue>
#include<cmath>
using namespace std;
template <typename T>
struct cmp{
//重载 () 运算符
bool operator()(T &a,T &b){
return abs(a)<abs(b);
}
};
priority_queue<int,vector<int>,cmp<int> > q;
int main(){
int n,m;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&m),q.push(m);
while(!q.empty()){ //PS:仔细观察输出,你可能会发现坑点
printf("%d ",q.top());
q.pop();
}
return 0;
}
是的,你会发现这个程序很坑
如果您运行了这个程序,就会发现,它是按照绝对值从大到小排的。
但是我上面的cmp
写的是abs(a)<abs(b)
,这说明,它和sort
的cmp
还真的不一样,是反的 qwq。
num2.判空
格式:队名.empty()
num3.长度
格式:队名.size()
num4.元素入队
格式:队名.push(入队元素)
此操作时间复杂度为 \(O(logn)\)
num5.元素出队
格式:队名.pop()
这个时间复杂度我还真不清楚,不过应该是 \(O(1)\) 吧……
num6.查找队首元素
格式:队名.top
这里是
top
!这里是top
!这里是top
!不要打成front
了!!1
以上是以前学过的内容,下面来看新学的。
(4) vector 不定长数组
\(\text{vector}\) 是不定长数组,既然都是数组了,那肯定有一些数组的特征。而它又是不定长的,可以根据存的东西的数量改变长度,比一般的数组方便,而且它的操作大多数时间复杂度都是 \(O(1)\),不容易超时。要说它有什么缺点的话,那就是内存大,容易 \(\text{MLE}\)。
需要的头文件:#include<vector>
num1.定义
格式:
-
vector<数据类型> 不定长数组名;
-
vector<数据类型> 不定长数组名 {初始化的元素,用 “,” 隔开};
-
vector<数据类型> 不定长数组名(定义长度);
话说第三个跟普通数组有什么区别吗?
Upd:有区别的,比如您要定义一个二维数组,第一行长度是 \(1\),后面的每一行长度都是上一行的两倍,此时用 \(\text{vector}\) 的数组比起普通的二维数组就大大节省了空间。
num2.访问元素
格式:不定长数组名[下标]
和普通的数组是一样的很方便,注意下标是从 \(0\) 开始存。
num3.判空
格式:不定长数组名.empty()
num4.长度
格式:不定长数组名.size()
num5.尾端插入值
格式:不定长数组名.push_back(插入的值)
相当于在数组末尾加一个值,此时 \(\text{vector}\) 会自动扩容。
num6.尾端删除值
格式:不定长数组名.pop_back()
相当于在数组末尾清空一个值。
话说为什么 \(\text{vector}\) 没有在数组最前面插入删除的操作呢?
其实这个问题很简单,毕竟人家 \(\text{vector}\) 功能不管再怎么强大,本质上终究是个数组嘛!您见过谁在数组头插入删除元素?
num7.清空
格式:不定长数组名.clear()
把 \(\text{vector}\) 的长度变成 \(0\),即清空,注意时间复杂度为 \(O(n)\)。
num8.修改长度
格式:不定长数组名.resize(目标大小)
在修改了 \(\text{vector}\) 的大小后,保留的是哪些元素呢?
实验为证。
代码:
#include<cstdio>
#include<vector>
using namespace std;
vector<int> a;
int main(){
//a.resize(10); //注释1
for(int i=1;i<=15;i++) a.push_back(i);
//a.resize(10); //注释2
for(int i=0;i<10;i++) printf("%d ",a[i]);
return 0;
}
如果把注释 \(2\) 删掉,那么什么事都不会发生;
但是如果把注释 \(1\) 删了,那么输出的就全部都是 \(0\)。
(另:这玩意儿的时间复杂度也是 \(O(n)\))
(5) pair 二元组模板————map 的前置芝士
pair
的学名叫二元组模板,就像有两个参数的结构体一样,用法也差不多(但是 pair
功能更多一些)。
pair
它不是一个容器,但是它是一个重要容器———— map
(以及它的变形)的重要组成部分。
需要的头文件:#include<utility>
num1.定义
格式:
pair<数据类型1,数据类型2> 二元组模板名;
pair<数据类型1,数据类型2> 二元组模板名(第一个数据,第二个数据);
num2.组合
格式:make_pair(第一个数据,第二个数据)
可以把两个数据“做成”一个 \(\text{pair}\),方便放入一些存 \(\text{pair}\) 的容器中。不过我觉得还是直接定义来得快些。
num3.访问元素
格式:
第一个:二元组模板名.first
第二个:二元组模板名.second
很直白的名字,可以对两个元素进行修改。
num4.比较大小
格式:二元组模板名1 < (>,<=,>=,!=,==) 二元组模板名2
排序的方式:
first
为第一关键字,second
为第二关键字。意思就是说:当first
不相同时,按照first
排序;first
相同时,按照second
排序。当两个元素都相等时,两个 \(\text{pair}\) 相等。
注意此时的 \(\text{pair}\) 竟然可以再套别的 STL 容器了!但是我并不知道它是怎么排序的了……容器内部再从大到小?(应该是吧……)
实验代码:
#include<cstdio>
#include<vector>
#include<cstring>
#include<utility>
using namespace std;
pair<int,vector<double> > a,b;
int main(){
a.first=1;
b.first=1;
for(double i=0.5;i<=2.5;i+=0.5) a.second.push_back(i);
for(double i=1.5;i<=4.5;i+=1.0) b.second.push_back(i);
printf("%s",a>b?"a is bigger!":"b is bigger!");
return 0;
}
另外,还有个元组容器 tuple
可以存多个元素,头文件是 #include<tuple>
。但是因为它和结构体差不多,我们一般使用结构体实现,就不会用它了。大家了解即可。
(6) map
不知道中文是什么就没写,毕竟我总不可能填个“地图”上去吧……
map
存的是 pair
,pair
当中的两个元素在 map
中分别叫“键”和“值”。map
不能修改里面的元素(和栈、队列一样),但是可以用删除原元素再添加新元素的方式实现,其效果和修改元素是一样的(就是时间复杂度大了点(雾))。
那有人就会说了:为什么效果一样呢?如果那个元素不是在最后,那顺序就不一样了!
emm…… 那是因为 map
是可以自动排序的啊……
map
排序的方式是依据 pair
的排序方式来排序的(毕竟里面装的是 pair
嘛),不定义时默认为 less
(升序),也可以使用 greater
或自定义等排序。
自定义排序再前面优先队列的时候已经讲过了,这里不多赘述。
Upd:其实还是有点区别的,不过我不想写了,所以大家可以参考一下这篇博客鸭~
map
所有的操作时间复杂度都是 \(O(\log_2 n)\),小心 \(\texttt{TLE}\)。
(PS:不知道 \(\log_2\) 是啥的:若 \(2^x=y\),则 \(\log_2 y=x\))。
num1.定义
格式:
-
map<第一个数据类型(PS:键数据类型),第二个数据类型(PS:值数据类型)> map 名;
-
map<第一个数据类型(PS:键数据类型),第二个数据类型(PS:值数据类型)> map 名{{数据1.1,数据1.2},{数据2.1,数据2.2}......};
num2.访问元素
格式:map 名[key]
这个操作的意思是返回键(第一个元素的值)为
key
的元素。因为有这个东西,所以,一个map
容器里不能存键值相同的pair
,但是值(第二个元素的值)却可以。
敲黑板啦!(战术性清嗓)
我们之前说过,如果一个空的容器访问/删除元素会 RE,可是map
偏偏就是个特例!那为什么呢?
因为在访问map
中并不存在的元素时,map
会自动插入一个你所访问的之前不存在的元素,所以等你访问的时候系统就已经给您把这个元素弄到map
里面去了。
带来的影响就是,我们不能用!map 名[key]
来判断是否有键为key
的元素(因为如果没有的话系统也会新建)。对策是:使用find
或count
。
num3.添加元素
格式:
1.map 名[key,值]
意思是插入键为
key
,值为 {写“值”的那个地方的数据} 的pair
。插入完成之后会自动排序。
2.map 名.insert(一个 pair 的名字)
map
也是可以直接插入pair
的哟 OvO。
num4.删除元素
格式:map 名.erase(key)
删除键为
key
的元素。
num5.判断有无元素
格式:map 名.count(key)
统计此容器中键为
key
的元素数量。但是因为一个 \(\text{map}\) 里面所有元素的键互不相同,所以这个数量只有可能是 \(0/1\)。
在插入一个元素之前记得先判断一下这个key
值是不是已经有了哦~
num6.查找元素
格式:map 名.find(key)
注意返回的是迭代器。如果没有的话,就会返回
map
内存中最后那个的下一个。
num7.二分
格式:
-
map 名.lower_bound(key)
-
map 名.upper_bound(key)
是的你没看错这玩意儿还能被二分
这说明map
要自动排序。
第一个lower_bound
,是查找此map
中第一个大于等于key
的键值对的对应迭代器。找不到同上。
第二个upper_bound
,和上面差不多,不过是找第一个大于key
的值。
另外还有一个神奇的容器叫 multimap
,头文件也是 #include<map>
。它可以有多个相同的键。
但是建议大家不要用!这个东西是个玄学,当你访问一个键所对应的值时,如果这个键在容器中存在多个,它会给您返回随机的一个,奇奇妙妙的随机数又增加了。
(7) set ————特殊的 map
set
也是 map
的一种,不过它存的 pair
键和值必须一样,所以只有一个值(不是键!)。和 map
一样,set
不能修改元素。
4.练习题
(1)关于栈的出题方向
因为栈是后进先出的,所以出题人会利用这个特征来出题。
这道题是一道典型的栈的题目,如果您非要用区间 DP 我也没办法, 但是显然这道题的最简便做法应该是用栈。
那么为什么是用栈来做呢?
我们来观察正确字符串的特征:
-
如果 \(A\) 和 \(B\) 都为正确的运算式,则 \(AB\) 也为正确的运算式。
-
如果 \(A\) 为正确的运算式,则 \((A)\) 及 \([A]\) 都为正确的运算式。
然后我们需要确定,当能确定这个式子不是正确的的时候,就直接输出“no”。
首先看第一点,我们需要考虑两个正确运算式的合并。如下图,我们假设 \(A\) 的两端字符编号为 \(1,2\),\(B\) 的两端字符编号为 \(3,4\),那么,如果 \(A\) 和 \(B\) 是正确的式子,可以得出,\(1,2\) 匹配,\(3,4\) 也匹配。
观察这张图,不难看出,如果 \(A,B\) 都是正确的,那么 \(A,B\) 除去两端也会是正确的式子。而观察栈,如果 \(A,B\) 中间那两个式子是错误的,那么我们在判断那两个式子中的括号时是可以判断出来的。
那么我们从最简单的情况开始考虑。假如说有一对连续的匹配括号,那么,在右括号入栈的时候,栈顶元素是左括号,可以判断出配对然后两个元素一起出栈。如果不是,那么这个式子就一定不是正确的。
那为什么一定不是呢?这还得从中间的式子出发思考。试想一下,如果中间的式子是正确的,那么中间那些括号一定被消完了。如果没有消完,就是式子不对。
而这一张图是考虑的第二种情况。注意到 \(top\) 和 \(i\) 之间的所有元素都必须匹配才可以。
栈可以有效避免这种情况的错误:([)]
,因为栈只能从一边进出。同时抛开两端去验证中间的思想有一些递归的感觉,而递归的模型就是一个栈。这就是我们用栈来解决这个问题的原因。
综上,思路就是这样的:
1.循环从前往后遍历字符串,字符串的元素从前往后依次入栈。
2.如果遇到左括号("("或"["),入栈等待配对。
3.如果遇到右括号,判断栈顶元素是否和这个括号匹配:
3.1.如果匹配,匹配的左括号出栈;
3.2.如果不匹配,直接输出“No”。
4.循环结束后,判断栈是否为空;如果为空输出“Yes”,反之输出“No”。
按照这个过程用栈模拟即可。
Code
这个代码卡时限 c++
和 c++(NOI)
,用 c++11(Clang)
可以过。(也不知道为啥,但 c++11(Clang)
真的跑得飞快,\(26\) 毫秒就跑完了 qwq)。
#include<cstdio>
#include<stack>
#include<cstring>
using namespace std;
int n,len;
char a[105];
int main(){
scanf("%d",&n);
while(n--){
bool f=1;
stack<char> s;
scanf("%s",a+1);
len=strlen(a+1);
for(int i=1;i<=len;i++){
if(a[i]=='('||a[i]=='[') s.push(a[i]);
else if(!s.empty()&&(s.top()=='('&&a[i]==')'||s.top()=='['&&a[i]==']')) s.pop();
else{
f=0;
break;
}
}
if(s.empty()&&f) printf("Yes\n");
else printf("No\n");
}
return 0;
}
参考资料
mjl 的 PPT
就酱紫吧,如果哪里讲错了或者讲得不清楚请告诉我,谢谢。
还没写完呢你慌个 P