链表(用数组),单链表:存储图和树

双链表:优化某些题

做法:

  • e[],ne[]
  • -1表示结尾

==演示==:

head->0->1->2->3->-1
值: 3 5 7 9

e[0]=3 e[1]=5 e[2]=7 e[3]=9

ne[0]=1 ne[1]=2 ne[2]=3 ne[3]=-1

单链表的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
void init(){
head=-1;
idx=0;//当前可以从0号点开始分配
}
void add_to_head(int x){
e[idx]=x,ne[idx]=head,head=idx++;
}
void add(int k,int x){//将x插入第k个点后面
e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}
void remove(int k){//把第k个点后面的点删掉
ne[k]=ne[ne[k]];
}

双链表:

没有head,0号点指向第一个点,1号点指向最后一个点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void init(){
r[0]=1,l[1]=0;
idx=2;
}
void d_add(int x){
r[l[x]]=r[x];
l[r[x]]=l[x];
}
void IR_add(int k,int x){
e[idx]=x;
r[idx]=r[k];
l[idx]=k;
l[r[k]]=idx;
r[k]=idx;
idx++;
}

栈:先进后出

队列:先进先出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(op=="push"){//插入
int k;
cin>>k;
stk[++tt]=k;
}
else if(op=="pop"){//弹出
tt--; `
}
else if(op=="empty"){//判空
if(!tt) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
else{//取出栈顶
cout<<stk[tt]<<endl;
}

表达式求值

如何判断某棵子树是否被遍历完?

答:看当前运算符优先级≤上一运算符优先级,表示子树遍历完了。”(“ :无影响 “)”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<iostream>
#include<stack>
#include<string>
#include<unordered_map>
using namespace std;
const int N=100010;
stack<int> num;
stack<char> op;
unordered_map<char,int> h{{'+',1},{'-',1},{'*',2},{'/',2}};
void eval(){
int r=0;
int a=num.top();
num.pop();
int b=num.top();
num.pop();
char c=op.top();
op.pop();
if(c=='+') r=a+b;
if(c=='-') r=b-a;
if(c=='*') r=a*b;
if(c=='/') r=b/a;
num.push(r);
// cout<<num.top()<<endl;
}
int main(){
string str;
cin>>str;
for(int i=0;i<str.size();i++){
auto c=str[i];
if(isdigit(str[i])){
int x=0,j=i;
while(j<str.size()&&isdigit(str[j])){
x=x*10+str[j]-'0';
j++;
}
i=j-1;
num.push(x);
//cout<<num.top()<<endl;
}
else if(str[i]=='('){
op.push(str[i]);
}
else if(str[i]==')'){
while(op.top()!='(') eval();//eval:用末尾的运算符操作末尾的两个数
op.pop();
}
else{
while(op.size()&&h[op.top()]>=h[str[i]]){
eval();//操作栈顶元素
}
op.push(str[i]);
}
}
while(op.size()) eval();
cout<<num.top()<<endl;
return 0;
}

队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(s=="push")//插入
{
int x;
cin>>x;
p[++tt]=x;
}
else if(s=="pop")//弹出
{
hh++;
}
else if(s=="empty")//判断是否为空
{
if(hh<=tt) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
else{//取出队头元素
cout<<p[hh]<<endl;//q[tt]:队尾元素
}

单调栈

给定一个序列,求每一个数,左边最近比他小的数

3 4 2 7 5
-1 3 -1 2 2

若x<y,ax>ay,则ax不会被输出;

因此,最后输出的肯定是一个单调上升的序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
using namespace std;
const int N=100010;
int s[N],tt=-1;
int main(){
int n;
cin>>n;
while(n--){
int x;
cin>>x;
while(tt!=-1&&s[tt]>=x){
tt--;
}
if(tt!=-1) cout<<s[tt]<<" ";
else cout<<"-1"<<" ";
s[++tt]=x;
}
return 0;
}

滑动窗口

整数数组,大小为k的滑动窗口

适用于:每次滑动的过程中,输出窗口中的最小值、最大值/找出离他最近的最大/最小的元素

思考方式:用一个队列维护窗口里面的所有值,每次把一个数查到队尾,队头去除一个数;每次求最小值的时候进行暴力枚举

时间复杂度:O(nk)

优化:在队列中,如果后面有一个元素比当前元素小,前面的元素删掉(视为冗余元素);则可以看成从小到大递增的队列;则此时找最小值只需要O(1);整体复杂度O(n)

  1. 用普通队列怎么做
  2. 将队列中的冗余元素删掉——具有了单调性
  3. 可以用O(1)时间从队头/队尾取出最值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
using namespace std;
const int N=1000010;
int a[N],q[N],hh,tt=-1;
int main(){
int n,k;
cin>>n>>k;
for(int i=0;i<n;i++) {
scanf("%d",&a[i]);
if(i-k+1>q[hh]) ++hh;
while(hh<=tt&&a[q[tt]]>=a[i]) --tt;//如果队尾元素大于当前值,删掉·
q[++tt]=i;
if(i+1>=k) printf("%d ",a[q[hh]]);
}
cout<<endl;
hh=0,tt=-1;
for(int i=0;i<n;i++) {
if(i-k+1>q[hh]) ++hh;
while(hh<=tt&&a[q[tt]]<=a[i]) --tt;
q[++tt]=i;
if(i+1>=k) printf("%d ",a[q[hh]]);
}
return 0;
}

KMP

  1. 暴力算法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    S[N],p[M];
    for(int i=1;i<=n;i++){
    bool flag=true;
    for(int j=1;j<=m;j++){
    if(s[i]!=p[j]){
    flag=false;
    break;
    }
    }
    }
  2. 利用额外信息(每一个点的后缀和前缀相等的最大长度)——next[i]:以i为终点的后缀和从1开始的前缀相等,且后缀长度最长,即:p[1,j]=p[i-j+1,i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
using namespace std;
const int N=100010,M=1000010;
int n,m,i,j;
char p[N],s[M];
int ne[N];
int main(){
cin>>n>>p+1>>m>>s+1;//下标从1开始
for(i=2,j=0;i<=n;i++){//求next的过程,next[1]=0,也就是第一个不一样,只能从头开始
while(j&&p[i]!=p[j+1]) j=ne[j];
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
for(i=1,j=0;i<=m;i++){
while(j&&s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;//如果j退无可退
if(j==n){
printf("%d ",i-j);//下标是从0开始的
j=ne[j];
}
}
return 0;
}

Trie树

基本用法:高效地存储和查找字符串集合的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void insert(char str[]){//插入字符串
int p=0;
for(int i=0;str[i];i++){
int u=str[i]-'a';
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];
}

并查集

处理:

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合中

时间复杂度:近乎O(1)

基本原理:每个集合用一颗树来表示,树根的编号就是整个集合的编号,每个节点存储它的父节点,p[x]表示x的父节点

问题1:如何判断树根 :if(p[x]==x)

问题2:如何求x的集合编号:while(p[x]!=x) x=p[x]

问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号,p[x]=y

优化(路径压缩):一旦往上走找到了根节点,路径上所有的点直接指向根节点

做题时候,有一道题的find(int x)我用的

1
2
3
4
int find(int x){
while(x!=p[x]) x=find(p[x]);
return x;
}

直接TLE

后面改成优化的代码,AC了

1
2
3
4
int find(int x){
if(x==p[x]) return x;
return p[x]=find(p[x]);
}

食物链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<iostream>
#include<algorithm>
using namespace std;

int n,m,sum;
const int N=(1e4+10)*5,M=1e5+10;
int p[N],t[N];
int find(int x){
if(p[x]!=x){
int k=find(p[x]);
t[x]=t[p[x]]+t[x];
p[x]=k;
}
return p[x];
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;
while(m--){
int x,a,b;
cin>>x>>a>>b;
if(a>n||b>n) sum++;
else{
int pa=find(a),pb=find(b);
if(x==1){
if(pa==pb&&((t[a]-t[b])%3)) sum++;
else{
if(pa!=pb){
p[pa]=pb;
t[pa]=t[b]-t[a];
}
}
}
else{
if(pa==pb&&((t[a]-t[b]-1)%3)) sum++;
else{
if(pa!=pb){
p[pa]=pb;
t[pa]=t[b]-t[a]+1;
}
}
}
}
}
cout<<sum;
return 0;
}

堆排序

如何手写一个堆?

  1. 插入一个数

    1
    2
    heap[++size]=x;//堆的下标从1开始
    up(size);
  2. 求集合当中的最小值

    1
    heap[1];
  3. 删除最小值

    1
    2
    3
    heap[1]=heap[size];
    size--;
    down(1);
  4. 删除任意一个元素

    1
    2
    3
    4
    heap[k]=heap[size];
    size--;
    down(k);
    up(k);//up、down只会执行一个
  5. 修给任意一个元素

    1
    2
    3
    heap[k];
    down(k);
    up(k);//up、down只会执行一个

完全二叉树:除了最后一层其他层都是非空的,最后一层从左向右依次排布

小根堆:每一个点小于等于左右儿子

根节点:最小值

堆的存储:1号点位个节点;左儿子:2x;右儿子:2x+1(因此,堆的下标从1开始)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void down(int x){//把一个数变大了,进行下沉操作
int t=x;//t表示三个点中的最小值
if(2*x<=siz&&h[x]>h[2*x]) t=2*x;//判断左儿子
if(2*x+1<=siz&&h[t]>h[2*x+1]) t=2*x+1;//判断右儿子
if(x!=t) {
swap(h[x],h[t]);
down(t);
}
}
void up(int x){//把一个数变小了,看他该往上走到什么时候
while(x/2&&h[x/2]>h[x]){
heap_swap(x,x/2);
x=x/2;
}
}

堆排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int n,m,siz,h[N];
void down(int x){//把一个数变大了,进行下沉操作
int t=x;
if(2*x<=siz&&h[x]>h[2*x]) t=2*x;
if(2*x+1<=siz&&h[t]>h[2*x+1]) t=2*x+1;
if(x!=t) {
swap(h[x],h[t]);
down(t);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&h[i]);
}
siz=n;
for(int i=n/2;i;i--) down(i);//建堆,时间复杂度为n,原因讲解如图
while(m--){
printf("%d ",h[1]);
h[1]=h[siz];
siz--;
down(1);
}
return 0;
}

模拟堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<iostream>//用来堆优化Dijkstra
#include<algorithm>//支持随机修改删除
#include<string.h>
using namespace std;
const int N=100010;
int h[N],hp[N],ph[N],siz;
void heap_swap(int a,int b){
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a],hp[b]);
swap(h[a],h[b]);
}
void down(int x){
int t=x;
if(2*x<=siz&&h[x]>h[2*x]) t=2*x;
if(2*x+1<=siz&&h[t]>h[2*x+1]) t=2*x+1;
if(x!=t) {
heap_swap(x,t);
down(t);
}
}
void up(int x){
while(x/2&&h[x/2]>h[x]){
heap_swap(x,x/2);
x=x/2;
}
}
int main(){
int n,m=0;
scanf("%d%d",&n);
while(n--){
char op[10];
int k,x;
scanf("%s",op);
if(!strcmp(op,"I")){//插入元素
scanf("%d",&x);
siz++;
m++;
ph[m]=siz,hp[siz]=m;
h[siz]=x;
up(siz);
}
else if(!strcmp(op,"PM")) printf("%d\n",h[1]);//输出堆顶元素
else if(!strcmp(op,"DM")){//删除元素
heap_swap(1,siz);
siz--;
down(1);
}
else if(!strcmp(op,"D")){//删除第k个数
scanf("%d",&k);
k=ph[k];
heap_swap(k,siz);
siz--;
up(k),down(k);
}
else{//修改元素
scanf("%d%d",&k,&x);
k=ph[k];
h[k]=x;
down(k),up(k);
}
}
return 0;
}

模拟散列表(哈希表)

把较大的值域映射到较小的空间

  1. x mod 1e5(一般取质数,一般离2的整次幂较远)
  2. 冲突解决:开放寻址法/拉链法
  3. 一般只有添加和查找两个操作

存储结构

开放寻址法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<cstring>
#include<iostream>
using namespace std;
const int N=200003,null=0x3f3f3f3f;
int h[N];

int find(int x){
int k=(x%N+N)%N;//让余数>0
while(h[k]!=null&&h[k]!=x){//蹲坑法,如果坑位有人,并且这个人不是x
k++;
if(k==N) k=0;
}
return k;//如果查询的数在哈希表中,返回位置,否则返回可以存储的位置
}
int main(){
int n;
scanf("%d",&n);
memset(h,0x3f,sizeof h);//标记这个位置没有数据,memset函数需要cstring库
while(n--){
char op[2];
scanf("%s",op);//字符串读入尽量使用scanf,因为可以忽略回车、换行、制表符(有的数据会有空格)
int x;
scanf("%d",&x);
int k=find(x);
if(*op=='I') h[k]=x;//这个是插入,如果涉及到删除,只需要设置标记即可
else{
if(h[k]==x) puts("Yes");
else puts("No");
}
}
return 0;
}

拉链法

  • 开一个一维数组存储所有的值[0,1e5-1]
  • 每一个槽上的链用来存储这条链上的数
1
2
3
4
5
6
7
8
9
10
11
void insert(int x){
int k=(x%N+N)%N;
e[idx]=x,ne[idx]=h[k],h[k]=idx++;//头插法,该存储结构跟图论一样
}
bool find(int x){
int k=(x%N+N)%N;//让余数>0
for(int i=h[k];i!=-1;i=ne[i]){
if(e[i]==x) retirn true;
}
return false;
}

字符串哈希方式

使用字符串前缀哈希法

str=”ABCABCDEYXCACWING”

h[0]=0(h为哈希值)
h[1]=”A”
h[2]=”AB”
h[3]=”ABC”
h[4]=”ABCA”

A B C D
(1 2 3 4)p
=(1p^3^+2p^2^+3p^1^+4p^0^)mod Q

tips:

  1. 不能映射成0,因为他们mod Q之后都是0,会产生冲突
  2. 加入Rp足够好,就不存在冲突,所以不考虑冲突
  3. 一般来说,令p=131或13331,Q=2^64^,可以保证不会出现冲突