天天看点

KMP算法KMP算法前言1.KMP算法概述2.next数组3.KMP实现来自作者最真诚的吐槽

文章目录

  • KMP算法
  • 前言
  • 1.KMP算法概述
  • 2.next数组
  • 3.KMP实现
    • KMP的简单应用:
      • [J - 剪花布条](https://vjudge.net/problem/HDU-2087)
        • 题意:
        • 思路:
  • 来自作者最真诚的吐槽

KMP算法

字符串匹配算法,是在实际工程中经常遇到的问题,也是各大公司笔试面试的常考题目。此算法通常输入为原字符串(string)和子串(pattern),要求返回子串在原字符串中首次出现的位置(做题的时候变化挺多的,基本上没有只返回首次出现的位置,一般都是返回所有的位置,或者出现的次数,还有让你打印出next数组)。比如原字符串为“ABCDEFG”,子串为“DEF”,则算法返回3。常见的算法包括:BF(Brute Force,暴力检索)、RK(Robin-Karp,哈希检索)、KMP(教科书上最常见算法)、BM(Boyer Moore)、Sunday等。今天我来介绍一下KMP算法,以及基本的代码实现和每一步的原理分析……

前言

我觉得KMP算法比较容易理解,你要是认真看了阮大神的KMP文章,基本上就明白他的原理了(懂是懂,但是看不懂算法?! ……)其实就有两部分比较重要,第一个是next数组的构建,另一个就是如何实现他的字符匹配。(我手推了一下下,发现十行代码之间,KMP灰飞烟灭,属实有点神奇!!!)

1.KMP算法概述

首先还是简单介绍一下KMP与传统的暴力匹配算法的不同点在哪里,难点在哪里。

我们先来看一下暴力的字符串匹配算法

// 暴力匹配(伪码)
int search(String pat, String txt) {
    int M = pat.length;
    int N = txt.length;
    for (int i = 0; i <= N - M; i++) {
        int j;
        for (j = 0; j < M; j++) {
            if (pat[j] != txt[i+j])
                break;
        }
        // pat 全都匹配了
        if (j == M) return i;
    }
    // txt 中不存在 pat 子串
    return -1;
}
           

对于暴力算法,如果出现不匹配字符,会同时回退

txt

pat

指针,时间复杂度为

O(MN)

,空间复杂度为

O(1)

,最主要的问题是,如果字符串中重复的字符较多,该算法就很蠢。

比如

tex

= ‘’ aaacaaab’’,

pat

= ‘‘aaab’’:

很显然

pat

中跟本没有c,导致在c这里比较了4次,根本没有必要退回指针

i

,暴力算法很明显做了不必要的操作。

KMP优秀就优秀在他花费了一些空间来记录一些信息,来解决上述的情况

比如

txt

= “aaaaaaab”,

pat

= “aaab”,暴力算法会和上面那个例子一样蠢蠢的返回指针i,而kmp算法就会耍小聪明

因为KMP算法知道b字符之前的a都是匹配的,所以每次只需比较b是否被匹配即可

KMP永远不会回退

txt

的指针,就像 我高中晓明老师说的好:不走回头路。(就是不会重复扫描

txt

),而是借助next数组的信息把

pat

移动到正确的位置继续匹配.

这个算法的难点在于如果计算next函数的信息?如何根据这些信息正确的移动

pat

的指针?

所以我们接下来来解决一下next数组

2.next数组

咱们再举个栗子

txt

= “BBC ABCDAB ABCDABCDABDE”

pat

= “ABCDABD”

我们要求的是

pat

的next数组

首先next数组其实是字符串的部分匹配表,由部分搜索词和部分匹配值组成

搜索词就是

pat

的每一个元素

部分匹配值就是前缀和后缀的最长共有元素长度

在这之前,我们要先知道什么是前缀和后缀。

前缀是指除了最后一个字符以外的,一个字符串的全部头部组合;

后缀是指除了第一个字符以外的,一个字符串的全部尾部组合

比如字符串“bread”

前缀:b, br, bre, brea

后缀:read, ead, ad,d

对于

ABCDABD

的部分匹配表如下

  • "A"的前缀和后缀都为空集,共有元素的长度为0;
  • "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
  • "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
  • "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
  • “ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A”,长度为1;
  • “ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB”,长度为2;
  • "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
KMP算法KMP算法前言1.KMP算法概述2.next数组3.KMP实现来自作者最真诚的吐槽

看到这里你应该明白什么是next数组了,但是你心中一定有个疑惑,我怎么用代码实现呢???

别急,听我慢慢道来

你先观察一下上面那个ABCDABD,你会发现第二个A出现的时候,其匹配值就是1,而他后面的B如果和第一个A后面的字符相等,那他的匹配值就是2,一看,确实是一样的B,所以就变成了2。

其实是这么一个过程:你是从第一个开始求,但是第一个字符只有一个元素,故没有前缀和后缀,就是0,然后你就开始比较第二个,一看,A != B,所以也是0,再看三四,都是0,直到你看到了第五个,第五个的A == 第一个的A ,一看匹配成功,我们就记录第五个数的匹配值是1。再就比较下一个字符,也就是第六个。这个时候细心的同学就会问:哎呀,你怎么就判断了这一个元素就判定他是1,而不去遍历看看他的所有前缀后缀有没有其他的一样的,而且长度还超过1的?其实第i个是在第i- 1 的基础上进行的,如果第i个与前面那个匹配了,就在第i- 1的基础上加1.等你看第6和7个字符就明白了,因为第五个字符与第一个字符匹配了,我们就把第六个字符与第二个字符比较一下,实现了“移动”。这个时候发现第六个和第二个字符是相等的,所以第6个的部分匹配值就在第五个的基础上加1,1 + 1 = 2,就是2.。你写一下想一想,就能明白是什么意思。然后我们在看第七个与第三个比较,一看C != D,匹配失败,就为0.这个时候如果还有第八个元素,那第八是要和第一个重新比较的,因为第七个已经匹配失败,所以他的前缀后缀共有元素最多就是1(当且仅当第八个和第一个匹配成功)。

next构造的代码奉上

int next1[1000005];
void getnext()//打表,构造出next数组(这也太神奇了吧这个法)
{
    int j, k;
    j = 0;
    k = -1;
    next1[0] = -1;//当字符串只有一个元素的时候,就没有前缀和后缀,所以直接赋值为0;
    while(j < m)//表面上看其实可以换成从j从1到m的for循环的,但实际上你要是手推一波你就会发现这个循环不止进行了m-1次,这就是神奇所在
    {
        if(k == -1 || ss[j] == ss[k])//这个ss[j] 其实就是到最后一个元素,而ss[k] 其实就是第k个元素,看看这两个想不想等
            next1[++j] = ++k;
        else
            k = next1[k];
        /*这个就是用来清值的,因为每次求第j个的时候,如果不匹配,就得清值,有可能求一个next1的值时不止用了一次这个玩意,很神奇*/
    }
}
           
KMP算法KMP算法前言1.KMP算法概述2.next数组3.KMP实现来自作者最真诚的吐槽

这个是我手推的一遍循环过程(不推不知道,一推才知道其中的奥秘)

3.KMP实现

这个地方就要看如何灵活运用next数组的数据进行跳跃了……

(你可以当做板子背下来)

void kmp()
{
    int ans = 0;//用来记录出现几次
    int i = 0;//i是用来记录被搜字符串的位置
    int j = 0;//j是用来记录要搜的字符串的位置
    getnext();
    for(int i = 0; i < n; i++)
    {
        while(j > 0 && s[i] != ss[j])//就在这一步,实现了跳跃!,因为j = next[j],从而在i的地方进行s[i]与ss[j]的新的比较
            j = next1[j];
        if(s[i] == ss[j])//主要是用来找第一次匹配的位置
            j++;
        if(j == m)//当找到一次以后就输出这个位置
        {
            ans ++;//找到一次就直接++,计算次数用
            cout<<i - m + 2 <<'\n';//这个地方是输出位置的,不知道的话就直接背过就完事了了,咱也不知道为什么要加2,一遍遍试出来的……
            j = next1[j];//再刷新j的值,好找下一个,因为最后一个数的next值肯定为0,所以赋它
        }
    }
}
           

下面是一到洛谷的KMP模板题

题意大概是这样的:给你两个字符串s1,s2.

让你按从小到大的顺序输出s2在s1中的位置

在输出一行整数,第i个整数代表s2的长度为i的前缀的最长长度(其实就是让你打印next数组的值)

下面是AC码

#include<bits/stdc++.h>
using namespace std;

int next1[1000005];
string s, ss;
int n, m;

void getnext()
{
    int j, k;
    j = 0;
    k = -1;
    next1[0] = -1;
    while(j < m)
    {
        if(k == -1 || ss[j] == ss[k])
            next1[++j] = ++k;
        else
            k = next1[k];
    }
}
void kmp()
{
    int i = 0;
    int j = 0;
    getnext();
    for(int i = 0; i < n; i++)
    {
        while(j > 0 && s[i] != ss[j])
            j = next1[j];
        if(s[i] == ss[j])
            j++;
        if(j == m)
        {
            cout<<i - m + 2 <<'\n';
            j = next1[j];
        }
    }
}

int main()
{
    cin>>s>>ss;
    n = s.size();
    m = ss. size();
    kmp();
    for(int i = 1; i <= m; i++)
    {
        cout<<next1[i]<<' ';
    }
    return 0;
}

           

代码的解释都在上面写了

KMP的简单应用:

J - 剪花布条

题意:

给了两个字符串a,b。让你输出a中有几个完整的b

思路:

可以用KMP算法统计,也可以直接用find函数找,然后找到了就可以把这之前的所有字符删除

#include <bits/stdc++.h>
using namespace std;
int next1[1000005];
string s, ss;
int n, m, ans ;
void getnext()
{
    int j, k;
    j = 0;
    k = -1;
    next1[0] = -1;
    while(j < m)
    {
        if(k == -1 || ss[j] == ss[k])
            next1[++j] = ++k;
        else
            k = next1[k];
    }
}
int kmp()
{
    ans = 0;//用来记录出现几次
    int i = 0;//i是用来记录被搜字符串的位置
    int j = 0;//j是用来记录要搜的字符串的位置
    getnext();
    while(i <= n && j <= m)
    {
        if(j == m)//找到了一次s2,就让ans++
        {
            ans ++;
            j = 0;
            continue;
        }
        if(j == -1 || s[i] == ss[j])//i与j处字符匹配成功
        {
            i++;
            j++;
        }
        else//匹配不成功就利用next数组来跳跃
            j = next1[j];
    }
    return ans;
}
int main()
{
    while(cin>>s)
    {
        if(s[0] == '#')
            break;
        else
        {
            cin>>ss;
            n = s.size();
            m = ss. size();
            cout<<kmp()<<endl;
        }
    }
    return 0;
}

           

来自作者最真诚的吐槽

之前也学了一遍KMP,但是没静下心来去理解,就当做板子直接套(也不知道为什么,我明明记得我在oj上面做了一个KMP的题,还wa了,清清楚楚的,但是现在就是找不到,难道我活在梦里??!!),现在真正理解了,才知道他有多神奇多牛。还有那个洛谷的题,千万记得写函数用int时,得加返回值,不然就会RE,淦!(你说这直接用void他不香嘛)我差点到死都不知道哪里错了,还好有ycgg告诉我了。

昨天下午学了个markdown,晚上又学KMP,从六点在集训室,到回宿舍继续干,一直干到了零点多,今天上午可算是在图书馆用markdown语言写出来了这篇KMP算法的博文,属实不容易……

不得不说,typora真滴好用,太喜欢这种简约的风格了,吹爆typora……

(好奇怪啊,我导入到csdn的时候,外部的照片就会丢失,果然得去设置一下typora的图片的存储……)

继续阅读