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
0   0   0   0   1   2   0

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

我们之前是在字符‘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
0   0   0   0   1   2   0
-1  0   0   0   0   1   2

上面的中间那行是之前的最大前缀后缀公共元素长度数组,我们将其整体右移一位,多出的位置补上一个-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值:   -1  0  0  0  0  1  2  ?
索引:             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值:   -1  0  0  0  0  1  2  ?
索引:             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值:   -1  0  0  0  0  1  2  3  ?
索引:                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 = -1, j = 0;
    vector<int> next(n, -1);
    while (j < n - 1) {
        if (k == -1 || 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 = -1, j = 0;
    vector<int> next(n, -1);
    while (j < n - 1) {
        if (k == -1 || 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 = -1, j = 0;
    vector<int> next(n, -1);
    while (j < n - 1) {
        if (k == -1 || 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 = 0, j = 0;
    vector<int> next = getNext(p);
    while (i < m && j < n) {
        if (j == - 1 || s[i] == p[j]) {
            ++i; ++j;
        } else {
            j = next[j];
        }
    }
    return (j == n) ? i - j : -1;
}

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

时间: 2024-10-27 13:08:46

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

字符串匹配算法KMP算法

数据结构中讲到关于字符串匹配算法时,提到朴素匹配算法,和KMP匹配算法. 朴素匹配算法就是简单的一个一个匹配字符,如果遇到不匹配字符那么就在源字符串中迭代下一个位置一个一个的匹配,这样计算起来会有很多多余的不符合的匹配做了冗余的比较.假设源字符串长n,字串长m 该算法最差时间复杂度为 m*(n-m+1),记为O(n*m);这里不做过多解释朴素匹配算法. KMP算法: kmp算法不是在源字符串中下手,他是从字串下手,比如我要在源字符串(acabaabaabcacaabc)中匹配一个字符串字串(ab

字符串匹配算法KMP详细解释——深入理解

1. 前言 字符串匹配是一个经典算法问题,展开来讲各类问题多达几十种,有名称的算法也不下三十种,所以需要深入学习的东西有很多.这次我们来探讨一个最简单的问题,假设现在随机输入一个长度为m的主串T,另外输入一个长度为n(n≤m)的字符串P,我们来判断字符串P是否是主串T的一个子串(即能否从T中随机取出与P同长的一段字符串,与P完全匹配). 2. 蛮力匹配法 问题很简单,当然也有最直接.最直观也是最好想到的方法,蛮力串匹配.即两个字符串像物流传送带一般,主串固定,子串一步步像前移动,一位位匹配比较,

字符串匹配算法-KMP

举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"? 在上面这个例子中,字符串"BBC ABCDAB ABCDABCDABDE"称为主串,字符串"ABCDABD"称为模式串 许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一.下面,我用自己的语言,解释KMP算法. 1.首先,主串"BBC ABCDA

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

1.字符串匹配 字符串匹配是计算机的基本任务之一. 字符串匹配是什么?举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"? 许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一.它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth(<计算机程序设计艺术>的作者). 2.KMP算法 这个算法不太容易理解,网上有很多解释,但

[小明学算法]6.字符串匹配算法---KMP

1.简介  字符串匹配就是看看那字符串b是不是字符串a的子串.常用的Knuth-Morris-Pratt 算法,又称KMP算法. 2.主要思想 当patter在某一位置与string匹配失败时,我们除了知道从string的这个位置进行匹配失败这个结果外,是否可以从前面的匹配中获得更多的信息呢.即当前匹配点匹配失败之后,向右滑动的距离是可以提前计算出来的. 3.举例 abcabcabcdef   --------- string abcabcdef         --------- patter

字符串匹配算法-kmp算法

一原理: 部分转自:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html 字符串匹配是计算机的基本任务之一. 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"? 许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一.它以三个发

字符串匹配算法KMP Java实现

看了一些的kmp实现,依葫芦画瓢,很死板,前缀什么的完全没必要. kmp算法的核心思想:先对搜索字串生成偏移对照表,匹配时从左向右依次比较(bm从右向左,号称比kmp更快),相等则文档和搜索字串的下标+1迭代,否则查表,定位最优的偏移位置(文档下标不变,搜索字串下标改变).例外是,字符不匹配时,若搜索字串的下标为0,则文档的下标+1,继续迭代比较. import java.util.Arrays; public class KMPSearch { public static int[] tabl

KMP字符串匹配算法及next前缀数组的应用

#KMP字符串匹配算法及next前缀数组的应用------ KMP算法通常是我们学习字符串匹配算法时遇见的第一个算法,另外还有Rabin-Karp, Sunday算法等. 相对于其他字符串匹配算法, kmp在字符串中字符重复率低的情况下并不具备优势,那为什么KMP算法会作为经典的教学算法呢? 原因可能是:KMP算法充分利用next前缀数组的信息来优化算法,减小时间复杂度的思路在很多字符串相关问题中能给我们启发. 首先上KMP字符串匹配算法, [leetcode在线测试地址](https://le

4种字符串匹配算法:BS朴素 Rabin-karp 有限自动机 KMP(上)

字符串的匹配的算法一直都是比较基础的算法,我们本科数据结构就学过了严蔚敏的KMP算法.KMP算法应该是最高效的一种算法,但是确实稍微有点难理解.所以打算,开这个博客,一步步的介绍4种匹配的算法.也是<算法导论>上提到的.我会把提到的四种算法全部用c/c++语言实现.提供参考学习.下图的表格,介绍了各个算法的处理时间和匹配时间.希望我写的比较清楚.如果不理解的,或者不对的,欢迎留言. 字符串匹配算法及其处理时间和匹配时间 算法 预处理时间 匹配时间 朴素算法 0 O((n-m+1)m) Rabi