学习笔记1_kmp算法

(这篇文章是一篇以小白的视角对kmp算法的理解,代码并不简洁,但感觉比较好懂)

kmp算法是做什么的?

        当我们有两个字符串a和b,我们想知道a中出现了几次b,在哪里出现的,我们就可以用这个算法。(当然一定比暴力的复杂度小很多)

首先我们看一个例子

这有一个例子

当我们在a中找b:

a串:ababababacab

b串:ababc

我们通常会用两个指针i和j,如果a[i] == b[j],就i++;j++; 可见,当i=4,j=4时(字符串第一个下标是0),a[i]是‘a’,b[j]是‘c’,它们不同了,按照暴力的方法,我们会将b串后移一位,令i=1,j=0,重新比较,就像这样

  i
a b a b a b a b a c a b
  a b a b c
  j

那有没有一种可能,我们可以将b串直接后移两位,理由如下:

a b a b a b a b a c a b
    a b a b c

首先为什么我们不后移一位?

因为在比较a[4]和b[4]之前,我们一定已经确认了

(1)a串的0~3一定和b串的0~3完全一样,即a[0,3] == b[0,3]

0 1 2 3 4
a b a b a b a b a c a b
a b a b c
0 1 2 3 4

且我们在拿到字符串b的一瞬间就知道

(2)b串的1~3和0~2不同("bab" != "aba"),即b[1,3] != b[0,2]

a b a b 
  a b a b 

由(1)可知a[1,3] == b[1,3] ,又(2)b[1,3] != b[0,2],则a[1,3] != b[0,2]

  1   3
a b a b a b a b a c a b
  a b a b c
  0   2

也就是说我们根本不用比较,就可以知道将b串后移一位肯定不行

but为什么后移2位就一定行呢?

由(1)我们知道a[2,3] == b[2,3]

    2 3
a b a b a b a b a c a b
a b a b c
    2 3

且我们在拿到字符串b的一瞬间就知道

(3)b[0,1] == b[2,3]

a b a b c
0 1 2 3

那么由a[2,3] == b[2,3]; b[0,1] == b[2,3]  我们一定能得到b[0,1] == a[2,3]

    2 3 
a b a b a b a b a c a b
    a b a b c
    0 1

这时前两位我们知道相同了,就可以直接从i = 4, j = 2开始比了

到这里,我们就已经见识过kmp的核心了(这个例子让kmp看上去很鸡肋,但大伙去oj试试就知道数据量大的时候差多少了)

回归正题,kmp的初步思路

我们要在字符串a中找字符串b

指针i和j初始是0

如果a[i] == b[j],就i++;  j++;  //这行和暴力的方法是一样的

如果a[i] != b[j](重点来了),就让j退到一个位置(相当于将字符串b后移了),使得b[0, j - 1] == a[i - j, i - 1](大白话:b的开头==a的i之前的部分),即b[0, j - 1] == b[j0 - j, j0 - 1](大白话:b的开头==b的原来的j之前的部分)

画了个小图:)))))

                                    i
-----------------------------(--s--)X----------...
            (--s--)----------(--s--)Y---
                                    j
 
                      |
                      |
                      V
                                    i
-----------------------------(--s--)X----------...
            (--s--)----------(--s--)Y---
                   j

相当于后移:
                                    i
-----------------------------(--s--)X----------...
                             (--s--)----------(--s--)Y---
                                    j

那么问题来了,我们怎么知道让j退多少呢?

神奇的next数组(由于next触发关键字,我就写作nxt[]了)

nxt[i] = 字符串b中以i位为结尾以0位为起始的子串最长的相同的前缀和后缀的长度

(用语言描述可以说是相当晦涩。。其实就是上面那个图里s的长度)

举个栗子:

i      0 1 2 3 4 5 6 7 8 9   
b[i]   a b a b c a b a b c
nxt[i] 0 0 1 2 0 1 2 3 4 5

理解了这个数组是啥东西,聪明的读者应该发现了这个nxt数组不就可以解决j退多少的问题吗

我们在需要让j退回时,直接j = nxt[j - 1];不就行了

好的,那我们现在的目标就变成如何填这个nxt数组了,显然我们可以考虑一下nxt[i]和nxt[i + 1]

的关系

分情况讨论一下:

1.若b[i + 1] == b[nxt[i]],就是:

                        i i+1
                        | |
a b a X - - - - - - a b a X - -
      |
     nxt[i]

那么nxt[i + 1] = nxt[i] + 1 (看图很显然)

2.若b[i + 1] != b[nxt[i]](重点来了)

                        i i+1
                        | |
a b a X - - - - - - a b a Y - -
      |
     nxt[i]

请时刻记住我们的目标,填nxt[i+1],找最长的相同的前缀和后缀

这是不是很像上文找子串的过程?(如果我这么画就能看出来了)

                        i i+1
                        | |
a b a X - - - - - - a b a Y - -
                    a b a X - - - - - - a b a Y - -
                          |
                          j=nxt[i] 发现X和Y不同了,就让j退到合适的位置...

是不是有点眼熟,下一句应该是“那么问题来了,我们怎么知道让j退多少呢?”,but我们这时怎么就不知道呢?我们已经求完了j之前的所有nxt[i]了啊,我们只需要j = nxt[j - 1];就完成了

                        i i+1
                        | |
