XCPC第四站!链表+栈和队列+kmp一网打尽!

本文介绍了链表(包括单链表和双链表)的基础知识,并通过数组模拟实现,接着讲解了栈(单调栈)和队列(单调队列)的概念及其应用,最后详细阐述了经典的KMP字符串匹配算法,包括next数组的生成和递推计算方法。
摘要由CSDN通过智能技术生成

在这里插入图片描述

一、前言&前情回顾

前面我们已经学习了一些基础算法。从这一章开始,我们就会进入一些数据结构的学习啦!那么闲言少叙,就让我们直接开始吧!

二、链表(数组模拟)

二、1单链表

关于单链表的结构体模拟,请参看保姆级链表教程!一次搞定链表基础知识!
基本思想:用两个数组,一个数组存储值,另一个数组存储指向的结点编号(空结点的下标用-1表示)。如图:
在这里插入图片描述

下面我们来看看如何用代码具体实现吧!注意:以下所说的head头结点并非第一个结点,而是第一个结点前面的一个结点!如图:
在这里插入图片描述
3是第一个结点,下标为0,head是3的前面一个结点。这里的空链表指的是head结点指向NULL。

#include<iostream>
using namespace std;
const int N = 1e6;
int head,e[N],ne[N],idx;

//head表示头结点指向的下标
//idx表示当前已经存储到的结点下标(即预备要存进链表里的结点下标)

//初始化,头结点指向NULL
void Init()
{
	head = -1;
	idx = 0;
}
//将值为x的结点插到头结点后一个位置
void add_to_head(int x)
{
	e[idx] = x;
	ne[idx] = head;
	head = idx;
	idx++;
}
//将值为x的结点插到下表为k的结点的后面
void add_to_k(int x,int k)
{
	e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx;
	idx++;
}
//将下标为k的结点后面的一个结点删除
void remove_k(int k)
{
	ne[k] = ne[ne[k]];
}

在这里插入图片描述

#include <iostream>
using namespace std;

const int N = 1e6;

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;
    idx++;
}

void add_to_k(int k,int x)
{
//要注意,这里的k指的不是第k个插入的数,而是下标为k!
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}

void delete_k(int k)
{
    //让坐标为k的结点(即第k+1个结点)指向后面再后面的结点
    ne[k] = ne[ne[k]];
}

int main()
{
    int M;
    cin>>M;
    
    Init();
    
    while(M--)
    {
        char op;
        cin>>op;
        
        if(op=='H')
        {
            int x;
            cin>>x;
            
            add_to_head(x);
        }
        
        else if(op=='D')
        {
            int k;
            cin>>k;
            //当k为0时,表示删除头结点。
            if(!k)
            {
                head = ne[head];
            }
            //第k个插入的数
            delete_k(k-1);
        }
        
        else
        {
            int k,x;
            cin>>k>>x;
            //表面上我们删除了数据,但实际上我们改变的只是ne数组,e数组中仍然保存着值的数据,因此我们第k个插入的数在e中的下标就是k-1.
            add_to_k(k-1,x);
        }
    }
    //i!     =-1是为了防止访问空结点
    //i      = ne[i]是让i指向下一个结点
    for(int i= head;i!=-1;i=ne[i])
    {
        cout<<e[i]<<' ';
    }
}

二、2双链表

在这里插入图片描述

#include <iostream>
using namespace std;
const int N = 1e6;
//l[N]用来存储指向左边的“伪指针”,r[N]用来存储指向右边的“伪指针”
int l[N],r[N],e[N],idx;
//在下标是k的结点的右边插入值为x的结点
void add_to_k_right(int k,int x)
{
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx;
    idx++;
}
//在下标是k的结点的左边插入值为x的结点,相当于在下标为k的结点的左边的结点的右边插入x
void add_to_k_left(int k,int x)
{
    //一定要注意,左边未必是k-1,因为结点可能会被取下!所以要用这种表示法
    add_to_k_right(l[k],x);
}
//删除下标为k的点
void remove_k(int k)
{
    //让左边的右边等于右边,右边的左边等于左边
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}
int main()
{
    int M;
    cin>>M;
    //这次我们就不用虚拟头结点和虚拟尾结点了,0是头结点,1是尾结点
    r[0] = 1;l[1] = 0;idx = 2;
    
    while(M--)
    {
        string op;
        cin>>op;
        
        int k,x;
        
        if(op=="L")
        {
            cin>>x;
            
            add_to_k_right(0,x);
        }
        else if(op=="R")
        {
            cin>>x;
            
            add_to_k_right(l[1],x);
        }
        else if(op=="D")
        {
            cin>>k;
            //这下第k个插入的数的右边的数下标是k+1了!
            remove_k(k+1);
        }
        else if(op=="IL")
        {
            cin>>k>>x;
            
            add_to_k_left(k+1,x);
        }
        else if(op=="IR")
        {
            cin>>k>>x;
            
            add_to_k_right(k+1,x);
        }
    }
    
    for(int i = r[0];i!=1;i = r[i])
    {
        cout<<e[i]<<' ';
    }
    
    return 0;
}

三、栈和队列

三、1栈

栈是一种先进后出的数据结构。你可以把它想象成一个圆筒,只能从筒顶出数据,那么就是后面进入的数据先出来。

const int N = 1e6;
//stk是用来存储数据的数组,tt是栈顶的位置
int stk[N],int tt;
//插入x
stk[++tt] = x;
//删除x
tt--;
//判断栈是否为空
if(tt>0)//非空
else//空
//找到栈顶数据
stk[tt];

例:单调栈
在这里插入图片描述
暴力的做法是用两层循环:

