写在前面(持续更新中,会补充很多细节的尤其是一些总结&&知识点干货,还有一些dalao的图解/ 小方法,可以先收藏 等后续看完整滴!)
题源:AcWing算法基础课,本文谨以记录学习过程 + 复盘,如有写的模糊 or 错误的地方敬请谅解 and 在评论区指正,谢谢~
博主大一(备战明年蓝桥杯ing),周末&&寒假 有兴趣互相打卡学习进度的同学可以私信我!互相监督进步hhhh(也不一定必须备战蓝桥杯嗷,其他相关学习/运动都可以一起打卡!!
目录
写在前面(持续更新中,会补充很多细节的尤其是一些总结&&知识点干货,还有一些dalao的图解/ 小方法,可以先收藏 等后续看完整滴!)
单链表
AcWing 826.单链表
#include <iostream>
#include <algorithm>
using namespace std;
const int N=100010;
int head,idx,e[N],ne[N];
void init()//初始化
{
head=-1;//-1为空.最后的空节点不会消失,在第一次头插后ne[idx]=-1即指向它
idx=0;
}
//头插
void add_to_head(int x)
{
e[idx]=x;
ne[idx]=head;
head=idx++;
}
//插入下标为k的数后面
void add(int k,int x)
{
e[idx]=x;
ne[idx]=ne[k];
ne[k]=idx++;
}
//删除下标为k的数后面的数
void remove(int k)
{
ne[k]=ne[ne[k]];
}
int main()
{
int m, k, x;
char op;
init();
cin>>m;
while(m--)
{
cin>>op;
if(op=='H')
{
cin>>x;
add_to_head(x);
}
else if(op=='D')
{
cin>>k;
if(k==0) head=ne[head];//head指向头节点的下标
else remove(k-1);//第一个插入的数idx=0,所以第k个插入的数下标为k-1
}
else
{
cin>>k>>x;
add(k-1,x);
}
}
//遍历list
for(int i=head;i!=-1;i=ne[i]) cout<<e[i]<<" ";
cout<<endl;
return 0;
}
头指针head初始化为-1表示链表为空,idx表示当前节点的下标(参考了数组下标从0开始),无论是头插还是一般的插入,都是 1.存储数据 2.新节点的指针指向右侧 3.左侧节点(或头指针)指向新节点 三个步骤。删除某节点后面的节点,那就直接让这个节点的指针指向下一个的下一个(至于被删的节点就孤零零留那里咯·)
这题要注意操作的是“第k个插入的数”,第一个插入的idx为0,所以注意写码是k-1
双链表
AcWing 827.双链表
#include <iostream>
#include <algorithm>
using namespace std;
const int N=100010;
int m;
int e[N], l[N], r[N], idx;
//在节点k的右边插入一个点x
//写code之前可以画个图!
void insert(int k,int x)
{
e[idx]=x;
r[idx]=r[k];
l[idx]=k;
l[r[k]]=idx;//下面这两行不能改变顺序,因为要在修改r[k]之前完成这一步
r[k]=idx++;
}
//删除下标为k的节点
void remove(int k)
{
l[r[k]]=l[k];
r[l[k]]=r[k];
}
int main()
{
r[0]=1, l[1]=0;//0是左端点,1是右端点
idx=2;
cin>>m;
while(m--)
{
string op;
cin>>op;
int k, x;
if(op=="L")
{
cin>>x;
insert(0,x);
}
else if(op=="R")
{
cin>>x;
insert(l[1],x);
}
else if(op=="D")
{
cin>>k;
remove(k+1);//第一个插入的数下标为2,第k个插入的数下标为k+1
}
else if(op=="IL")
{
cin>>k>>x;
insert(l[k+1],x);//在节点左边插入相当于在l[k]的右边插
}
else if(op=="IR")
{
cin>>k>>x;
insert(k+1,x);
}
}
//遍历时初始化为r[0],避免多打印了无效的0
for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";
cout<<endl;
return 0;
}
左端点是0,右端点是1。实现了只写在某节点右边插入新节点的函数insert,却可以实现头插/尾插/一般情况插入。插入时要记住先把右侧节点的 l [ r[k] ] = idx,再r[k]=idx++(后修改r[k])。遍历时 i 初始化为r[0](最左端节点),直到 i==1停止(最右端的指针),每次i=r[i]即可实现从左到右遍历
栈
队列
AcWing 829.模拟队列
#include <iostream>
#include <algorithm>
using namespace std;
const int N=100010;
int q[N];
int hh=0,tt=-1;
void push(int x)
{
q[++tt]=x;
}
void pop()
{
hh++;
}
void empty()
{
if(tt>=hh) printf("NO\n");//队尾大于等于队头时队列不为空
else printf("YES\n");
}
void query()
{
printf("%d\n",q[hh]);
}
int main()
{
int m, x;
cin>>m;
string s;
while(m--)
{
cin>>s;
if(s=="push")
{
cin>>x;
push(x);
}
else if(s=="empty") empty();
else if(s=="pop") pop();
else query();
}
return 0;
}
队列是一种先进先出的数据结构,用hh和tt分别代表队头和队尾。出队只需++hh,入队只需q[++tt],纯 模板题 啦
单调栈
AcWing 830.单调栈
//性质:一旦右边的数比左边的数小,左边的数将不再有用,出栈!
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=100010;
int tt, stk[N];
int main()
{
int n, x;
cin>>n;
for(int i=0;i<n;i++)
{
scanf("%d",&x);
while(tt && stk[tt]>=x) tt--;
if(!tt) printf("-1 ");//栈为空,tt==0
else printf("%d ",stk[tt]);//栈顶即为左边第一个满足性质的数
stk[++tt]=x;//新元素入栈
}
return 0;
}
栈是一种先进后出的数据结构。(相当于一个垃圾桶只能放进去 or 拿出最上面的东西)数组模拟栈就肥肠煎蛋,tt表示栈中元素个数
单调队列
AcWing 154.滑动窗口
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=1000010;//看清数据范围!!1e6
int n,k;
int a[N],q[N];//q[]存放的是下标
int main()
{
int hh=0,tt=-1;//队头在左,队尾在右,窗口向右滑
cin>>n>>k;
for(int i=0;i<n;i++) scanf("%d",&a[i]);
//处理最小值
for(int i=0;i<n;i++)
{
//判断队头是否出队
if(hh<=tt && q[hh]<i-k+1 ) ++hh;
while(hh<=tt && a[q[tt]]>= a[i]) --tt;//若队尾元素大于新元素,则这个元素后面将一点用都没有,出队
q[++tt]=i;//把当前元素入队
if(i+1>=k) printf("%d ",a[q[hh]]);//在形成窗口后,队头一直都是当下的最小值
}
puts("");
hh=0,tt=-1;//不要忘记重置队列
for(int i=0;i<n;i++)
{
if(hh<=tt && q[hh]<i-k+1 ) ++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
AcWing 831.KMP字符串
/*KMP的思考方式和单调队列、双指针算法是类似的,先想清楚朴素算法怎么做,然后如何去优化
求next数组时,我们关心对于每个不同的下标i,j能走多远;匹配时,我们只关心j是否走到末尾
ne[1]=0,因为第一个元素如果匹配失败,那只能从零开始
所以求ne[i]时i从2开始即可*/
//对模板串的每一个点都预处理出来:后缀和前缀相等的最大长度,即ne[i]
#include <iostream>
#include <algorithm>
using namespace std;
const int N=100010,M=1000010;
int n,m;
int ne[N];
char p[N],s[M];
int main()
{
cin >> n >> p+1 >> m >> s+1;
//处理ne[],j表示已经成功匹配的个数
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;//i是不会倒退的,所以每次最后更新出来的j一定是最优的
}
//匹配字符串
for(int i=1,j=0;i<=m;i++)
{
while(j && s[i]!=p[j+1]) j=ne[j];//注意是一旦匹配不成功就要一直倒退(while)
if(s[i]==p[j+1]) j++;
if(j==n)
{
printf("%d ",i-n);//不妨考虑直接匹配成功,此时i=n,输出0
j=ne[j];//继续尝试下一次匹配
}
}
return 0;
}
Trie
并查集
AcWing 836.合并集合
/* 并查集
1.将两个集合合并
2.询问两个元素是否在集合中
基本原理:每个集合都用一棵树表示,树根的编号就是整棵树的编号
每个节点存储它的父节点
Q1:如何判断树根:if(p[x]==x)
Q2:如何求x的集合编号:while(p[x]!=x) x=p[x];
Q3:如何合并两个集合(编号px、py) :p[x]=y */
//优化:路径压缩
#include <iostream>
using namespace std;
const int N=100010;
int n, m;
int p[N];
int find(int x)//返回祖宗节点+路径压缩
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main()
{
char op[2];
int a, b;
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;
while(m--)
{
scanf("%s%d%d",op, &a, &b);
if(*op=='M') p[find(a)]=find(b);//如果a、b已经在一个集合内,相当于将祖宗节点自身赋给自己
else
{
if(find(a)==find(b)) printf("Yes\n");
else printf("No\n");
}
}
return 0;
}
AcWing 837. 连通块中点的数量
//询问连通块中点的数量,只需让根节点的size有意义即可
//合并后只需size[b]+=size[a];
#include <iostream>
using namespace std;
const int N=100010;
int n, m;
int p[N], cnt[N];
int find(int x)//返回祖宗节点+路径压缩
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main()
{
char op[3];
int a, b;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
p[i]=i;
cnt[i]=1;
}
while(m--)
{
scanf("%s",op);
if(*op=='C')
{
cin>>a>>b;
if(find(a)==find(b)) continue;//如果在同一集合中就不要再合并了,不然下一行cnt又要多加
cnt[find(b)]+=cnt[find(a)];//把一棵树插过去,只改变根节点的size即可(一定是根节点!)
p[find(a)]=find(b);
}
else if(op[1]=='1')//Q1和Q2的区别只在于op[1]
{
cin>>a>>b;
if(find(a)==find(b)) printf("Yes\n");
else printf("No\n");
}
else
{
cin>>a;
printf("%d\n",cnt[find(a)]);
}
}
return 0;
}
堆
AcWing 838.堆排序
/* 手写一个堆
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);
5.修改任意一个元素 heap[k]=x; down(k);up(k);
用一维数组存下一个堆,根节点是1而不是0是因为这样比较方便,也避免了0*2=0
x的左儿子是2x,右儿子是2x+1
*/
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
const int N=100010;
int n, m;
int h[N], cnt;
void down(int u)
{
int t=u;
if(u*2<=cnt && h[u*2]< h[t]) t=u*2;
if(u*2+1<=cnt && h[u*2+1]<h[t]) t=u*2+1;
if(u!=t)
{
swap(h[u], h[t]);
down(t);//递归
}
}
void up(int u)
{
while(u/2 && h[u/2]<h[u])
{
swap(h[u/2], h[u]);
u/=2;
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
cnt=n;
for(int i=n/2; i; i--) down(i);
while(m--)
{
printf("%d ",h[1]);
h[1]=h[cnt],cnt--;
down(1);
}
return 0;
}
AcWing 839.模拟堆
/*
这题要额外开两个数组 ph[k]存第k个数的下标, hp[k]存堆里下标是k的点在ph[]中下标
能通过hp[a]反找ph[]的下标,它们互为反函数
更新的时候记得两个数组也都要改
*/
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N=100010;
int h[N], ph[N], hp[N], cnt;
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 u)
{
int t=u;
if(u*2<=cnt && h[u*2]< h[t]) t=u*2;
if(u*2+1<=cnt && h[u*2+1]<h[t]) t=u*2+1;
if(u!=t)
{
heap_swap(u, t);
down(t);//递归
}
}
void up(int u)
{
while(u/2 && h[u/2]>h[u])
{
heap_swap(u/2, u);
u/=2;
}
}
int main()
{
int n, m=0;
cin>>n;
while(n--)
{
char op[10];
int k, x;
scanf("%s",op);
if(!strcmp(op,"I"))
{
cin>>x;
cnt++;
m++;
ph[m]=cnt, hp[cnt]=m;
h[cnt]=x;//插入元素
up(cnt);
}
else if(!strcmp(op,"PM")) printf("%d\n",h[1]);
else if(!strcmp(op,"DM"))//删除最小值把最后一个元素交换后删除堆中最后一个元素,down(1)
{
heap_swap(1,cnt);
cnt--;
down(1);
}
else if(!strcmp(op,"D"))
{
cin>>k;
k=ph[k];
heap_swap(k,cnt);
cnt--;
down(k), up(k);
}
else
{
cin>>k>>x;
k=ph[k];
h[k]=x;
down(k), up(k);
}
}
return 0;
}