KMP算法详解

来源:互联网 发布:中国居民膳食指南 知乎 编辑:程序博客网 时间:2024/04/27 14:40

以下转自http://blog.csdn.net/buaa_shang/article/details/9907183
字符串匹配是计算机的基本任务之一。
举例来说,有一个字符串”BBC ABCDAB ABCDABCDABDE”,我想知道,里面是否包含另一个字符串”ABCDABD”?

1.
http://image.beekka.com/blog/201305/bg2013050103.png
首先,字符串”BBC ABCDAB ABCDABCDABDE”的第一个字符与搜索词”ABCDABD”的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

2.这里写图片描述
因为B与A不匹配,搜索词再往后移。

3.这里写图片描述
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

4.这里写图片描述
接着比较字符串和搜索词的下一个字符,还是相同。

5.这里写图片描述

直到字符串有一个字符,与搜索词对应的字符不相同为止。

6.这里写图片描述
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把”搜索位置”移到已经比较过的位置,重比一遍。

7.这里写图片描述
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是”ABCDAB”。KMP算法的想法是,设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。

8.这里写图片描述
怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

9.这里写图片描述
已知空格与D不匹配时,前面六个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符B对应的”部分匹配值”为2,因此按照下面的公式算出向后移动的位数:
  移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。

10.这里写图片描述
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(”AB”),对应的”部分匹配值”为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

11.这里写图片描述
因为空格与A不匹配,继续后移一位。

12.这里写图片描述
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

13.这里写图片描述
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

14.这里写图片描述
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:”前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。

15.这里写图片描述
“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”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。
  
16.这里写图片描述
“部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是2(”AB”的长度)。搜索词移动的时候,第一个”AB”向后移动4位(字符串长度-部分匹配值),就可以来到第二个”AB”的位置。
(完)

以下转自http://kenby.iteye.com/blog/1025599

一 kmp算法为什么比传统的字符串匹配算法快

假设文本T = y1y2y3….yn, 模式 P = p1p2p3…pm, 传统的匹配算法把位移为0,1,…n-m时的文本依次跟P比较,每次比较最多花费O(m)的时间,算法的复杂度为O((n-m+1)*m)。这种算法没有利用匹配过的信息,每次都从头开始比较,速度很慢。而kmp算法充分利用了之前的匹配信息,从而避免一些明显不合法的位移。加快匹配过程。来看一个例子:

#000xxxx000###### 文本T

|<—- s —->|000xxxx000~~~ 模式P

假设位移为s时,T和P匹配了上述000xxxx000部分,即匹配到了模式P的前10个字符,如果按照传统的匹配方法,下一步就是从位移s+1开始比较,而kmp算法则直接从位移s+7开始比较,而且断定:位移s+7对应的串和模式P的前3个字符是相同的,可
以不用比较,直接从第4个字符开始比较,这种跳跃式的匹配是不是比传统匹配方法快很多,如下图所示:

###000xxxx000###### 文本T

|<——– s+7——–>| 000xxxx000~~~ 模式P

那么kmp是如何实现这种跳跃的呢?注意到所给的字符,即模式P的前10个字符,有一个特点:它的开始3个字符和末尾3个字符是一样的,又已知文本T也存在相同部分的字符,我们把位移移动 10-3 = 7个位置,让模式P的开始3个字符对准文本T的末尾3个字符,那么它们的前3个字符必然可以匹配。

二 构造前缀数组

上面的例子是文本T和模式P匹配了前面10个字符的情况下发生的,而且我们观察到模式P的前缀P10中,它的开始3个字符和末尾3个字符是一样的。如果对于模式P的所有前缀P1,P2…Pm,都能求出它们首尾有多少个字符是一样的,当然相同的字符数越多越好,那么就可以按照上面的方法,进行跳跃式的匹配。

定义:
Pi表示模式P的前i个字符组成的前缀, next[i] = j表示Pi中的开始j个字符和末尾j个字符是一样的,而且对于前缀Pi来说,这样的j是最大值。next[i] = j的另外一个定义是:有一个含有j个字符的串,它既是Pi的真前缀,又是Pi的真后缀

规定:
next[1] = next[0] = 0

next[i]就是前缀数组,下面通过1个例子来看如何构造前缀数组。
例子1:cacca有5个前缀,求出其对应的next数组。
前缀2为ca,显然首尾没有相同的字符,next[2] = 0
前缀3为cac,显然首尾有共同的字符c,故next[3] = 1
前缀4为cacc,首尾有共同的字符c,故next[4] = 1
前缀5为cacca,首尾有共同的字符ca,故next[5] = 2

000#xxx000 前缀P10
000 末尾3个字符

如果仔细观察,可以发现构造next[i]的时候,可以利用next[i-1]的结果。假设模式已求得next[10] = 3,如下图所示:

000#xxx000 前缀P10
000 末尾3个字符

根据前缀函数的定义:next[10] = 3意味着末尾3个字符和P10的前3个字符是一样的
为求next[11],可以直接比较第4个字符和第11个字符,如下图所示:蓝色和绿色的#号所示,如果它们相等,则
next[11] = next[10]+1 = 4,这是因为next[10] = 3保证了前缀P11和末尾4个字符的前3个字符是一样的.

000#xxx000# 前缀P11
000# 末尾4个字符

所以只需验证第4个字符和第11个字符。但如果这两个字符不想等呢?那就继续迭代,利用next[next[10] = next[3]的值来求next[11]。其实这里用到的是一种DP思想,因为next[10] = 3的,即表明了[1,3]跟[8,10]是相同,既然相同的话,那么next[3] = 2的时候,即[1,2]跟[2,3]是相同的
,因为有[1,3] == [8,10],所以有[1, 2] == [8,9],所以接下来要判断的,便是第三个字符是否和第10个字符相等了

这里给出的KMP匹配的时候,下标是从0开始计算的,且初始next[0]时候是-1

void getFail() {    next[0] = -1;    int i = 0, j = -1;    while (i < len) {        //当j等于-1时,即表明前缀和后缀相等的长度为0        //当str[j] == str[i]时,即[0,j] == [i - j, i]的,所以公共长度为j,又因为从0开始,所以j + 1,这里也有另一种含义,当第i + 1位不匹配的时候,下一个用来判断的是第j + 1个        if (j == -1 || str[j] == str[i]) {            i++; j++;            next[i] = j;        }        else j = next[j];    }}
0 0