for(int i = 0;i<n;i++)
{
	for(int j = i-1;j>=0;i--)
	{
		if(arr[j]<arr[i])
		{
			cout<<arr[j]<<' ';
			break;
		}
	}
}

我们能不能想办法优化一下呢?我们知道,若a(m)>=a(n)但是m<n,那么a(m)是一定不会作为答案出现的。因为若a(m)满足要求,a(n)自然也满足要求,但是a(n)更近。因此,所有这些构成逆序对的数的大的那一个我们都不用考虑,可以删掉。那么删掉以后我们将剩下的数据存到栈里,就得到一个单调栈。

#include <iostream>
using namespace std;

const int N = 1e6;

int stk[N],tt;

int main()
{
    int m;
    cin>>m;
    
    for(int i = 0;i<m;i++)
    {
        int x;
        cin>>x;
        //如果不符合题意,直接把前面 那个数删了就行
        while(tt&&stk[tt]>=x)   tt--;
        //如果到最后还没有退到0,说明找到了
        if(tt)  cout<<stk[tt]<<' ';
        else    cout<<-1<<' ';
        //最后别忘了把新的数插入栈
        stk[++tt] = x;
    }
    return 0;
}

三、2队列

队列可以被想象成一个两边开口的圆筒,我们可以在筒的一边放东西,在另一边拿东西,那么队列就是先进先出的。我们在队尾插入元素,在队头弹出元素。

const int N = 1e6;
//hh表示队列头,tt表示队列尾
int q[N];
int hh = -1;
int tt = -1;
//插入x
q[++tt] = x;
//弹出队头元素
hh++;
//判断队列是否为空
if(hh<=tt) not empty
else empty;
//取出队头元素
q[hh];
//取出队尾元素
q[tt];

例:单调队列
在这里插入图片描述
队列更新的步骤是:
1.将新的元素插入队尾。
2.将队头的元素弹出。
在这里插入图片描述

与上一题类似,如果我们挨个遍历,那么时间复杂度将会是O(Nk)。但是我们可以注意到,对于一个窗口来说,右边的数总是比左边的数后弹出。那么如果右边的数<左边的数,那么左边的数就永远不可能作为最小数输出。(是否似曾相识?这又是逆序对中较大的数没有机会的思想)所以最后我们得到的队列一定是单调上升的。那么单调上升的队列的最小值是什么呢?当然就是队头的元素啦!所以我们只需要取出队头的元素即可。下面让我们一起来看看具体的代码实现:

#include <iostream>

using namespace std;

const int N = 1000010;
//a[N]是原数组,q[N]是队列,队列里存的是下标!
int a[N], q[N];

int main()
{
    //把n个数读到a数组里
    int n, k;
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    //hh表示队头,tt表示队尾
    int hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        //判断队头是否已经划出窗口。这里i是窗口的终点,那么窗口的起点就是i-k+1,如果它>q[hh],说明队头已经滑出了窗口
        //在判断之前,还要判断队列是否为空。若hh<=tt则不为空
        //为什么这里用if不用while呢?因为我们的窗口只能滑动一次,每次队列中最多只有一个不在窗口内
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;
        //如果a[q[tt]]>=a[i]那么它一定不是答案,就出队
        while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
        q[ ++ tt] = i;

        if (i >= k - 1) printf("%d ", a[q[hh]]);
    }

    puts("");

    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) printf("%d ", a[q[hh]]);
    }

    puts("");

    return 0;
}

// 作者:yxc
// 链接:https://www.acwing.com/activity/content/code/content/43107/
// 来源:AcWing
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

四、kmp算法

kmp算法是应用于字符串匹配的经典算法。请看以下例题:
在这里插入图片描述
我们很容易想到一种暴力求解的算法,那就是用两个“伪指针”i、j分别指向主串和模板串,逐个比较主串和子串中的字母,若不相同,则让i++,再让j从0开始到i逐个比较。但是这样会造成很多重复的比较,由于我们在前一次的比较中已经掌握了一些信息,我们能不能利用这些信息对下一次比较做出优化呢?这时我们可以利用next数组。我们先不用理会next数组是如何得到的,只要清楚它记录了下一次比较时我们可以跳过的字符个数就行啦。例如:
在这里插入图片描述
第一次出现不匹配的字符是C,我们观察它前一个字符next值,就知道在下一次匹配时子串可以跳过2个字符,从第二个A开始匹配。
       那么这种神奇的next数组是如何生成的呢?我们直接给出结论:next的值是到当前字符范围内最长公共前后缀的长度。例如在上面的例子中,第一个A没有公共前后缀,因此next值为0;第一个B没有公共前后缀,next值也为0;第二个A有公共前后缀A,因此next值为1……那么,难道计算next值时也要用暴力遍历的方法么?并不,我们可以利用递推。假定我们已经知道最后一个字符的next值,那么若下一个字符可以与已有最长公共前后缀构成新的公共前后缀,那么新的next值让前一个+1即可;如果不能,我们可以通过分析前缀的情况来得知后缀的情况,因为前后缀是公共的。这时,我们只要访问对应的数组下标即可。
      那么下面就让我们一起来看看具体的代码实现吧!

#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);
            j = ne[j];
        }
    }

    return 0;
}

// 作者:yxc
// 链接:https://www.acwing.com/activity/content/code/content/43108/
// 来源:AcWing
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如果读者想了解具体的证明过程,不妨看看这个视频:
最浅显易懂的kmp算法讲解!
相信你一定会感到大有收获的。
好啦,以上就是本篇文章的全部内容。如果你感觉对自己有帮助的话,请三连支持一下作者,你们的支持是我不断更新的最大动力!

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值