a b a X - - - - - - a b a Y - -
                    a b a X - - - - - - a b a Y - -
                      |
                      nxt[j-1]
                          
相当于:
a b a X - - - - - - a b a Y - -
                        a b a X - - - - - - a b a Y - -
                          |
                          nxt[j-1]

但又有新的问题了,如果Y是'd',也就是

3.若我们无论将j退到哪里都找不到b[i + 1] == b[j]

也就是当判断完b[i + 1] == b[j],我们发现j == 0

那其实很简单,那说明i+1没救了,nxt[i+1]就是0了,我们直接i++就行了

不多说了直接放码,注释中写了一些思路,可以参考来找灵感(烂但确实是ac代码):

#include<iostream>
using namespace std;
string a,b;
int na,nb;
int nxt[1000005];
void getnext(){  //获取nxt数组
    int i = 1;  //要填nxt[i],因为nxt[0]一定等于0,所以我们从1开始(你非要从0也行)
    int j = 0;  //保证j之前的字符串和i之前对应长度的字符串完全一样,那么,如果b[i]==b[j],就可以说明,字符串b的[0,i]部分和[j-i,j]部分字符一一相同,也就是nxt[i] = j+1;
    while(i < nb){  //接下来的任务主要是按上面的要求维护j,且,如果填完了nxt[i],就i++
        if(b[i] == b[j]){  //若i位置和j位置的字符一样,且已知它们前面的字符也对应相同,那么我们就知道nxt[i] = j + 1;并后移ij(为什么呢,i++是因为nxt[i]填完了,j++是维护j的过程,现在b[j]==b[i]了,那j+1之前的字符串和i之前对应长度的字符串完全一样了,也就是j可以等于j+1)
            nxt[i] = j + 1;
            i++;
            j++;
        }
        else if(j == 0){  //如果ij位置的字符不同,且j又退到0了,那就说明没救了,nxt[i]就是0了,填完了就i++
            nxt[i] = 0;
            i++;
        }
        else{  //如果ij位置的字符不同,但j还能后退,并且j还是要满足上述条件,我们这时就可以借助已经填完的nxt数组,nxt[j-1]就是以j-1结尾的子串的相同前后缀的最长长度,也是这个前缀的末位的下一位
            j = nxt[j - 1];  //我们就可以试试nxt[j-1]位的字符和i位的一不一样,不一样就再退,那为什么不退到其他位置呢?因为退到其他位置后,这个位置之前的部分都有字符不同的情况,那么纵使这个位置和i位置的字符一样,也没有用
        }
    }
}

kmp完整思路

其实之前已经说个大概了,只需要补充一下

我们要在字符串a中找字符串b

指针i和j初始是0

如果j跑完了整个b串,就输出,并且退j,即j = nxt[j - 1];

如果a[i] == b[j],就i++;  j++;

如果a[i] != b[j]又j == 0,就i++

如果a[i] != b[j] ,退j,即j = nxt[j - 1];

码(和上面的码拼在一起就是完整的):

int main(){
    cin >> a >> b;
    na = a.size();
    nb = b.size();
    getnext();
    //for(int i = 0;i < nb;i++) cout << nxt[i] << ' ';
    int i = 0;
    int j = 0;
    while(i <= na){  //注意要取等,因为你要给机会让j跑到nb
        if(j == nb){  //如果j跑完了整个b串,就输出,并且j退到上一个满足j条件(上面的注释说了)的位置
            cout << i - nb + 1 << endl;
            j = nxt[j - 1];
        }
        if(a[i] == b[j]){  //如果ij对应的字符一样,就可以比i+1和j+1的字符一不一样了
            i++;
            j++;
        }
        else if(j == 0){  //如果ij位的字符不同(因为上一个if都没通过),j又退无可退了,就可以判死刑,继续判断i+1了
            i++;
        }
        else{  //如果ij位字符不同,j可退,就看看j最近能退到哪,且退完j还要满足j条件,那我们就可以去看b串以j-1结尾的子串的最长相同前后缀有多长,就是nxt[j-1]
            j = nxt[j - 1];
        }
    }
    for(int i = 0;i < nb;i++) cout << nxt[i] << ' ';
    return 0;
}

算了还是复制一个源码吧。。(这道题是洛谷的P3375,【模板】KMP字符串匹配 - 洛谷

#include<iostream>
using namespace std;
string a,b;
int na,nb;
int nxt[1000005];
void getnext(){
    int i = 1;
    int j = 0;
    while(i < nb){
        if(b[i] == b[j]){
            nxt[i] = j + 1;
            i++;
            j++;
        }
        else if(j == 0){
            nxt[i] = 0;
            i++;
        }
        else{
            j = nxt[j - 1];
        }
    }
}
int main(){
    cin >> a >> b;
    na = a.size();
    nb = b.size();
    getnext();
    //for(int i = 0;i < nb;i++) cout << nxt[i] << ' ';
    int i = 0;
    int j = 0;
    while(i <= na){
        if(j == nb){
            cout << i - nb + 1 << endl;
            j = nxt[j - 1];
        }
        if(a[i] == b[j]){
            i++;
            j++;
        }
        else if(j == 0){
            i++;
        }
        else{
            j = nxt[j - 1];
        }
    }
    for(int i = 0;i < nb;i++) cout << nxt[i] << ' ';
    return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值