Chapter 2 数据结构
1:链表与邻接表
单链表:用于构造邻接表,存储图和树
双链表:用于优化某些问题
数组模拟链表
e[N]
表示当前位置的值
ne[N]
表示指向的下一个位置的编号
head -> value0(0) -> value1(1) -> end(-1)
题目:单链表
# include <iostream>
using namespace std;
const int N = 1e6+10;
int head, e[N], ne[N], idx;
// head头指针
// e数组 数值
// ne数组 下一个指针编号
// idx 当前已经用到哪个点
// 初始化
void init(){
head = -1;
idx = 0;
}
// head插入
void add_to_head(int x){
ne[idx] = head; // 当前点的next,更新为head指向的起点
head = idx; // head指向当前点
e[idx] = x; // 存储value
idx++;
}
// 将x插入下标是k的点后面
void add(int k, int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}
// 将下标是k的点后面的点删除
void remove(int k){
ne[k] = ne[ne[k]];
}
int main(){
int m;
cin>>m;
init();
while(m--){
int k,x;
char op;
cin>>op;
if(op=='H'){
cin>>x;
add_to_head(x);
}
else if(op=='D'){
cin>>k;
if(!k) head = ne[head]; // delete head
remove(k-1); // 第k个插入的点,其下标是k-1
}
else{
cin>>k>>x;
add(k-1,x);
}
}
for(int i=head;i!=-1;i=ne[i]){
cout<<e[i]<<" ";
}
cout<<endl;
return 0;
}
/*
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
6 4 6 5
*/
题目:双链表
# include <iostream>
using namespace std;
const int N = 1e6+10;
int m;
int e[N], l[N], r[N], idx;
void init(){
// 0是左端点,1是右端点
// 一开始的状态是 point0 <=> point1
r[0] = 1; // point0右边是point1
l[1] = 0; // point1左边是point0
idx = 2;
}
// 第k个点右侧插入
void add(int k, int x){
e[idx] = x;
// define x pointer
r[idx] = r[k]; //idx的right,为k点右侧指向
l[idx] = k; //idx的left,为k本身
// update previous pointer
l[r[k]] = idx; //k原来的右侧指向,其left更改为idx
r[k] = idx;
idx ++;
// 左侧插入,在k-1点右侧插入
// add(l[k], x)
}
// 删除第k个点
void remove(int k){
int pre = l[k];
int next = r[k];
r[pre] = next;
l[next] = pre;
}
int main(){
// 输入和输出操作
init();
int m, k, x;
cin >> m;
while (m--) {
string op;
cin >> op;
if (op == "L") {
cin >> x;
// 最左端插入数x
// 最左端就是表示在0的右侧插入一个数据
add(0, x);
} else if (op == "R") {
cin >> x;
// 最右端插入数x
add(l[1], x); // 1号端点的左侧,就是最右端
} else if (op == "D") {
cin >> k;
remove(k + 1); // idx从2开始,所以下标需要k+1传入才对
} else if (op == "IL") {
cin >> k >> x;
add(l[k + 1], x); // 在这个k的左侧那个元素的右侧插入数据x
} else {
cin >> k >> x;
add(k + 1, x); // 正常调用
}
}
// 注意双链表的输出方法,0和1是一头一尾,idx从2开始,换言之,
// 从r[0]就是开始进行遍历,出现1就结尾了
for (int i = r[0]; r[i]!=NULL ; i = r[i]) printf("%d ", e[i]);
return 0;
}
/*
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
8 7 7 3 2 9
*/
邻接表:多个单链表组成
2:栈与队列
栈:先进后出
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[ ++ tt] = x;
// 从栈顶弹出一个数
tt -- ;
// 栈顶的值
stk[tt];
// 判断栈是否为空,如果 tt > 0,则表示不为空
if (tt > 0)
{
}
队列:先进先出
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
// 向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++ ;
// 队头的值
q[hh];
// 判断队列是否为空,如果 hh <= tt,则表示不为空
if (hh <= tt)
{
}
单调栈:
作用:求数组中距离当前数最近的符合某个特征的数值
题目:输出每个数左边第一个比它小的数
# include <iostream>
using namespace std;
const int N = 1e6+10;
int stk[N], tt;
int n;
int main(){
cin>>n;
for(int i=0;i<n;i++){
int x;
cin>>x;
// tt: stack is not empty
// stk[tt]>=x: stack top >= current
// 需要改变的是stk[tt] >= x,即check()函数
while(tt && stk[tt] >= x) tt--;
// i左边离i最近且比i小的数
if(tt) cout<<stk[tt]<<" ";
else cout<<-1<<" ";
stk[++tt] = x;
}
return 0;
}
/*
5
3 4 2 7 5
-1 3 -1 2 2
*/
cin.tie(0)
可以加快cin的读入速度
单调队列:
作用:求滑动窗口的最大值和最小值
题目:滑动窗口
# include <iostream>
using namespace std;
const int N = 1e6+10;
int a[N], q[N]; // q是单调队列
int n, k;
// k是窗口大小
// n是数组长度
int main(){
cin>>n>>k;
for(int i=0;i<n;i++){
cin>>a[i];
}
int hh = 0, tt = -1;
// hh头, tt尾
for(int i=0;i<n;i++){
// 判断队头是否离开滑动窗口,不确定移动多少次时使用while,这里的if只有一次判断
if(hh <= tt && i-k+1 > q[hh]){
hh++; // 离开
}
//q[tt]是窗口末尾数,如果a[末尾]超过了当前a[i],则说明队列q不是单调递增了,应该剔除a[末尾]
while(hh <= tt && a[q[tt]]>=a[i]){
tt--;
}
q[++tt] = i; //将当前位置i插入队尾
if(i>=k-1){
// i走到第一次窗口的最右侧时,才开始输出
cout<<a[q[hh]]<<" ";
}
}
cout<<endl;
hh = 0, tt = -1;
// hh头, tt尾
for(int i=0;i<n;i++){
// 判断队头是否离开滑动窗口
if(hh <= tt && i-k+1 > q[hh]){
hh++;
}
while(hh <= tt && a[q[tt]]<=a[i]){
tt--;
}
q[++tt] = i;
if(i>=k-1){
cout<<a[q[hh]]<<" ";
}
}
cout<<endl;
return 0;
}
/*
8 3
1 3 -1 -3 5 3 6 7
-1 -3 -3 -3 3 3
3 3 5 5 6 7
*/
3:kmp
核心:前缀和后缀相同的最长字符串
next[]的含义:当子串j+1
点和父串i
点不相同时,j
点应该更新的下一个点
# include <iostream>
using namespace std;
const int N = 1e6+10, M = 1e6+10;
int ne[N];
char p[N], s[M]; // 模式串和主串
int n, m;
int main(){
// 主串是1开始编号,模式串是0开始编号
cin>>n>>p+1>>m>>s+1;
// 求next,则是模式串和模式串本身求kmp
//i从2开始,因为ne[1]=0
for(int i=2,j=0; i<=n; i++){
while(j && p[i]!=p[j+1]){
j = ne[j]; // 退回
}
if(p[i] == p[j+1]){
j++; // 匹配成功
}
ne[i] = j; // 更新
}
// kmp 匹配过程
for(int i=1,j=0; i<=m; i++){
// 没退回到模式串的起点 且 下一个字符总是不匹配
while(j && s[i]!=p[j+1]){
j = ne[j]; // 退回
}
// 下一个字符,退回到,匹配成功了
if(s[i] == p[j+1]){
j++; // 继续匹配下下一个字符
}
// 整个模式串匹配成功
if(j == n){
cout<<i-n<<" ";
j = ne[j];
}
}
return 0;
}
/*
3
aba
5
ababa
0 2
*/
4:Trie树
作用:快速存储和查找字符串集合的数据结构
字典树
题目:字符串统计
l x是插入一个字符串x
Q x是询问字符串x出现了多少次
# include <iostream>
using namespace std;
const int N = 1e6+10;
int son[N][26], cnt[N], idx;
// son数组是每个idx的分叉,小写字母最多26个分叉
// cnt数组记录每个idx的结尾信号数
// 下标是0的点,是根节点或空节点
char str[N];
void insert(char str[]){
int p=0; // 从根节点开始
for(int i=0;str[i];i++){
int u = str[i]-'a';
// 如果不存在儿子,则创建,且儿子里存放的是唯一idx序号
if (!son[p][u]) son[p][u]=++idx;
p = son[p][u]; // 更新根节点为儿子
}
cnt[p]++; // 存好了,给最后的儿子一个结尾信号
}
int query(char str[]){
int p=0;
for(int i=0;str[i];i++){
int u = str[i]-'a';
// 如果不存在儿子,则不存在单词
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
int main(){
int n;
cin>>n;
while(n--){
char op[2];
cin>>op>>str;
if(op[0]=='I') insert(str);
else cout<<query(str)<<endl;
}
return 0;
}
/*
5
I abc
Q abc
Q ab
I ab
Q ab
1
0
1
*/
5:并查集
作用:
【1】将两个集合合并
【2】询问两个元素是否在一个集合当中
基本原理:
以树的形式维护每一个集合,根节点的编号是当前集合的编号,每个节点存储其父节点,p[x]
表示x的父节点。
【1】判断树根:if(p[x] == x)
【2】求x的集合编号:while(p[x] != x) x = p[x]
,从当前节点x向上递归,寻找根节点,返回这个集合的编号。
【3】合并两个集合:px
是x的编号,py
是y的集合编号,则p[x]=y
使得集合x的父节点变成集合y的根节点
优化:路径压缩——向上搜索过的结点,全部连接到根节点,不再经过中间结点
其他优化:按秩合并——把高度较小的树的根节点,接到高度较大的树的根节点上(几乎不用)
题目:合并集合
# include <iostream>
using namespace std;
const int N = 1e6+10;
int p[N];
// 返回x的祖宗结点 + 路径压缩
int find(int x){
if(p[x] != x){
p[x] = find(p[x]);
}
return p[x];
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
p[i]=i;
}
while(m--){
char op[2];
int a,b;
cin>>op>>a>>b;
if(op[0] == 'M'){
p[find(a)] = find(b);
// a的祖宗结点的父亲,等于b
// 实质上是让a集合的根节点的父亲,变成,b集合的根节点
}
else{
if(find(a) == find(b)){
puts("Y");
}
else{
puts("N");
}
}
}
return 0;
}
/*
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
Y
N
Y
*/
题目:连通块中的数量
# include <iostream>
using namespace std;
const int N = 1e6+10;
int p[N], size[N];
//size是每一个集合中点的数量
//只用保证根节点的size是有意义的
// 返回x的祖宗结点 + 路径压缩
int find(int x){
if(p[x] != x){
p[x] = find(p[x]);
}
return p[x];
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
p[i]=i;
size[i]=1; //刚开始只有一个点
}
while(m--){
char op[5];
int a,b;
cin>>op;
if(op[0] == 'C'){
cin>>a>>b;
if(find(a) == find(b)){
continue;
//如果a和b在同一个结合,不需要更新根节点的size
//需要特判!
}
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
// a的祖宗结点的父亲,等于b
// 实质上是让a集合的根节点的父亲为b集合的根节点
}
else if (op[1] == '1'){
cin>>a>>b;
if(find(a) == find(b)){
puts("Y");
}
else{
puts("N");
}
}
else{
cin>>a;
cout<<size[find(a)]<<endl;
}
}
return 0;
}
/*
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
Yes
2
3
*/
并查集的变形:维护一些额外信息
例如:上题是集合中的个数;集合中到根节点的距离
6:堆
用于Dijkstra,维护一个数据集合
如何手写一个堆?
【1】插入一个数
【2】求集合当中的最小值
【3】删除最小值
【4】删除任意一个元素
【5】修改任意一个元素
STL中可以直接实现【1】到【3】。
无法直接实现【4】【5】
堆是一个完全二叉树
小根堆的性质,每个点都小于等于左右两个儿子,根节点为最小值
按一维数组存储点,x的左儿子是2x
,x的右儿子是2x+1
核心操作:
down:结点往下调整(与儿子比大小,比儿子大则下沉)
up:结点往上调整(与父亲比大小,比父亲小则上升)
heap表示堆,size表示当前大小
下标从1开始!
【1】
heap[++size] = x;
up(size);
【2】
heap[1];
【3】
heap[1] = heap[size]; //最后一个点直接覆盖第一个点
size--; //修改大小(直接删除最小值了相当于)
down(1); //将第一个点下沉(重新维护堆)
【4】
heap[k] = heap[size];
size--;
down(k);
up(k);
// 只执行down或up
【5】
heap[k] = x;
down(k);
up(k);
题目:堆排序
输入一个长度为n的整数数列,升序输出前m个数
# include <iostream>
using namespace std;
const int N = 1e6+10;
int n, m;
int h[N], size;
void down(int u){
int min = u; // 假设父亲最小
if(u*2<=size && h[u*2]<h[min]){
//如果左儿子存在 且 左儿子比父亲小
min = u*2;
}
if(u*2+1<=size && h[u*2+1]<h[min]){
//如果右儿子存在 且 右儿子比父亲小
min = u*2+1;
}
if(u != min){
//如果有儿子比父亲小
swap(h[u],h[min]); // 交换儿子和父亲
down(min); // 让新儿子继续下沉
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>h[i];
}
size=n;
// 整理每个结点,使得小根堆成立
for(int i=n/2;i;i--){
down(i);
}
while(m--){
cout<<h[1]<<" ";
h[1] = h[size];
size--;
down(1);
}
return 0;
}
/*
5 3
4 5 1 3 2
1 2 3
*/
题目:模拟堆
实现以下操作:
1:插入一个数
2:输出最小值
3:删除最小值(不唯一时,删除最早插入的)
4:删除第k个插入的
5:修改第k个插入的
# include <iostream>
# include <string.h>
# include <algorithm>
using namespace std;
const int N = 1e6+10;
int h[N], ph[N], hp[N], size;
//ph[k]存第k个插入点,在堆中的下标
//hp[k]存堆中k下标的点,是第几个插入点
//hp和ph是互为反函数
void heap_swap(int a, int b){
swap(h[a],h[b]);
//交换2个值
swap(hp[a],hp[b]);
//交换hp指针,即更新从点获取的插入编号
swap(ph[hp[a]],ph[hp[b]]);
//交换ph指针,hp[i]是i点对应的插入编号,即交换插入编号
}
void down(int u){
int min = u; // 假设父亲最小
if(u*2<=size && h[u*2]<h[min]){
//如果左儿子存在 且 左儿子比父亲小
min = u*2;
}
if(u*2+1<=size && h[u*2+1]<h[min]){
//如果右儿子存在 且 右儿子比父亲小
min = u*2+1;
}
if(u != min){
//如果有儿子比父亲小
heap_swap(u,min); // 交换儿子和父亲
down(min); // 让新儿子继续下沉
}
}
void up(int u){
// 当父亲存在 且 父亲比儿子大
while(u/2 && h[u/2]>h[u]){
heap_swap(u/2,u); // 交换父亲和儿子
u /= 2; // 让新父亲继续上升
}
}
int main(){
int n, idx = 0;
cin>>n;
while(n--){
char op[2];
int k,x;
cin>>op;
if(!strcmp(op,"I")){
cin>>x;
size++;
idx++;
ph[idx] = size;
hp[size] = idx;
h[size] = x;
up(size);
}
else if(!strcmp(op,"PM")){
cout<<h[1]<<endl;
}
else if(!strcmp(op,"DM")){
heap_swap(1,size);
size--;
down(1);
}
else if(!strcmp(op,"D")){
cin>>k;
k = ph[k];
heap_swap(k,size);
size--;
down(k);
up(k);
}
else{
cin>>k>>x;
k = ph[k];
h[k] = x;
down(k);
up(k);
}
cout<<endl;
for(int k=1;k<=size;k++){
cout<<" "<<h[k];
}
cout<<endl;
}
return 0;
}
/*
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
-10
6
*/
7:Hash表
存储结构:
【1】开放寻址法
【2】拉链法(单数组模拟映射块+数组模拟链表)
取模的数为范围内最大的素数
题目:拉链法模拟hash
每个取模的位置,连接一个链表,放置当前数字x进入链表
—1—2—3—4—5—6—7—
—[]—[]—[]—x1—[]—[]—[]—
—[]—[]—[]—x2—[]—[]—[]—
# include <iostream>
# include <string.h>
# include <algorithm>
using namespace std;
const int N = 1e5+3;
int h[N], e[N], ne[N], idx;
void insert(int x){
int k = ((x%N) + N) % N; // 让复数取模是正数
// 把x插入到h[k]上
e[idx] = x; // 数值x存入
ne[idx] = h[k]; // 新的点的next指针为 h[k]
h[k] = idx; // h[k]指向新的点
idx++;
}
bool find(int x){
int k = ((x%N) + N) % N;
for(int i=h[k]; i!=-1; i=ne[i]){
if(e[i] == x){
return 1;
}
}
return 0;
}
int main(){
int n;
cin>>n;
memset(h, -1, sizeof h);
while(n--){
char op[2];
int x;
cin>>op>>x;
if(*op=='I'){
insert(x);
}
else{
if(find(x)){
puts("Yes");
}
else{
puts("No");
}
}
}
return 0;
}
/*
5
I 1
I 2
I 4
Q 2
Q 5
Yes
No
*/
题目:开放寻址法模拟hash
开一个目标长度2~3倍是数组,按照上厕所找坑位的方法遍历,找到坑位x就插入
# include <iostream>
# include <string.h>
# include <algorithm>
using namespace std;
const int N = 2e5+3, null = 0x3f3f3f3f;
int h[N];
int find(int x){
int k = (x%N+N) % N;
// 当k位置非空,且,k位置的数不是x
while(h[k] != null && h[k] != x){
k++;
// 查找下一个位置
if(k == N){
k = 0;
//走到最后一个位置了,应该从头开始遍历
}
}
return k;
}
int main(){
int n;
cin>>n;
memset(h, 0x3f, sizeof h);
while(n--){
char op[2];
int x;
cin>>op>>x;
int k = find(x);
if(*op=='I'){
h[k] = x;
}
else{
if(h[k] != null){
puts("Yes");
}
else{
puts("No");
}
}
}
return 0;
}
/*
5
I 1
I 2
I 4
Q 2
Q 5
Yes
No
*/
字符串hash方式
作用:快速判断两个字符串是否相等,从 O(n) 到 O(1)
字符串前缀哈希法(从1开始映射,且假定不存在冲突)
【1】把字符串看成一个p进制的数
【2】将p进制的数转换成10进制的数
【3】将10进制的数对小整数Q取模
P和Q的经验值(这样的话不会出现冲突)
P = 131 或 13331
Q = 2^64
# include <iostream>
# include <string.h>
# include <algorithm>
using namespace std;
// 0839
typedef unsigned long long ULL;
const int N =1e5+10, P = 131;
int n, m;
char str[N]; // 输入的字符串
ULL h[N], p[N];
// h数组是 某个前缀的 哈希值
// p数组是 进制移位的乘数
// 比较两个字符串是否相等
ULL get(int l, int r){
return h[r] - h[l-1] * p[r-l+1];
// 将 h[l-1] 左移,补全位数,然后作差
}
int main(){
cin>>n>>m>>str+1;
p[0]=1; // 初始化
for(int i=1;i<=n;i++){
p[i] = p[i-1] * P;
//基数,例如:补全n位需要乘以n个p
h[i] = h[i-1] * P + str[i];
//前缀和
}
while(m--){
int l1, r1, l2, r2;
cin>>l1>>r1>>l2>>r2;
if(get(l1,r1) == get(l2,r2)) puts("Yes");
else puts("No");
}
return 0;
}
/*
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
Yes
No
Yes
*/
8:STL
【1】vector, pair
变长数组,倍增思想
vector常用函数
//size() 返回元素个数
//empty() 返回是否为空
//clear() 清空
//front() 返回第一个数
//back() 返回最后一个数
//push_back() 末尾插入一个数
//pop_back() 末尾删除一个数
//begin() 迭代器,第一个数
//end() 最后一个数的后面一个数
测试用例
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
using namespace std;
int main(){
int n=10, value=3;
vector<int> aa(n,value); //长度为n,数值初始化为value
for(auto x:aa){
cout<<x<<" ";
}
cout<<endl;
vector<int> b[10]; //vector数组
/*-----基本函数-----*/
//size() 返回元素个数
//empty() 返回是否为孔
//clear() 清空
//front() 返回第一个数
//back() 返回最后一个数
//push_back() 末尾插入一个数
//pop_back() 末尾删除一个数
//begin() 迭代器,第一个数
//end() 最后一个数的后面一个数
/*-----遍历vector-----*/
vector<int> a;
for(int i=0;i<10;i++) a.push_back(i);
// method 1
for(int i=0;i<a.size();i++) cout<<a[i]<<" ";
cout<<endl;
// method 2
for(vector<int>::iterator i=a.begin(); i!=a.end(); i++) cout<<*i<<" ";
cout<<endl;
// method 3
for(auto x:a) cout<<x<<" ";
cout<<endl;
/*-----比较运算-----*/
// 按字典序来比较大小
vector<int> c(4,3), d(3,4);
if(c < d) puts("c<d");
else puts("c>=d");
return 0;
}
pair用法
pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
两个不同属性的变量需要存储在一起,通常用于排序
存储3个内容:pair<int, pair<int,int>> p
,以此类推
创建一个pair:p = {20, "abc"}
或p = make_pair{20, "abc"}
作用:直接实现结构体
【2】string
字符串
string常用函数
//size() 返回元素个数
//length() 同上
//empty() 返回是否为空
//substr(start,len) 返回从start开始长度为n的子字符串
//c_str() 返回字符串所在字符数组的起始地址
【3】queue, priority_queue
队列和优先队列
queue常用函数
//size() 返回元素个数
//empty() 返回是否为空
//push() 向队尾插入一个元素
//front() 返回队头元素
//back() 返回队尾元素
//pop() 弹出队头元素
没有clear函数,如果需要清空则需要q = queue<int>()
优先队列默认按大根堆实现
priority_queue常用函数
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
如果想实现小根堆,则可以使用
priority_queue<int, vector<int>, greater<int>> q;
或直接插入复数
priority_queue<int> heap
heap.push(-x)
【4】stack
栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素
【5】deque
双端队列
size()
empty()
clear()
front()/back() //返回第一个/最后一个元素
push_back()/pop_back() //队尾插入/弹出一个元素
push_front()/pop_front() //队首插入/弹出一个元素
begin()/end() //迭代器
[] //随机寻址
缺点:速度慢
【6】set, map, multiset, multimap
基于平衡二叉树(红黑树),动态维护有序序列
multi支持重复元素,没有multi就不支持
size()
empty()
clear()
begin()/end()
++, -- 返回前驱和后继,时间复杂度 O(logn)
set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn),k是x的个数
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()
【7】unordered_set, unordered_map, unordered_multiset, unordered_multimap
哈希表
内部无序
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--
【8】bitset
压位
布尔变量压缩,一个bool变量是1B,占8位,压缩之后只占1位了
bitset<10000> s;
~, &, |, ^ 取反,与,或,异或
>>, << 左移,右移
==, != 等于,不等于
[] 取出某一位
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 把所有位反转,即等价于~
flip(k) 把第k位取反