这篇小结主要是参考这篇帖子从头到尾彻底理解KMP,不得不佩服原作者,写的真是太详尽了,让博主产生了一种读学术论文的错觉。后来发现原作者是写书的,不由得更加敬佩了。博主不才,尝试着简化一些原帖子的内容,希望能更通俗易懂一些。博主的帖子一贯秉持通俗易懂的风格,使得非CS专业的人士也能读懂,至少博主自己是这么认为的-.-|||

KMP算法,全称Knuth-Morris-Pratt算法,根据三个作者Donald Knuth、Vaughan Pratt、James H. Morris的姓氏的首字母拼接而成的。是一种字符串匹配的算法,用于在一个文本串S中查找模式串P的位置。在讲解KMP算法之前,我们先来看暴力破解法是如何运作的,假如我们有一个文本串S和一个模式串P如下:

文本串: BBC_ABCDAB_ABCDABCDABDE

模式串: ABCDABD

那么我们首先来找模式串的第一个字母A在文本串出现的位置:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

找到后,再来一一比较后面的字母,比较到模式串的D的位置,发现不匹配:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

暴力破解的下一步是将模式串后移一步,继续来匹配开头的A

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

直到找到下一个A,然后开始往后一一比较:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

后面的步骤就不一一列举了,都是按这种方法来查找的,这种算法十分的不高效,时间复杂度是O(m*n),其中m和n分别是文本串和模式串的长度。当m和n都很大的时候,运算速度就会很慢,那么此时就有请KMP算法闪亮登场!!

我们再回到暴力破解方法中的一一比较后面的字母那一步,比较到模式串的D的位置,发现不匹配:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

此时KMP算法并不是将模式串向右移动一位,而是向后移动四位,直接到这一步:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

这样文本串的遍历位置并不会移回去,而是'_'直接跟'C'匹配,是不是很神奇,它怎么知道要跟模式串上的哪个字符相比呢,实际上是从next数组中查的值,再讲解next数组之前,我们先来讲一下最大前缀后缀公共元素。

所谓最大前缀后缀公共元素,就是模式串中最大且相等的前缀和后缀,比如aba,有长度为1的相同前缀后缀a,再比如,字符串acdac有长度为2的相同前缀后缀ac,那么我们可以写出ABCDABD的每一位上的前缀后缀长度:

A   B   C   D   A   B   D
                  

由于模式串的尾部可能有重复的字符,所以我们可以得出一个重要的结论:失配时,模式串向右移动的距离 = 已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

我们之前是在字符'D'处失配的,上一位字符是'B',对应的最大长度是2,此时已经成功匹配了6个字符,那么我们就将模式串向右移动6-2=4位,并继续匹配即可。

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

此时我们发现'_'和'C'不匹配,那么'C'的上一个字符'B'的最大长度为0,此时已经匹配了2个字符,所以模式串向右移动2-0=2位继续匹配,得到:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

此时发现'_'和'A'不匹配,'A'已经是第一个了,不需要查表了,此时将模式串向右移动一位:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

发现此时模式串的首字母'A'匹配上了,然后就按顺序一路往下匹配,直到最后一个'D'和'C'失配:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

我们进行和之前相似的操作,上一位字符是'B',对应的最大长度是2,此时已经成功匹配了6个字符,那么我们就将模式串向右移动6-2=4位,并继续匹配即可:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

移动后发现模式串的首字母'A'匹配上了,然后就按顺序一路往下匹配,最终完成模式串的匹配:

BBC_ABCDAB_ABCDABCDABDE
ABCDABD

我们发现文本串中的遍历位置始终没有退后,一直都是在向前的,这样使得其比暴力破解法节省了大量的时间,其时间复杂度为O(m+n),简直碉堡了。读到这里是不是有疑问,怎么算法都结束了,还没next数组什么事呢,其实next数组和这里的最大前缀后缀公共元素长度数组是有关联的,上面的方法在失配时,要找失配字符前一个字符的最大前缀后缀公共元素长度值,那么如果我们将最大前缀后缀公共元素长度数组整体右移一位,形成next数组,如下所示:

A   B   C   D   A   B   D

-                 

上面的中间那行是之前的最大前缀后缀公共元素长度数组,我们将其整体右移一位,多出的位置补上一个-1,就变成了下面的一行。那么我们此时就直接找失配字符的next值就行了。于是我们就得到了新的结论:失配时,模式串向右移动的距离 = 失配字符所在位置 - 失配字符对应的next值。

读到这里是不是对KMP算法的发明者佩服的五体投地,别着急,还剩最后一部分,就是用代码来递推计算next数组。对于next的数组的计算,可以采用递推来算。根据上面的分析,我们知道如果模式串当前位置j之前有k个相同的前缀后缀,那么可以表示为next[j] = k,所以如果当模式串的p[j]跟文本串失配后,我们可以用next[j]处的字符继续和文本串匹配,相当于模式串向右移动了j - next[j]位。那么问题就来了,如何求出next[j+1]的值呢,我们还是来看例子吧:

