KMP算法讲义

Posted 明璐花生牛奶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMP算法讲义相关的知识,希望对你有一定的参考价值。

首先给你一个这样的问题,你会如何处理?

直接了当使用暴力

我们处理问题的一般方式就是先暴力求解出来,根据暴力的每一个步骤我们进行优化从而创造出一个占用空间小,耗费时间短的“最佳”算法。(没错!就是先把问题揍一遍,看看哪里轻哪里重,然后再对症下药!)

以长的字符串 S = “ababababfab” ,短的字符串 P = "ababf"为例。
暴力求解的过程是:

首先用S[0]开头的子串:ababa 与 P比较,不匹配。

接着用S[1]开头的子串:babab 与 P比较,不匹配。

接着用S[2]开头的子串:ababa 与 P比较,不匹配。

接着用S[3]开头的子串:babab 与 P比较,不匹配。

接着用S[4]开头的子串:ababf 与 P比较,匹配。输出4
以下为求解动态图:

然后我们的代码大致就可以这样去写:

int ViolentMatch(string& s, string& p)//s为长字符串,p为短字符串。

    for (int i = 0; i < m;)
    
        int start = i, j = 0;//star 记录此次s中的开始位置
        while (i < m && j < n && s[i++] == p[j++]);//一次比较,直到不相等,注意句尾带分号
        if (j == n)//如果p串比较完了,输出开始位置
        
            cout << start;
            return 0;
        
        i = start + 1;//i更新到本次起始位置的下一个位置
    
    cout << -1;

嗯!似乎可以解决字符串匹配的问题,并且这还是一个很不错的算法;
时间复杂度达到了O(nm)(n表示原字符串的长度,m表示子串长度)很明显如果m的数量级和n一样,那么这个问题就变成了一个二阶的问题。
但是我们再仔细去想,一般计算机的1s操作次数也就是109级别。如果这个n达到了106,并且我们此时要查找的字符串长度为104。很显然,这样的计算让计算机在暴力求解1s内很难完成。其实这样说大家肯定更容易明白:我们在一个百万字的书里去查找一段10000字内容,以目前大家所遇到的情况,是不是查找在很短的时间内就完成了?甚至于不会超过1s。
所以暴力求解肯定不是目前最优的字符串匹配算法,需要对此进行优化!
这个算法的优化问题在上个世纪已经被一个叫KMP的组合将这个二阶问题优化成了一个线性的问题。

问题优化

首先按照暴力的思想,先从S[0]开始的子串:“ababa” 与 P = “ababf” 比较,比较到 S[4] 与 P[4] 的时候,S[4] != P[4]。

按照暴力的思路,接下来是用S[1]开头的子串:P = “babab” 与 P = “ababf” 比较。
但是这时候有一位上帝上帝告诉我们S[1~ 3] 与 P[0~2] 不匹配,那以S[1] 为开头的字符串肯定与 P 不匹配。我们就无需进行以S[1]开头子串与 P 的比较了。

我们继续按照暴力的思路,上帝告诉我们S[1]没有可以比较的必要,我们就跳过S[1],来到S[2].这时候,上帝又告诉我们,S[2~ 3] 与 P[0~1] 匹配,那以S[2] 为开头的字符串与 P 匹配时,无需从S[2]开始,直接从S[4] 与 P[2] 开始比较即可。比较到S[7] 与 P[4] 的时候,S[7] != P[4]。


按照暴力的思路,接下来用 S[3] 开头的子串:P = “babab” 与 P = “ababf” 比较。这时候,上帝又告诉我们,S[3~ 5] 不等于 P[0~2],那以S[3] 为开头的字符串肯定与 P 不匹配。我们就无需进行以S[3]开头子串与 P 的比较了。

按照暴力的思路,接下来用 S[4] 开头的子串:P = “ababf” 与 P = “ababf” 比较。这时候,上帝又告诉我们,S[4~ 5] 与 P[0~2] 相同,那以S[4] 为开头的字符串与 P 匹配时,无需从S[4]开始,直接从S[6] 与 P[2] 开始比较即可。比较到S[8] 与 P[5] 的时候,S[8] = P[5],字符串匹配完成,返回起始位置4。


上帝的作用是:

当匹配过程中,出现了 S[i ] != P[j],上帝会告诉我们:S[i-j+1 ~ i-1] 与 P[0 ~ j-2] 是否匹配,S[i-j+2 ~ i-1] 与 P[0 ~ j-3] 是否匹配······。也就是下图中 S 蓝色框里的子串与 P 蓝色框里的子串是否匹配,S 绿色框里的子串与 P 绿色框里的子串是否匹配,S 红色框里的子串与 P红色框里的子串是否匹配。

