第四课:单双链表、(单调)栈和队列、KMP

目录

一、单双链表

双向链表:

二、(单调)栈和队列

1.普通的栈和队列

栈:

队列

栈习题:表达式求值

队列习题:滑动窗口

单调栈习题

三、KMP字符串匹配算法


一、单双链表

#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开始

如上所述,匹配过程大致应如下:

  1. 遍历s串,用双指针i,j分别指示正在匹配的s串,p串的下标
  2. 当s串与p串下一个位置不匹配时,si!=p(j+1),j要变为next(j)
  3. 若仍不匹配,j要再变为next(j)    (由此得出需要while循环,由于初始化为0,j=0时要出循环)
  4. 匹配时,j可以走到下一个位置,如果走到了n,就代表匹配成功。
  5. 如果匹配成功,需要继续匹配,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串

  1. 从p串下标为2处开始,与p串起点开始匹配。
  2. 不匹配时,需要让j=ne[j]。注意j=0时出循环
  3. j到达合适位置后,看p[i]是否等于p[j+1],若相等,则j++。
  4. 不相等则无变化
  5. 此时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
}   

字符串kmp模式匹配算法讲解

//这里填你的代码^^
#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
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值