模式串:    A  B  C  D  A  B  C  E
next值: - ?
索引: k j

如上所示,模式串为"ABCDABCE",且j=6, k = 2,我们有next[j] = k,这表示j位置上的字符C之前的最大前后缀长度为2,即AB。现在我们要求next[j+1]的值,因为p[k] == p[j],所以next[j+1] = next[j] + 1 = k + 1 = 3。即字母E之前的最大前后缀长度为3,即ABC。

那么我们再来看p[k] != p[j]的情况下怎么处理,还是来看例子:

模式串:    A  B  C  D  A  B  D  E
next值: - ?
索引: k j

这个例子把上面例子中的第二个'C'换成了'D',所以字符'E'前面的相同后缀就不再是3了,所以我们希望在k前面找出个k'位置,使得p[k']为D,这样next[j+1] = k' +1,但是这个例子中不存在这样的'D',所以next[j+1] = 0。我们看一个能在前缀中找到'D'的例子:

模式串:    D  A  B  C  D  A  B  D  E
next值: - ?
索引: k j

这个例子上面例子的最前面加上了个'D',此时j = 7, k = 3了,我们有next[j] = k,这表示j位置上的字符3之前的最大前后缀长度为3,即DAB。要求next[j+1]的值,我们发现此时p[k] != p[j],然后我们让k = next[k] = 0,此时p[0]是D,那么next[j+1] = k + 1 = 1了,这说明字母E之前的最大前后缀长度为1,即D。综上所述,我们可以写出next的生成函数如下:

vector<int> getNext(string p) {
int n = p.size(), k = -, j = ;
vector<int> next(n, -);
while (j < n - ) {
if (k == - || p[j] == p[k]) {
++k; ++j;
next[j] = k;
} else {
k = next[k];
}
}
return next;
}

上面这种计算next数组的方式可以进一步的优化,可以优化的原因是因为上面的方法存在一个小小的问题,如果用这种方法求模式串ABAB,会得到next数组为[-1 0 0 1],我们用这个模式串去匹配ABACABABC:

ABACABABC
ABAB

我们会发现C和B失配,那么根据上面的规则,我们要向右移动j - next[j] = 3 - 1 = 2位,于是有:

ABACABABC
ABAB

我们右移两位后发现又是C和B失配了,而我们在上一步中,已知p[3] = B, s[3] = C,就已经失配了,让p[next[3]] = p[1] = B再去和s[3]比较,肯定还是失配。原因是当p[j] != s[i]时,下一步要用p[next[j]]和s[i]去匹配,而如果p[j] == p[next[j]]了,再用p[next[j]]和s[i]去匹配必然会失配。所以我们要避免出现p[j] == p[next[j]]的情况,一旦出现了这种情况,我们可以再次递归,next[j] = next[next[j]],修改后的代码如下:

vector<int> getNext(string p) {
int n = p.size(), k = -, j = ;
vector<int> next(n, -);
while (j < n - ) {
if (k == - || p[j] == p[k]) {
++k; ++j;
next[j] = (p[j] != p[k]) ? k : next[k];
} else {
k = next[k];
}
}
return next;
}

讲到这里,KMP算法的内容就完全讲完了,原帖子中还有两个扩展方法,这里就不讲了,感觉能把上述内容吃透就很不容易了,下面贴上完整的KMP的代码仅供参考:

#include <iostream>
#include <vector> using namespace std; vector<int> getNext(string p) {
int n = p.size(), k = -, j = ;
vector<int> next(n, -);
while (j < n - ) {
if (k == - || p[j] == p[k]) {
++k; ++j;
next[j] = (p[j] != p[k]) ? k : next[k];
} else {
k = next[k];
}
}
return next;
} int kmp(string s, string p) {
int m = s.size(), n = p.size(), i = , j = ;
vector<int> next = getNext(p);
while (i < m && j < n) {
if (j == - || s[i] == p[j]) {
++i; ++j;
} else {
j = next[j];
}
}
return (j == n) ? i - j : -;
} int main() {
cout << kmp("BBC_ABCDAB_ABCDABCDABDE", "ABCDABD") << endl; // Output: 15
}

参考资料:

http://blog.csdn.net/v_july_v/article/details/7041827

转载请注明出处:来自Grandyang的博客园:http://www.cnblogs.com/grandyang/p/6992403.html

