KMP Algorithm 字符串匹配算法KMP小结
这篇小结主要是参考这篇帖子从头到尾彻底理解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小结的更多相关文章
- [Algorithm] 字符串匹配算法——KMP算法
1 字符串匹配 字符串匹配是计算机的基本任务之一. 字符串匹配是什么?举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串& ...
- 字符串匹配算法 - KMP
前几日在微博上看到一则微博是说面试的时候让面试者写一个很简单的字符串匹配都写不出来,于是我就自己去试了一把.结果写出来的是一个最简单粗暴的算法.这里重新学习了一下几个经典的字符串匹配算法,写篇文章以巩 ...
- 【原创】通俗易懂的讲解KMP算法(字符串匹配算法)及代码实现
一.本文简介 本文的目的是简单明了的讲解KMP算法的思想及实现过程. 网上的文章的确有些杂乱,有的过浅,有的太深,希望本文对初学者是非常友好的. 其实KMP算法有一些改良版,这些是在理解KMP核心思想 ...
- 字符串匹配算法——KMP算法学习
KMP算法是用来解决字符串的匹配问题的,即在字符串S中寻找字符串P.形式定义:假设存在长度为n的字符数组S[0...n-1],长度为m的字符数组P[0...m-1],是否存在i,使得SiSi+1... ...
- 4种字符串匹配算法:KMP(下)
回顾:4种字符串匹配算法:BS朴素 Rabin-karp(上) 4种字符串匹配算法:有限自动机(中) 1.图解 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R ...
- 字符串匹配算法KMP算法
数据结构中讲到关于字符串匹配算法时,提到朴素匹配算法,和KMP匹配算法. 朴素匹配算法就是简单的一个一个匹配字符,如果遇到不匹配字符那么就在源字符串中迭代下一个位置一个一个的匹配,这样计算起来会有很多 ...
- 字符串匹配算法--KMP字符串搜索(Knuth–Morris–Pratt string-searching)C语言实现与讲解
一.前言 在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S内查找一个词W的出现位置.此算法通过运用对这个词在不匹配时本身就包含足够的信息 ...
- 字符串匹配算法——KMP算法
处理字符串的过程中,难免会遇到字符匹配的问题.常用的字符匹配方法 1. 朴素模式匹配算法(Brute-Force算法) 求子串位置的定位函数Index( S, T, pos). 模式匹配:子串的定位操 ...
- 字符串匹配算法——KMP、BM、Sunday
KMP算法 KMP算法主要包括两个过程,一个是针对子串生成相应的“索引表”,用来保存部分匹配值,第二个步骤是子串匹配. 部分匹配值是指字符串的“前缀”和“后缀”的最长的共有元素的长度.以“ABCDAB ...
随机推荐
- Beamer 目录分栏
导言区加入 \usepackage{multicol} 然后 \section*{目录} \frame{\begin{multicols}{} \tableofcontents[hideallsubs ...
- [物理学与PDEs]第5章习题5 超弹性材料中客观性假设的贮能函数表达
设超弹性材料的贮能函数 $\hat W$ 满足 (4. 19) 式, 证明由它决定的 Cauchy 应力张量 ${\bf T}$ 满足各向同性假设 (4. 7) 式. 证明: 若贮能函数 $W$ 满足 ...
- $A,B$ 实对称 $\ra\tr((AB)^2)\leq \tr(A^2B^2)$
设 $A,B$ 是 $n$ 阶实对称矩阵. 试证: $\tr((AB)^2)\leq \tr(A^2B^2)$. 又问: 等号何时成立? 证明: 由 $$\bex \sum_i \sez{\su ...
- SpringBoot使用Redis共享用户session信息
SpringBoot引入Redis依赖: <dependency> <groupId>org.springframework.boot</groupId> < ...
- 设计模式六: 模板方法(Template Method)
简介 模板方法属于行为型模式的一种. 实现层面上, 在抽象类中定义了算法或流程的骨架, 将其中易变的部分延迟到子类实现, 也就是允许它的子类实现其中的某些步骤. 模板方法适用于算法不变, 但算法中某些 ...
- Web从入门到放弃<6>
<1> Canvas. 1,灰度图: js: function showAsGray() { var imgNode = document.getElementById('img'); ...
- 墨水屏 E-Paper module【转】
转自:https://blog.csdn.net/smallmount123/article/details/77489196 https://www.digikey.com/product-deta ...
- 在右键菜单中加入BitLocker重新上锁功能
当使用BitLocker给磁盘上锁后,可以通过命令:manage-bde -lock d: -forcedismount 将已经解锁的磁盘重新上锁,如果觉得每次都通过命令行写命令很麻烦,那可以通过修改 ...
- Task.Run与Task.Factory.StartNew的区别
Task是可能有延迟的工作单元,目的是生成一个结果值,或产生想要的效果.任务和线程的区别是:任务代表需要执行的作业,而线程代表做这个作业的工作者. 在.Net 4中,Task.Factory.Star ...
- 【原创】大数据基础之Parquet(1)简介
http://parquet.apache.org 层次结构: file -> row groups -> column chunks -> pages(data/index/dic ...