【学习笔记】STL学习总结

前言

鉴于笔者的 STL 掌握情况实在是弱(指几乎人人 AK 的练习题只 A 了 \(20\%\)),所以就滚过来写总结了。

因为这是大坑,就慢慢填。


写作计划

  1. 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}\)

  2. STL 的常用函数

  3. 如何访问容器里的元素?

  4. 重载运算符 & 迭代器

  5. 练习题

  6. 哪些情况适合用哪些容器?

  7. STL 易错点


1. STL 的常用容器

(1) stack 栈

栈就类似于一个放羽毛球的桶,只能从最上面放取物品,而且先放进去的后拿出来,即 FILO(First In Last Out)。

因为这是 STL 专题,所以只讲 STL 的实现方法。

实现需要的头文件:#include<stack>

注意:所有 STL 的头文件都需要加 using namespace std;!这个也不例外!

栈的所有操作时间复杂度都为 \(O(1)\)

num1.定义

格式:stack<数据类型> 栈名;

注意这里的数据类型不仅可以填类似于 intchardouble之类的,还可以填结构体,甚至你往里面再套一个 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,后面的类型和前面填成一样的。
第三个部分是优先队列的排序方式,后面的类型填队列的类型即可。排序方式有两种:库函数和自定义排序函数。
系统自带的函数有 lessgreater 两种常用的。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),这说明,它和 sortcmp 还真的不一样,是反的 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.定义

格式:

  1. vector<数据类型> 不定长数组名;

  2. vector<数据类型> 不定长数组名 {初始化的元素,用 “,” 隔开};

  3. 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.定义

格式:

  1. pair<数据类型1,数据类型2> 二元组模板名;
  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 存的是 pairpair 当中的两个元素在 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.定义

格式:

  1. map<第一个数据类型(PS:键数据类型),第二个数据类型(PS:值数据类型)> map 名;

  2. 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 的元素(因为如果没有的话系统也会新建)。对策是:使用 findcount

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.二分

格式:

  1. map 名.lower_bound(key)

  2. 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 我也没办法, 但是显然这道题的最简便做法应该是用栈。

那么为什么是用栈来做呢?

我们来观察正确字符串的特征:

  1. 如果 \(A\)\(B\) 都为正确的运算式,则 \(AB\) 也为正确的运算式。

  2. 如果 \(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

priority_queue容器适配器实现自定义排序

C++:map自定义排序


就酱紫吧,如果哪里讲错了或者讲得不清楚请告诉我,谢谢。

还没写完呢你慌个 P

posted @ 2021-07-24 21:35  Saiodgm  阅读(128)  评论(0编辑  收藏  举报