KMP Algorithm 字符串匹配算法KMP小结的更多相关文章

  1. [Algorithm] 字符串匹配算法——KMP算法

    1 字符串匹配 字符串匹配是计算机的基本任务之一. 字符串匹配是什么?举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串& ...

  2. 字符串匹配算法 - KMP

    前几日在微博上看到一则微博是说面试的时候让面试者写一个很简单的字符串匹配都写不出来,于是我就自己去试了一把.结果写出来的是一个最简单粗暴的算法.这里重新学习了一下几个经典的字符串匹配算法,写篇文章以巩 ...

  3. 【原创】通俗易懂的讲解KMP算法(字符串匹配算法)及代码实现

    一.本文简介 本文的目的是简单明了的讲解KMP算法的思想及实现过程. 网上的文章的确有些杂乱,有的过浅,有的太深,希望本文对初学者是非常友好的. 其实KMP算法有一些改良版,这些是在理解KMP核心思想 ...

  4. 字符串匹配算法——KMP算法学习

    KMP算法是用来解决字符串的匹配问题的,即在字符串S中寻找字符串P.形式定义:假设存在长度为n的字符数组S[0...n-1],长度为m的字符数组P[0...m-1],是否存在i,使得SiSi+1... ...

  5. 4种字符串匹配算法:KMP(下)

    回顾:4种字符串匹配算法:BS朴素 Rabin-karp(上) 4种字符串匹配算法:有限自动机(中) 1.图解 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R ...

  6. 字符串匹配算法KMP算法

    数据结构中讲到关于字符串匹配算法时,提到朴素匹配算法,和KMP匹配算法. 朴素匹配算法就是简单的一个一个匹配字符,如果遇到不匹配字符那么就在源字符串中迭代下一个位置一个一个的匹配,这样计算起来会有很多 ...

  7. 字符串匹配算法--KMP字符串搜索(Knuth–Morris–Pratt string-searching)C语言实现与讲解

    一.前言   在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S内查找一个词W的出现位置.此算法通过运用对这个词在不匹配时本身就包含足够的信息 ...

  8. 字符串匹配算法——KMP算法

    处理字符串的过程中,难免会遇到字符匹配的问题.常用的字符匹配方法 1. 朴素模式匹配算法(Brute-Force算法) 求子串位置的定位函数Index( S, T, pos). 模式匹配:子串的定位操 ...

  9. 字符串匹配算法——KMP、BM、Sunday

    KMP算法 KMP算法主要包括两个过程,一个是针对子串生成相应的“索引表”,用来保存部分匹配值,第二个步骤是子串匹配. 部分匹配值是指字符串的“前缀”和“后缀”的最长的共有元素的长度.以“ABCDAB ...

随机推荐

  1. Java Swing实现一个简单而优美的记事本( 较详细注释 )

    Java Swing实现具有基本功能的记事本 目前实现了: 文件 新建 打开 保存 退出前保存询问 编辑 剪切 复制 粘贴 清除 撤销 格式 字体选择 字体颜色选择 帮助 关于 (样式采用了css与h ...

  2. Android App性能测试之二:CPU、流量

    CPU---监控值的获取方法.脚本实现和数据分析 1.获取CPU状态数据 adb shell dumpsys cpuinfo | findstr packagename 自动化测试脚本见cpustat ...

  3. springMVC入门思路整理

  4. 【洛谷P1303A*Bprublem】

    题目描述 求两数的积. 输入输出格式 输入格式: 两行,两个数. 输出格式: 积 输入输出样例 输入样例#1: 1 2 输出样例#1: 2 说明 每个数字不超过10^2000,需用高精 这道题还是比较 ...

  5. 让你爱不释手的 Python 模块

     一. logzero 在一个完整的信息系统里面,日志系统是一个非常重要的功能组成部分.它可以记录下系统所产生的所有行为.我们可以使用日志系统所记录的信息为系统进行排错,优化系统的性能,或者根据这些 ...

  6. centos 6 部署Nodejs

    线上环境需要一套nodjs,没话说,那就部署唠. 一.下载编译包.解压.软链 nodjs历史版本连接:https://nodejs.org/zh-cn/download/releases/ cd /u ...

  7. 关于button去掉自带阴影效果的方法

    在button的属性设置里加上: style=”?android:attr/borderlessButtonStyle” 即: <Button android:layout_width=&quo ...

  8. ubuntu 32/64 bit

    https://askubuntu.com/questions/454253/how-to-run-32-bit-app-in-ubuntu-64-bit how to run 32-bit app ...

  9. GIt -- Window下配置 git

    全局配置  git config --global user.name "账户名"  git config --global use r.email '账户邮箱' 生成ssh,命令 ...

  10. 【原创】Java基础之Session机制

    Session机制 JSESSIONID是Session的标识,当客户端请求服务器端的时候,服务器端会检查是否已经给这个客户端创建过Session,也就是看客户端的请求中的header是否有Cooki ...