其实上帝只需要告诉我们一个值 k,k 是能满足下面性质的最大值:
长字符串从 i-k 到第 i-1 的字符 S[i-k ~ i-1] 与短字符串前k个字符 P[0 ~ k-1] 相同。也就是说对于一个大于0的数x,对于S[i-k-x ~ i - 1]和P[0 ~ k-1+x]都是不匹配的。所以S[i-k-x] 为开头的子串与 P 肯定不匹配。下次匹配,i 无需回溯到 i-k-x, 只需回溯到 i-k,j 回溯到 0。因为 P[0 ~ k-1]S[i-k ~ i-1] 相同,因此这一段无需比较。故 i 直接可以移动到回溯之前的位置(其实就是保持原来的位置不动),j 直接可以移动到 k 。

这样i就不需要后移,只需要一直向前进即可,j只需要回溯到特定的位置,然后与i进行比较。这样我们的时间复杂度就降到了O(n+m)级别

不太懂?看下面的具体解释:

第一次出现字符不匹配的时候为:S[4] != P[4]。保证 P[0 ~ k-1] = S[ 4-k ~3] 的 k 的最大值为 2。如下图:

因为 k 的最大值为 2, 所以 P[0 ~ 2+1-1] != S[2-1 ~ 3], 即 P[0 ~ 2] != S[1 ~ 3]。因此以 S[1] 为开头的子串与 P 肯定不相同,无需进行后续比较。如下图:

因为 P[0 ~ 1] = S[2 ~3],所以以 S[2] 为开头的子串是否与 P 相同,只需要从 P[2] 与 S[4] 开始比较即可。i 之前的位置为 4,现在还是 4,相当于 i 没有回溯。j 之前的位置是 4 ,现在是 2,也没有回溯到 0。如下图:

好,现在知道了上帝的作用,如何寻找上帝?

给出一个最大的 k 值,使得短字符串 P 的前 k 个字符 P[0 ~ k-1] 与长字符串 S 的从 i-k 到第 i-1 个字符 S[i-k ~ i-1] 相同(如果k 为0,表示没有相同部分,下同)。又因为 i 前面的字符已经匹配过了,所以 S[0 ~ i-1]P[0 ~ j-1] 相同。等价于给出了一个 0 ~ j-1中的最大的 k,使得 P[0 ~ k-1]P[j-k ~ j-1] 相同。如下图:

这里我们就引入了KMP中的上帝------next[ ]数组。next[j]含义是:P[j] 前面的字符串的最长公共前后缀长度。有点绕口,看下面例子:

P=“ababf” 的最长公共前后缀:

  • P[0] 前面没有字符串,所以最长公共前后缀长度为 0。
  • P[1] 前面的字符串为:a,a没有前后缀(前后缀长度要小于字符串长度)。最长公共前后缀长度为 0。
  • P[2] 前面的字符串为:ab,它的前缀为:a,后缀为b。前缀不等于后缀,所以没有公共前后缀,最长公共前后缀长度为 0。
  • P[3] 前面的字符串为:aba,aba 的前缀有:a,ab, 后缀有:a,ba。因为 ab 不等于 ba,所以最长公共前后缀为 a,最长公共前后缀长度为 1。
  • P[4] 前面的字符串为:abab,abab 的前缀有:a,ab,aba,后缀有:a,ab, bab。最长公共前后缀为 ab,长度为 2。

    求最长公共前后缀长度的时候,可以用暴力的求法,但效率很低。我们可以用另一种方法来求:

假设已经知道 P[i] 之前的各个字符的最长公共前后缀长度 next[0 ~ i-1],看看能不能通过 next[0 ~ i-1] 求出 next[i]。

以 P = “ababdababaa” 为例,已知next[8] = 3,求next[9] :

next[8] 等于黄色部分长度:next[8] = 3。含义:P[8] 的最长公共前后缀长度为3,即:P[0 ~ 2] 与 P[5 ~ 7] 相同。

上图这种情况,next[8] = 3, 即 P[0 ~ 2] 与 P[5 ~ 7] 相同,又因为 P[ next[i-1] ] ( next[i - 1] = 3 ) 与 P[i - 1] (i - 1 = 8) 是相同字母,所以P[0 ~ 3]P[5 ~ 8] 相同。所以next[9] = next[8] + 1 = 4。可以得出结论:当P[ next[i-1] ] 与 P[i - 1] 是同一个字母的时候,P[i] = P[i-1] + 1。

next[i-1]就意味着P[0 ~ next[i-1] - 1] 与P[i - 1 - next[i - 1] ~ i - 2]是匹配上的。如果此时在P[i - 1 - next[i - 1] ~ i - 2] 往后看一位(P[i -1])

那么前面与其匹配的P[0 ~ next[i-1] - 1] 也要往后看一位 P[next[i - 1]]。如果P[i -1] == P[next[i - 1]] 那么P[i] = P[next[i -1]] + 1;

那么如果P[i -1] != P[next[i - 1]]该如何处理呢?

先看下 next[10] 怎么求:因为 P[9] 与 P[4] 不同,所以无法从 next[9] 推导出 next[10]。根据 next[9] = 4 的含义,可以知道,P[0 ~ 3] (“abab”) 与 P[5 ~ 8] (“abab”) 相同。


