目录
一、单双链表
#include<iostream>
using namespace std;
const int N=100010;
//head 表示头结点的下标
//e 表示节点i的值
//ne 表示节点i的next指针
//idx 存储当前已经用到了哪个点
int head,e[N],ne[N],idx;
void init()
{
head=-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; 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];
else remove(k-1);//注意是k-1
}
else
{
cin>>k>>x;
add(k-1,x);//千万注意是k-1,因为第一次插入操作的是head,向下标为0的地方插入,所以第k个数idx是k-1
}
}
for(int i=head;i!=-1;i=ne[i])
{
cout<<e[i]<<' ';
}
cout<<endl;
return 0;
}
比较值得注意的思想是:用idx表示当前用到了哪个节点。因此节点在数组中的位置和他在链表中的位置没有任何关系,链的存储用ne(next)来表示了。head是虚拟头结点,其值为第一个节点的下标。-1作为终止节点(尾结点)
双向链表:
#include<iostream>
#include<cstring>
using namespace std;
const int N=100010;
int l[N],r[N],e[N],idx;
int m;
void init()
{
//0是左端点,1是右端点
l[1]=0;
r[0]=1;
idx=2;
}
void insert(int k,int x)
{
e[idx]=x;
l[idx]=k;
r[idx]=r[k];
l[r[k]]=idx;r[k]=idx++;
}
void remove(int k)
{
r[l[k]]=r[k];
l[r[k]]=l[k];
}
int main()
{
init();
cin>>m;
while(m--)
{
string op;
int k,x;
cin>>op;
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);
else if(op=="IR") cin>>k>>x,insert(k+1,x);
else cin>>k>>x, insert(l[k+1],x);
}
for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";
return 0;
}
思想:下标0直接作为头结点,下标1作为终止节点;idx从2开始。(因此remove函数和insert函数中要用k+1)
插入操作分析:1.首先存储值 2.调整idx的左右链 3.调整k+1位置左链 4.调整k-1位置右链 5.idx++
删除操作:
1.k左边的结点 直接指向 k右边的结点 2.k右边的结点 直接指向 k左边的结点
二、(单调)栈和队列
1.普通的栈和队列
栈:
思想:单纯用一个数组和top模拟。插入一个元素就skt[++tt]=x, 删除直接tt--。
若栈为空时,刚好为0(第一个插入的数下标是1)
```
//这里填你的代码^^
#include<iostream>
using namespace std;
const int N=100010;
int stk[N],tt;//tt即是top 存储当前顶部元素的下标
int m;
int main()
{
cin>>m;
while(m--)
{
string op;int x;
cin>>op;
if(op=="push") cin>>x,stk[++tt]=x;
else if(op=="pop") tt--;
else if(op=="empty") cout<<(tt?"NO":"YES")<<endl;
else cout<<stk[tt]<<endl;
}
return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~
```
队列
思想:1.用hh,tt维护队头队尾。tt最开始为-1(第一个插入的数的下标就是hh,也就是0)
2.判空操作就是tt<hh。 3.插入操作为和栈一样,弹出操作则是从队头弹出,也就是直接hh++
#include<iostream>
using namespace std;
const int N=100010;
int q[N],hh,tt=-1;//tt为-1的作用:1.q[0]为第一个元素 2.判空操作可为hh>tt 3.队尾元素的下标是tt
int m;
int main()
{
cin>>m;
while(m--)
{
string op;int x;
cin>>op;
if(op=="push") cin>>x,q[++tt]=x;
else if(op=="pop") hh++;
else if(op=="empty") cout<<(hh<=tt?"NO":"YES")<<endl;
else cout<<q[hh]<<endl;
}
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3404419/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
栈习题:表达式求值
分析:1.计算过程,需要先计算优先级高的运算符。即当我们遇到一个新的优先级的运算符时,就把栈里存储的运算符从后往前计算一完。
2.括号内的运算需要特别处理,也就是当我们遇到一个括号时,先存入左括号。一直到我们遇到一个右括号时,就需要强制停止,运算栈内的运算符直到遇到左括号。最后需要将左括号弹出。
3.存储数字时,因为时一个字符一个字符的给出,纯数字需要特别处理一下。
4.当遍历完之后,栈内可能还存储这运算符,最后需要扫尾。
#include<iostream>
#include<stack>
#include<algorithm>
#include<string>
#include <unordered_map>
using namespace std;
unordered_map<char, int> h{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
stack<int> num;
stack<char> op;
void eval()
{
int a=num.top(); num.pop();//第二个操作数
int b=num.top(); num.pop();// 第一个操作数
char p= op.top();
op.pop();
int x=0;
if(p=='+') x=b+a;//注意b是第一个操作数,,,
if(p=='-') x=b-a;
if(p=='*') x=b*a;
if(p=='/') x=b/a;
num.push(x);
}
int main()
{
string s;
cin>>s;
for(int i=0; i<s.size(); i++)
{
if(isdigit(s[i]))//判断是不是数字;;读入一个操作数
{
int x=0,j=i;
while(j<s.size()&& isdigit(s[j]))
{
x=x*10+s[j]-'0';
j++;
}
i=j-1;
num.push(x);
}
else if(s[i]=='(') op.push(s[i]);//遇到左括号,直接入栈
else if (s[i]==')')
{
while(op.top()!='(') eval();//一直计算到遇到左括号
op.pop();//将左括号出栈
}
else
{
while(op.size()&&h[s[i]]<=h[op.top()]) eval();//即将进来的运算符优先级较低,可以先计算前面的运算
op.push(s[i]);
}
}
while(op.size()) eval();//注意循环过程,当循环结束时并没有计算完。
cout<<num.top();
return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3405958/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
队列习题:滑动窗口
分析:输出最大值用递减,最小值用递增,一递增队列为例
1.用一个递增队列存储给出数组a的下标。保证这个队列长度为k。(队列里只存窗口内值的下标)
2.当新进来的值不满足递增时,弹出队尾元素。直到新进来的值到达它该插入的位置。最后push。
3.对每个滑动窗口,最大值就是下标队头元素
#include<iostream>
using namespace std;
const int N=1000010;
int a[N],q[N],hh,tt;
int n,k;//最小值为递增队列,最大值递减队列
int main()
{
cin.tie(0);
cin>>n>>k;
hh=0,tt=-1;
for(int i=0;i<n;i++) cin>>a[i];
for(int i=0;i<n;i++)
{
if(hh<=tt&&i-k+1>q[hh]) hh++;//控制队列长度为k
while(hh<=tt&&a[q[tt]]>a[i]) tt--;//当队尾值比 a[i]大时弹出
q[++tt]=i;
if(i>=k-1) cout<<a[q[hh]]<<" ";//第k次之后才输出
}
cout<<endl;
hh=0,tt=-1;
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]]<<" ";
}
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3407641/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
单调栈习题
分析:运用递增栈,当我们遇到一个更小的数时,因为前面的比他大的数就再也不会用到了。
1.构建递增栈,当新进来的值小于等于栈顶元素时,则不断pop,直到找到新进来的值的位置。
2.此时栈顶元素就是第一个小于他的数。输出栈顶元素,最后push新进来的值
#include<iostream>
using namespace std;
const int N=100010;
int stk[N],tt;
int n;
int main()
{
stk[0]=-1;
cin>>n;
while(n--)
{
int x; cin>>x;
while(x<=stk[tt]) tt--;
cout<<stk[tt]<<" ";
stk[++tt]=x;
}
return 0;
}
三、KMP字符串匹配算法
原理:
先思考一下暴力做法:一个一个的匹配,如果发现不匹配了,匹配起点就右移一格,再一次从模板串的起点开始匹配。
但是这样就造成很多“浪费”,没有利用我们上一次匹配的信息,白白浪费了时间。
kmp算法:
当我们进行某次尝试后,p串的前一部分已经和s匹配(在红实线后处不匹配),我们可以试着找出一段(绿色),使得接下来的匹配可以直接从绿色段后面继续开始(因为上一次匹配成功匹配到了红实线处)。即使得绿色段等于绿色区间,并且等于紫色区间。这样就充分利用了之前尝试匹配时得到的信息。
接下来要继续匹配,即将模板串右移,即更改开始匹配的下标。前后的对应关系用next数组表示,即当前处在下标j处,j+1与s串不匹配时,令j=ne(j),就可继续匹配。
next数组图解如上
事实上,这个“绿色段”是不唯一的,为了节约更多的时间,我们应该找到最长的绿色段。即上图中最大的“j”。
考虑至此,不妨先让我们抛开如何求解next数组的问题,思考如何匹配的问题。
我们此处初始化next数组为0,字符串下标都从1开始
如上所述,匹配过程大致应如下:
- 遍历s串,用双指针i,j分别指示正在匹配的s串,p串的下标
- 当s串与p串下一个位置不匹配时,si!=p(j+1),j要变为next(j)
- 若仍不匹配,j要再变为next(j) (由此得出需要while循环,由于初始化为0,j=0时要出循环)
- 匹配时,j可以走到下一个位置,如果走到了n,就代表匹配成功。
- 如果匹配成功,需要继续匹配,j要变为next(j)
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)
{
printf("%d ",i-n+1-1);
j=ne[j];
}
}
接下来考虑求解next数组的问题。
next[0和1]显然为0,应该从下标2出开始求解。
求解过程与匹配过程非常相似,双指针i,j指示被匹配的p串(下标从2开始)和p串
- 从p串下标为2处开始,与p串起点开始匹配。
- 不匹配时,需要让j=ne[j]。注意j=0时出循环
- j到达合适位置后,看p[i]是否等于p[j+1],若相等,则j++。
- 不相等则无变化
- 此时p[1~j]==p[i-j+1~i],可认为解得next[i],令next[i]=j。
(此处j肯定是最大的next[i],因为next[i]不会被更小的数更新)
for (int i = 2, j = -0; i < len; i ++)
{
while (j != 0 && p[i] != p[j + 1]) // 前后缀匹配不成功
{
// 反复令 j 回退到 0,或是 p[i] == p[j + 1]
j = next[j];
}
if (p[i] == p[j + 1]) // 匹配成功
{
j ++; // 最长相等前后缀变长
}
next[i] = j; // 令 next[i] = j
}
//这里填你的代码^^
#include<iostream>
using namespace std;
const int N =100010, M=1000010;
int n,m;
int ne[N];
char s[M],p[N];
int main()
{
cin >> n>>p+1>>m>>s+1;
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;
}
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)
{
printf("%d ",i-n+1-1);
j=ne[j];
}
}
return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3440566/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。