详解KMP算法

算法介绍

字符串匹配是计算机的基本任务之一,而Knuth-Morris-Pratt算法(简称KMP)算法就是一种常见的处理字符串匹配问题的算法。

最基本最简单的字符串匹配(也就是一位一位将搜索词向后移,再从搜索词的第一位向后逐个比较)方法的时间复杂度(设字符串长度为n,搜索词长度为m)最差是O(n*m),而KMP算法的时间复杂度最差只有O(n)。

举个例子:有一个字符串”BDCC AABCDAB ABCDABCDABD”,我想知道,里面是否包含另一个字符串”ABCDABD”?

算法流程

1、

首先,将字符串”BDCC AABCDAB ABCDABCDABD”与搜索词“ABCDABD”的第一位进行比较,因为“B”与“A”不匹配所以将搜索词向后移一位

2、

继续按照上述步骤进行匹配

3、

就这样,一直找到一组匹配的字符

4、

继续匹配搜索词的第二位,发现无法匹配

先别急着将搜索词后移一位再从头逐个比较,这样有可能使效率变差,因为你要把搜索过的字符再搜索一遍

那么如何解决这个问题呢?

这时我需要针对搜索词算出一张《部分匹配表》(Partial Match Table)

至于这张《部分匹配表》(这张表也是KMP的关键部分)是怎样算出来的我们后面再说

已知最后一个匹配的字符是A,而A对应的部分匹配值是0,所以我们套用公式:

移动位数 = 已匹配的字符数 – 搜索词中最后一个匹配字符对应的部分匹配值

得出:  移动位数=1-0=1

也就是向后移一位

 5、

 我们将搜索词向后移动一位并继续匹配

发现第一位匹配成功,我们就逐个匹配搜索词的后几位

 6、

直到匹配到一位不同为止

继续套用之前的公式:移动位数 = 已匹配的字符数 – 搜索词中最后一个匹配字符对应的部分匹配值

得出: 移动位数=6-2=4

于是我们将搜索词向后移4位继续匹配(这样就省去了很多无用的时间)

7、

我们继续匹配,直到再一次匹配到一样的

8、

我们依旧逐个匹配搜索词的后几位,一直到不同为止

紧接着我们算出移动位数=2-0=2

我们将搜索词向后移2位

9、

继续仿制上述的方法进行求解

补充

我们现在来介绍一下《部分匹配表》(Partial Match Table)

首先我们明确两个概念:

  • 前缀:指的是字符串的子串中从原串最前面开始的子串,如abcdabd的前缀有:a,ab,abc,abcd,abcda,abcdab
  • 后缀:指的是字符串的子串中在原串结尾处结尾的子串,如abcdabd的后缀有:d,bd,abd,dabd,cdabd,bcdabd

那有人会问了,这个“前缀”和“后缀”又与这个《部分匹配表》有什么关系呢?

其实这个匹配表中的每一个“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度

比如:表中的第一个0表示“A”的”前缀”和”后缀”的最长的共有元素的长度

           表中的第二个0表示“AB”的”前缀”和”后缀”的最长的共有元素的长度

           表中的1表示“ABCDA”的”前缀”和”后缀”的最长的共有元素的长度

           表中的2表示“ABCDAB”的”前缀”和”后缀”的最长的共有元素的长度

接下来我们以“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。

代码

  

for(int i=2;i<=m;i++)
{                                  //b数组是搜索串
    while(j>0&&b[i]!=b[j+1])j=p[j];//好好体会这个while循环,失配的精髓。
    if(b[i]==b[j+1])j++;
    p[i]=j;
}

  

int kmp(char s,char p)
{
    int i=0;
    int j=0;
    int sLen=strlen(s);
    int pLen=strlen(p);
    while(i<sLen && j<pLen)
    {
        //①如果j=-1,或者当前字符匹配成功(即S[i]==P[j]),都令i++,j++
        if(j == -1 || s[i] == p[j])
        {
            i++;
            j++;
        }
        else
        {
            //②如果j!=-1,且当前字符匹配失败(即S[i]!=P[j]),则令 i 不变,j=next[j]
            //next[j]即为j所对应的next值
            j=next[j];
        }
    }
    if(j == pLen) return i-j;
    else return -1;
}

  

  

原文地址:https://www.cnblogs.com/chenjiaxuan/p/10989751.html