看一下next[next[9]] = next[4] = 2 : P[0 ~ 1] 与P[2 ~ 3]相同,又因为P[2 ~ 3] == P[7 ~ 8]。P[2] 和P [9]相同,也就是所说P[0 ~ 2] 和 P[7 ~ 9],也就是说next[10] = next[4] + 1 = 3;

所以当 P[ next[i - 1] ]P[i - 1] 不同的时候,可以这样求 next[i]:

令 j 等于 next[i - 1],如果 P[j] 与 P[i -1] 不同,一直通过 j = next[j] 更新 j 的值,直到遇到 P[j] 与 P[i - 1] 相同,P[i] 就等于 j + 1。

如果 j 的值更新到了 0 ,就不用继续更新了,这时候:如果 P[0] 与 P[i - 1] 相同, next[i] 就等于 1;如果 P[0] 与 P[i - 1] 不同,next[i] 就等于 0。

这里写一个易理解的版本,方便理解。

#include <iostream>
#include <cstring>

using namespace std;

const int N = 1e6 + 10;
string p, s;
int ne[N];
int main()

    cin >> s >> p;
    ne[0] = ne[1] = 0;
    for(int i = 2; i < p.size() + 1; i ++)
    
        int j = ne[i - 1];
        while(j && p[i - 1] != p[j])// p[i] 与 P[j] 不同且 j 不为 0,就一直更新 j 为next[j]。
        	j = ne[j];
        if(j)//如果 j 不为0,就是 p[i - 1] == p[j] 成立,导致 while 循环结束
        	ne[i] = j + 1;
        else
        
            if(p[j] == p[i - 1])
                ne[i] = 1;
            else
                ne[i] = 0;
        
    
    
    for(int i = 0, j = 0; i < s.size(); i++)
    
        while(j && s[i] != s[j])
            j = ne[j];
        if(s[i] == s[j]) j++;
        if(j = p.size())
        
            cout << i - j + 1 << endl;
            return 0;
        
    
    return 0;

kmp算法啥意思?

KMP算法之所以叫做KMP算法是因为这个算法是由三个人共同提出来的,就取三个人名字的首字母作为该算法的名字。其实KMP算法与BF算法的区别就在于KMP算法巧妙的消除了指针i的回溯问题,只需确定下次匹配j的位置即可,使得问题的复杂度由O(mn)下降到O(m+n)。
  在KMP算法中,为了确定在匹配不成功时,下次匹配时j的位置,引入了next[]数组,next[j]的值表示P[0...j-1]中最长后缀的长度等于相同字符序列的前缀。
  对于next[]数组的定义如下:
 1) next[j] = -1 j = 0
 2) next[j] = max(k): 0<k<j P[0...k-1]=P[j-k,j-1]
 3) next[j] = 0 其他
 如:
 P a b a b a
 j 0 1 2 3 4
next -1 0 0 1 2
 即next[j]=k>0时,表示P[0...k-1]=P[j-k,j-1]
 因此KMP算法的思想就是:在匹配过程称,若发生不匹配的情况,如果next[j]>=0,则目标串的指针i不变,将模式串的指针j移动到next[j]的位置继续进行匹配;若next[j]=-1,则将i右移1位,并将j置0,继续进行比较。追问

next(1)不是等于0吗?

参考技术A 一种改进的字符串匹配算法,由D.E.Knuth与V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。
完全掌握KMP算法思想
学过数据结构的人,都对KMP算法印象颇深。尤其是新手,更是难以理解其涵义,搞得一头雾水。今天我们就来面对它,不将它彻底搞懂,誓不罢休。
如今,大伙基本上都用严蔚敏老师的书,那我就以此来讲解KMP算法。(小弟正在备战考研,为了节省时间,很多课本上的话我都在此省略了,以后一定补上。)
严老的《数据结构》79页讲了基本的匹配方法,这是基础。先把这个搞懂了。
80页在讲KMP算法的开始先举了个例子,让我们对KMP的基本思想有了最初的认识。目的在于指出“由此,在整个匹配的过程中,i指针没有回溯,”。本回答被提问者和网友采纳
参考技术B 很高兴为您解答!
kmp算法是一种改进的字符串匹配算法。
虽然简短但是希望能帮到您!
参考技术C 先去理解AC自动机吧,KMP算法大部分是在模拟AC自动机.KMP算法涉及不少性质与证明,都需要理解.
算法导论上有,我曾经也是非常难理解,即便理解了,写出来的代码还是会出错,说明没理解透,没理解本质.

以上是关于KMP算法讲义的主要内容,如果未能解决你的问题,请参考以下文章

数据结构(C语言版)严蔚敏(字符串的模式匹配算法--KMP算法)

数据结构(C语言版)严蔚敏(字符串的模式匹配算法--KMP算法)

KMP算法详解——多图,多例子(c语言)

c_cpp 串的匹配算法和KMP算法

KMP模式匹配算法简单概述(c语言实现)

C Language 串 - BF算法&&KMP算法