
第十六课 STL容器与常见数据结构
------C++自带的"数据结构工具箱"
这一课主要复习:
vector
stack
queue
priority_queue
set
map
并且把它们与:
数组
栈
队列
堆
集合
映射
联系起来。
一、先问同学们一个问题
我们有一盒彩色积木。
如果要求:
把所有积木放进去。
可以用一个普通箱子。
如果要求:
最后放进去的积木先拿出来。
普通箱子就不太方便。
如果要求:
先来的积木先拿出来。
又需要另外一种箱子。
如果要求:
每次都把"最重要"的积木先拿出来。
又需要另一种箱子。
所以:
不同的问题,需要不同的数据结构。
这就是数据结构最核心的思想。
二、什么是数据结构?
给出的定义是:
数据结构是计算机存储、组织数据的方式。
特别强调:
精心选择的数据结构可以带来更高的运行或者存储效率。
小学生可以这样来理解:
数据结构就是"怎样把一堆数据装起来、管理起来"。
例如:
数组 → 一排座位
stack → 一摞盘子
queue → 一列排队的人
priority_queue → 按优先级排列的队伍
set → 不重复的集合
map → 电话簿
三、STL是什么?
STL是:
Standard Template Library
中文一般叫:
C++标准模板库
它里面已经准备好了很多非常好用的:
容器
算法
迭代器
所以我们不用每次都自己造轮子。
例如过去我们学数组:
int a[100];
如果想要一个可以自动变长的数组,现在就可以使用:
vector<int> a;
四、先建立一张"STL地图"
STL容器
│
┌───────────┼────────────┐
│ │ │
↓ ↓ ↓
vector stack queue
动态数组 栈 队列
│ │
│ ↓
│ 先进先出
↓
priority_queue
优先队列
┌───────────────┐
↓ ↓
set map
集合 映射
接下来逐个认识。
五、第一位成员:vector
------会自动长大的数组
以前:
int a[100];
最大只能放100个整数。
如果我们不知道到底需要多少个呢?
这时候:
vector<int> a;
就非常方便。
可以把它理解成:
一个会自动扩容的数组。
六、vector最简单的使用
#include <bits/stdc++.h>
using namespace std;
int main()
{
vector<int> a;
a.push_back(10);
a.push_back(20);
a.push_back(30);
cout << a[0] << endl;
cout << a[1] << endl;
cout << a[2] << endl;
}
最后:
10
20
30
七、push_back()是什么意思?
名字其实很好理解:
push
+
back
就是:
从后面放进去。
例如:
a.push_back(10);
变成:
10
再:
a.push_back(20);
变成:
10 20
再:
a.push_back(30);
变成:
10 20 30
八、vector和数组有什么相同?
vector<int> a;
和:
int a[100];
都可以使用:
a[i]
例如:
a[0]
a[1]
a[2]
我们可以先把:
vector理解成"升级版数组"。
九、vector有什么不同?
普通数组:
int a[100];
空间一开始就确定了。
而:
vector<int> a;
可以随着:
push_back()
不断增加元素。
例如:
开始:
[]
push 10:
[10]
push 20:
[10,20]
push 30:
[10,20,30]
所以:
vector = 可以自动变长的数组
十、size()
想知道vector里有多少个元素:
a.size()
例如:
vector<int> a;
a.push_back(10);
a.push_back(20);
a.push_back(30);
cout << a.size();
结果:
3
十一、vector遍历
最简单:
for(int i=0;i<a.size();i++)
{
cout << a[i] << " ";
}
这和数组的遍历:
for(int i=0;i<n;i++)
{
cout << a[i];
}
几乎一模一样。
所以:
学会数组以后,vector非常容易。
十二、vector还能和sort一起使用
这是CSP-J特别重要的组合。
vector<int> a;
a.push_back(5);
a.push_back(2);
a.push_back(8);
a.push_back(1);
sort(a.begin(),a.end());
结果:
1 2 5 8
所以:
vector
+
sort
是非常常见的组合。
十三、这里第一次出现:begin()和end()
sort(a.begin(), a.end());
我们不用把它想得太复杂。
可以理解成:
a.begin()
↓
第一个元素的位置
a.end()
↓
最后一个元素"后面"的位置
所以:
sort(a.begin(),a.end());
意思就是:
把整个vector排序。
这一点与:
sort(a, a+n);
非常类似。
十四、vector的几个常见操作
首先就是:
a.push_back(x);
后面加入 x
a.pop_back();
删除最后一个
a.size();
元素个数
a.empty();
判断是否为空
a[i]
访问第 i 个元素
十五、第二位成员:stack
------一摞盘子
栈的定义非常明确:
栈是后进先出 LIFO(Last In First Out)的结构。
这是什么意思?
想象桌子上有一摞盘子:
┌─────┐
│ 3 │ ← 最后放
├─────┤
│ 2 │
├─────┤
│ 1 │ ← 最先放
└─────┘
你不能从中间拿。
只能:
从最上面拿。
所以:
后进先出
十六、C++中的stack
stack<int> s;
放入:
s.push(10);
s.push(20);
s.push(30);
现在:
栈顶
↓
30
20
10
十七、top()
想看看最上面的元素:
s.top()
结果:
30
注意:
s.top();
只是:
看看栈顶。
不会删除。
十八、pop()
s.pop();
表示:
删除栈顶。
所以:
s.push(10);
s.push(20);
s.push(30);
s.pop();
变成:
20
10
十九、stack最重要的三个操作
push
↓
入栈
top
↓
看栈顶
pop
↓
出栈
记忆口诀:
push放进去,top看最上,pop拿出来。
二十、注意一个非常容易错的地方
很多初学者以为:
s.pop()
会返回被删除的数字。
实际上:
s.pop();
不返回元素。
正确做法:
int x = s.top();
s.pop();
先看:
x = 栈顶
再删除:
pop
二十一、如何判断stack是否为空?
s.empty()
如果:
true
说明:
空。
如果:
false
说明:
不空。
因此:
while(!s.empty())
{
cout << s.top() << " ";
s.pop();
}
就是:
不断把栈顶元素拿出来,直到栈为空。
二十二、这和CSP-J初赛题直接联系起来
考试中有这样的题:
元素依次入栈,判断哪些出栈序列不可能出现。
以及:
给定一串元素依次进入栈,问栈容量至少是多少。
这些题本质上都只考一个核心:
栈顶永远是最后进入、还没有出去的元素。
二十三、第三位成员:queue
------排队买奶茶
假设:
小明
小红
小刚
依次来到奶茶店:
小明 → 小红 → 小刚
谁先买?
当然:
小明。
这就是:
先进先出 FIFO
讲义也明确指出,队列是先进先出的线性表。
二十四、C++中的queue
queue<int> q;
加入:
q.push(10);
q.push(20);
q.push(30);
现在:
队头 队尾
↓ ↓
10 → 20 → 30
二十五、front()
看最前面的元素:
q.front()
结果:
10
二十六、back()
看最后面的元素:
q.back()
结果:
30
二十七、pop()
q.pop();
删除队头:
10
被删除。
剩下:
20 → 30
二十八、queue的核心操作
push
↓
从队尾加入
front
↓
看队头
back
↓
看队尾
pop
↓
删除队头
口诀:
队尾进,队头出。
二十九、stack和queue比较
| 数据结构 | 特点 | 进 | 出 |
|---|---|---|---|
stack |
后进先出 | 一端 | 同一端 |
queue |
先进先出 | 队尾 | 队头 |
可以用两个生活例子记忆:
stack
↓
一摞盘子
queue
↓
排队买票
三十、考试中的一个重要考点
考题中经常考到:
广度优先搜索(BFS)需要使用队列。
为什么?
因为BFS的思想是:
先发现的人
↓
先处理
这正好是:
先进先出
所以:
BFS → queue
以后看到:
广度优先搜索
层序遍历
一层一层处理
要想到:
队列。
三十一、第四位成员:priority_queue
------VIP排行榜
现在问题来了。
普通队列:
A → B → C → D
谁先处理?
A。
但是医院急诊室不是这样。
可能:
普通病人
普通病人
紧急病人
普通病人
谁应该先处理?
紧急病人。
所以:
我们需要优先队列。
三十二、priority_queue是什么?
priority_queue<int> q;
可以理解成:
一个永远把"最大值"放在最前面的队列。
例如:
q.push(3);
q.push(10);
q.push(5);
q.push(7);
里面有:
3 10 5 7
但:
q.top()
得到:
10
三十三、priority_queue最重要的三个操作
q.push(x);
加入元素。
q.top();
查看最高优先级元素。
q.pop();
删除最高优先级元素。
三十四、默认priority_queue是谁优先?
默认:
最大值优先。
例如:
priority_queue<int> q;
q.push(3);
q.push(8);
q.push(5);
q.push(1);
不断:
cout << q.top() << " ";
q.pop();
得到:
8 5 3 1
所以:
priority_queue<int>
=
大根堆
=
最大值优先
三十五、如果想让最小值优先呢?
可以写:
priority_queue<int, vector<int>, greater<int>> q;
这句话第一次看到可能非常吓人。
其实先记:
默认
↓
最大值优先
greater<int>
↓
最小值优先
例如:
priority_queue<int, vector<int>, greater<int>> q;
q.push(3);
q.push(8);
q.push(5);
q.push(1);
不断取:
1 3 5 8
三十六、为什么叫"优先队列"?
因为普通queue:
谁先来谁先走
而priority_queue:
谁优先级最高谁先走
所以:
queue
↓
时间顺序
priority_queue
↓
优先级顺序
这个区别必须掌握。
三十七、priority_queue和sort有什么区别?
例如:
3 8 5 1
sort:
把所有元素一次性排好。
priority_queue:
不断加入元素,然后每次快速找到当前最优的那个。
所以可以理解成:
sort
↓
先全部收集
↓
一次排序
priority_queue
↓
不断加入
↓
不断取当前最优
这在算法题中非常重要。
三十八、第五位成员:set
------一个不允许重复的集合
假设:
1 2 3 3 3 4 5 5
如果老师说:
我只想知道有哪些不同的数字。
答案:
1 2 3 4 5
这就是集合。
C++:
set<int> s;
三十九、set最大的特点
自动去重
例如:
set<int> s;
s.insert(3);
s.insert(5);
s.insert(3);
s.insert(8);
s.insert(5);
最终集合中:
3 5 8
重复的:
3
5
不会出现第二次。
四十、set还有一个重要特点:自动有序
普通:
vector<int>
加入:
5 2 8 3
可能就是:
5 2 8 3
而:
set<int>
插入:
5
2
8
3
遍历时会按照顺序:
2 3 5 8
所以:
set = 自动去重 + 自动排序
这是非常重要的认识。
四十一、set的常用操作
加入:
s.insert(x);
删除:
s.erase(x);
判断是否为空:
s.empty();
元素个数:
s.size();
查找:
s.find(x);
四十二、set和vector怎么选?
例如我要保存:
3 3 2 5 2 8
如果使用:
vector<int>
得到:
3 3 2 5 2 8
如果使用:
set<int>
得到:
2 3 5 8
所以:
vector
↓
允许重复
↓
保留插入顺序
set
↓
不允许重复
↓
自动有序
四十三、第六位成员:map
------超级电话簿
这是经常用到的一个数据结构。
就像电话簿:
小明 → 138xxxx
小红 → 139xxxx
小刚 → 137xxxx
这里:
姓名
↓
电话号码
就是:
key → value
C++:
map<string,string> phone;
四十四、map最核心的思想
例如:
map<string,int> score;
我们可以写:
score["Tom"] = 95;
score["Jack"] = 88;
score["Lucy"] = 100;
于是:
Tom → 95
Jack → 88
Lucy → 100
这就是:
用一个"名字"找到一个"值"。
四十五、map的两个部分
每一个元素都可以看成:
key → value
例如:
apple → 5
banana → 8
orange → 3
这里:
apple
banana
orange
是:
key
而:
5
8
3
是:
value
四十六、map最常见的用途:统计次数
这是CSP-J特别重要的用途。
例如:
1 2 1 3 2 1
统计每个数字出现次数。
map<int,int> cnt;
cnt[1]++;
cnt[2]++;
cnt[1]++;
cnt[3]++;
cnt[2]++;
cnt[1]++;
最后:
1 → 3
2 → 2
3 → 1
所以:
map是非常好用的"计数器"。
四十七、为什么cnt[x]++这么神奇?
例如第一次:
cnt[5]++;
如果5还不存在:
cnt[5]
会得到默认的0。
然后:
0 + 1 = 1
第二次:
cnt[5]++;
变成:
2
第三次:
3
所以:
cnt[x]++;
就是:
把x出现次数加1。
四十八、map和数组统计有什么关系?
以前统计:
1~100
可以:
int cnt[101] = {};
然后:
cnt[x]++;
但是如果数字可能是:
1000000000
或者:
-99999999
用一个巨大数组就不方便。
这时候:
map<int,int> cnt;
就非常方便。
所以:
数组计数
↓
数据范围小
map计数
↓
数据范围可以很大
四十九、unordered_map是什么?
我们不需要深入实现哈希表。
但程序阅读中会看到:
unordered_map<int,int> cnt;
可以先理解:
它也是"key → value"的电话簿。
区别简单记:
map
↓
有序
unordered_map
↓
通常不保证有序
所以:
map
=
有序电话簿
unordered_map
=
不按顺序排列的快速电话簿
五十、建立一张STL容器对比表
| STL容器 | 生活中的东西 | 核心特点 |
|---|---|---|
vector |
自动变长的盒子 | 动态数组 |
stack |
一摞盘子 | 后进先出 |
queue |
排队 | 先进先出 |
priority_queue |
VIP排行榜 | 优先级最高先出 |
set |
不重复的集合 | 去重、有序 |
map |
电话簿 | key → value |
五十一、STL操作口诀
vector
push_back
pop_back
size
empty
stack
push
top
pop
empty
queue
push
front
back
pop
empty
priority_queue
push
top
pop
empty
set
insert
erase
find
size
empty
map
[key]
insert
erase
find
size
empty
五十二、考试中重要的比较:stack、queue、priority_queue
这三个特别容易混淆。
stack
1进
2进
3进
3出
2出
1出
即:
后进先出。
queue
1进
2进
3进
1出
2出
3出
即:
先进先出。
priority_queue
3进
10进
5进
7进
先出:
10
然后:
7
然后:
5
最后:
3
即:
谁优先级高谁先出。
五十三、程序阅读题
vector<int> a;
a.push_back(5);
a.push_back(2);
a.push_back(8);
a.push_back(2);
sort(a.begin(),a.end());
cout << a[1];
先:
5 2 8 2
排序:
2 2 5 8
所以:
a[1] = 2
答案:
2
五十四、再看stack
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
cout << s.top() << " ";
s.pop();
cout << s.top();
过程:
push 1
→ 1
push 2
→ 1 2
push 3
→ 1 2 3
第一次:
s.top()
得到:
3
然后:
s.pop()
删除3。
现在:
1 2
再次:
s.top()
得到:
2
所以:
3 2
五十五、再看queue
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
cout << q.front() << " ";
q.pop();
cout << q.front();
过程:
1 → 2 → 3
第一次:
front = 1
然后:
pop
删除1。
剩下:
2 → 3
所以第二次:
front = 2
答案:
1 2
五十六、再看priority_queue
priority_queue<int> q;
q.push(3);
q.push(8);
q.push(5);
cout << q.top() << " ";
q.pop();
cout << q.top();
最大值优先:
8
删除8。
剩下:
5
3
所以:
5
答案:
8 5
五十七、再看set
set<int> s;
s.insert(5);
s.insert(2);
s.insert(5);
s.insert(3);
for(auto x:s)
cout << x << " ";
插入:
5
2
5
3
重复的5只保留一次。
最终:
2 3 5
所以输出:
2 3 5
五十八、再看map
map<string,int> m;
m["Tom"] = 90;
m["Jack"] = 80;
m["Tom"]++;
cout << m["Tom"];
第一次:
Tom → 90
然后:
m["Tom"]++;
变成:
Tom → 91
所以输出:
91
五十九、STL程序阅读"四步法"
以后我们看到一个STL容器,不要慌。
第一步:
认容器
看到:
vector
stack
queue
priority_queue
set
map
马上知道它是什么。
第二步:
认规则
例如:
stack
→ 后进先出
queue
→ 先进先出
priority_queue
→ 优先级最高先出
set
→ 去重+有序
map
→ key找value
第三步:
画状态
例如:
q.push(3);
q.push(5);
q.pop();
不要只在脑子里想。
画:
3
↓
3 5
↓
5
第四步:
最后看输出
不要一看到:
cout
就急着算。
先把容器状态模拟完。
六十、CSP-J容易考的对应关系
这部分非常重要。
看到:
"后进先出"
想到:
stack
看到:
"先进先出"
想到:
queue
看到:
"广度优先搜索"
想到:
queue
看到:
"每次取最大"
想到:
priority_queue<int>
看到:
"每次取最小"
想到:
小根堆 / 最小优先的
priority_queue
看到:
"去掉重复元素"
想到:
set
看到:
"某个东西对应一个值"
想到:
map
看到:
"动态数组"
想到:
vector
六十一、一个重要的"选择数据结构"训练
给同学们几个场景,来看适合哪种数据结构:
场景1
班级里有很多数字,需要动态增加。
答案:
vector
场景2
浏览器的后退操作,最近访问的页面最先回来。
答案:
stack
场景3
食堂排队,先来的人先打饭。
答案:
queue
场景4
每次从一堆任务中选择优先级最高的任务。
答案:
priority_queue
场景5
给一堆数字去重。
答案:
set
场景6
统计每个单词出现了多少次。
答案:
map
六十二、把本课和前面学过的知识串起来
本课不是一个孤立的新章节。
前面我们已经学了:
数组
↓
结构体
↓
排序
现在可以:
vector
↓
可以装结构体
sort
↓
可以给vector排序
priority_queue
↓
可以装结构体
set
↓
可以保存结构体
map
↓
可以统计数据
例如:
vector<Student> a;
就是:
动态的结构体数组。
再:
sort(a.begin(),a.end(),cmp);
就是:
给动态结构体数组排序。
这与前面的课连接起来了。
六十三、vector里也可以放结构体
例如:
struct Student
{
string name;
int score;
};
vector<Student> a;
然后:
a.push_back({"Tom",95});
a.push_back({"Jack",90});
这时候:
a
↓
┌───────────────┐
│ Tom 95 │
├───────────────┤
│ Jack 90 │
└───────────────┘
再:
sort(a.begin(),a.end(),cmp);
这就是:
结构体动态数组的排序
六十四、本课还有一个重要的思想
同学们以前可能认为:
数据结构就是"数组、链表、栈、队列"。
现在要慢慢建立一个更深刻的认识:
数据结构不是为了"记名字",而是为了"选择合适的操作方式"。
例如:
我需要随机访问
↓
vector
我只需要最后进来的先出来
↓
stack
我需要先进先出
↓
queue
我需要每次拿最大/最小
↓
priority_queue
我需要去重
↓
set
我需要 key → value
↓
map
这才是学习数据结构真正的目的。
六十五、本课知识地图
STL
│
┌────────────────┼────────────────┐
│ │ │
↓ ↓ ↓
vector stack queue
动态数组 栈 队列
│ │ │
↓ ↓ ↓
随机访问 后进先出 先进先出
│
↓
priority_queue
优先队列
│
↓
最大/最小优先
┌────────────────────────┐
│ │
↓ ↓
set map
集合 映射
│ │
↓ ↓
去重+有序 key → value
│
↓
计数
六十六、本课CSP-J必背清单
1、vector
vector<int> a;
a.push_back(x);
a.pop_back();
a.size();
a[i];
动态数组。
2、stack
stack<int> s;
s.push(x);
s.top();
s.pop();
后进先出。
3、queue
queue<int> q;
q.push(x);
q.front();
q.back();
q.pop();
先进先出。
4、priority_queue
priority_queue<int> q;
q.push(x);
q.top();
q.pop();
默认最大值优先。
5、set
set<int> s;
s.insert(x);
s.erase(x);
s.find(x);
自动去重、自动有序。
6、map
map<int,int> mp;
mp[x]++;
key → value,非常适合计数。
六十七、一句话口诀
vector是动态数组,数据增加不用愁。
stack像一摞盘子,后进先出记心头。
queue就是排队,先进先出往前走。
priority_queue看优先级,默认最大值先出头。
set自动去重还排序。
map像一本电话簿,key找到value。
看到BFS想队列,看到去重想集合。
看到统计想map,看到最大最小想优先队列。
六十八、课堂训练
第1部分:vector + stack
vector
↓
动态数组
↓
push_back
↓
遍历
↓
sort
stack
↓
LIFO
↓
push/top/pop
↓
栈模拟题
重点:
数组升级 + 栈模拟
第2部分:queue + priority_queue
queue
↓
FIFO
↓
front/back/pop
↓
BFS为什么需要队列
priority_queue
↓
优先级
↓
大根堆
↓
小根堆
重点:
看到"先进先出"马上想到queue;看到"每次取最大/最小"马上想到priority_queue。
第3部分:set + map + 综合阅读
set
↓
去重
↓
有序
map
↓
key/value
↓
计数
最后综合:
vector + sort
map + 统计
set + 去重
priority_queue + 取最值