Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。
下面先直接给出KMP的算法流程:
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,
- 如果
j = -1
,或者str[i] == p[j]
(即当前字符匹配成功),都令i++,j++
,继续匹配下一个字符; - 如果
j != -1
,且str[i] != p[j]
(即当前字符匹配失败),则i
不变,令j = next[j]
,可以理解为当匹配失败时,模式串p
并不是从头开始匹配,而是从next[j]
位置继续匹配;
与暴力匹配相比,KMP的不同之处就在于j
的回溯位置,j
不是傻乎乎的回溯到初始位置,因而大大提高了算法效率。这之中发挥巨大作用的就是next[]
数组,从上面的分析中我们已经知道,next[]
数组存在的意义就是当匹配失败时,我们能确定的知道j
应该回溯到什么位置。那么为什么next[]
会有这么强大的功能呢?
这就要从字符串的前缀和后缀说起:
- 先解释定义:
例如字符串hello
,它的前缀集合为{h, he, hel, hell}
,后缀集合为{ello, llo, lo, 0}
,要注意的是,字符串本身并不是自己的后缀。 - 求PMT;
例如,对于aba
,它的前缀集合为{a, ab}
,后缀 集合为{ba, a}。两个集合的交集为{a},那么PMT为1。再比如,对于字符串ababa
,它的前缀集合为{a, ab, aba, abab}
,它的后缀集合为{baba, aba, ba, a}
, 两个集合的交集为{a, aba}
,那么PMT为3。
这里解释一下:PMT是一个被称为部分匹配表(Partial Match Table)的数组,PMT中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度(这里的字字符串是指从初始位置到当前位置这一字串)。
下面我们举例说明如何求PMT,例如有一字符串abababca
:
char | a | b | a | b | a | b | c | a |
---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
pmt | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
接下来就是我们的重头戏了,如何求next[]
数组?
当我们匹配到c
时,如下图所示:
此时匹配失败,但是abababca,即前6个字符是已经匹配成功的,对应的子串是ababab
,前面已经知道pmt[5] = 4
,即存在前缀ababab,存在后缀ababab,这里最重要的一点就是c
前面的4个字符abababc也必定匹配成功,那么如果将模式串向右移动两个位置,即将原来后缀的位置替换成前缀这4个字符,如图(b)所示,此时前面4个字符一定匹配成功,j
回溯到这个位置,继续匹配,从图中可以看出next[6] = 4
(c
对应的index
为6);
说了半天,好像并没说如何求next[]
数组,但是真正的理解了的话,可以知道next[]
和pmt[]
是有直接关系的,next[j] = pmt[j - 1]
。
下面以表格的形式给出具体next[]
数组的值:
char | a | b | a | b | a | b | c | a |
---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
pmt | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
next | -1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |
接下来给出求next[]
数组的代码,这里跳过了求pmt
的环节:
void InitNextArr(char str[], int next[]){
int i = 0, j = -1;
next[0] = -1;
while(i < strlen(str)){
if(j == -1 || str[i] == str[j]){
i++;
j++;
next[i] = j;
}
else{
j = next[j];
}
}
}
现在给出匹配函数:
int KMP(char str[], char p[], int next[]){
int i = 0, j = 0;
while(i < strlen(str) && j < strlen(p)){
if(j == -1 || str[i] == p[j]){
i++;
j++;
}
else
j = next[j]; //当匹配失败时,j回溯
}
if(j == strlen(p))
return i - j;
else
return -1;
}
完整代码如下:
#include <iostream>
#include <stdio.h>
#include <cstring>
using namespace std;
void InitNextArr(char str[], int next[]){
int i = 0, j = -1;
next[0] = -1;
while(i < strlen(str)){
if(j == -1 || str[i] == str[j]){
i++;
j++;
next[i] = j;
}
else{
j = next[j];
}
}
}
int KMP(char str[], char p[], int next[]){
int i = 0, j = 0;
while(i < strlen(str) && j < strlen(p)){
if(j == -1 || str[i] == p[j]){
i++;
j++;
}
else
j = next[j];
}
if(j == strlen(p))
return i - j;
else
return -1;
}
int main(){
char ch1[] = "abcdabcde", ch2[] = "abcde";
int next[8];
InitNextArr(ch2, next);
printf("%d\n", KMP(ch1, ch2, next));
}