前面博文分别介绍了字符串匹配算法《朴素算法》、《Rabin-Karp算法》和《有限自动机算法》;本节介绍Knuth-Morris-Pratt字符串匹配算法(简称KMP算法)。该算法最主要是构造出模式串pat的前缀和后缀的最大相同字符串长度数组next,和前面介绍的《朴素字符串匹配算法》不同,朴素算法是当遇到不匹配字符时,向后移动一位继续匹配,而KMP算法是当遇到不匹配字符时,不是简单的向后移一位字符,而是根据前面已匹配的字符数和模式串前缀和后缀的最大相同字符串长度数组next的元素来确定向后移动的位数,所以KMP算法的时间复杂度比朴素算法的要少,并且是线性时间复杂度,即预处理时间复杂度是O(m),匹配时间复杂度是O(n)。
首先介绍下前缀和后缀的基本概念:
前缀:字符串中除了最后一个字符,前面剩余的其他字符连续构成的字符或字符子串称为该字符串的前缀;
后缀:字符串中除了首个字符,后面剩余的其他字符连续构成的字符或字符子串称为该字符串的后缀;
注意:空字符是任何字符串的前缀,同时也是后缀;
例如:字符串“Pattern”的前缀是:“P”“Pa”“Pat”“Patt”“Patte”“Patter”;
后缀是:“attern”“ttern”“tern”“ern”“rn”“n”;
在进行KMP字符串匹配时,首先要求出模式串的前缀和后缀的最大相同字符串长度数组next;下面先看下例子模式串pat=abababca的数组next:其中value值即为next数组内的元素值,index是数组下标标号;注意:next[i]是pat[0..i]的最长前缀和后缀相同的字符串,包括当前位置i的字符。之所以是这样,是因为这里讲解的KMP算法是最基本的,没有经过优化的,若要进行优化,则必须优化next数组,下面会介绍优化数组。
char: | a | b | a | b | a | b | c | a | index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
-"abababca"的前缀为[a, ab, aba, abab, ababa,ababab,abababc],后缀为[bababca, ababca, babca, abca, bca, ca,a],最大相同字符子串[a]的长度为1。
模式串的前缀和后缀的最大相同字符串长度数组next的递推求解
已知next[0..i-1],求出next[i]:
void computeNextArray(const string &pat, int M, int *next) { int len = 0; // lenght of the previous longest prefix suffix int i = 1; next[0] = 0; // next[0] is always 0 // the loop calculates next[i] for i = 1 to M-1 while(i < M) { if(pat[i] == pat[len]) { len++; next[i] = len; i++; } else // (pat[i] != pat[len]) { if( len != 0 ) {// This is tricky. Consider the example AAACAAAA and i = 7. len = next[len-1]; // Also, note that we do not increment i here } else // if (len == 0) { next[i] = 0; i++; } } } }
源码实现如下:
void KMPSearch(const string &pat, const string &txt) { int M = pat.length(); int N = txt.length(); // create next[] that will hold the longest prefix suffix values for pattern int *next = (int *)malloc(sizeof(int)*M); int j = 0; // index for pat[] // Preprocess the pattern (calculate next[] array) computeNextArray(pat, M, next); int i = 0; // index for txt[] while(i < N) { if(pat[j] == txt[i]) { j++; i++; } if (j == M) { cout<<"Found pattern at index:"<< i-j<<endl; j = next[j-1]; } // mismatch after j matches else if(pat[j] != txt[i]) { // Do not match next[0..next[j-1]] characters, // they will match anyway if(j != 0) j = next[j-1]; else i = i+1; } } free(next); // to avoid memory leak }下面举例,模式串 p at = “ abababca ” , 输入文本字符串 text = “ bacbababaabcbab ”。
由上面可知next表元素值如下
char: | a | b | a | b | a | b | c | a | index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |下面是匹配过程
第一次匹配成功的字符为相对应字符a,由于模式串下一个字符b与文本字符c不匹配,且j=1、已匹配字符数为j=1,next[j-1]=0;所以下一次向后移动的位数为j-next[j-1]=1-0=1;文本字符串当前位置i不变,更新模式串当前字符的位置j = next[j-1]=0;
bacbababaabcbab | abababca第二次匹配成功的是字符ababa;由于模式串下一个字符b与文本字符a不匹配,且j=5、已匹配字符数j=5、next[j-1]=3;所以下一次向后移动的位数为j-next[j-1]=5-3=2;即忽略两位文本字符;文本字符串当前位置i不变,更新模式串当前字符的位置j = next[j-1]=3;
bacbababaabcbab ||||| abababca经过上一步向后移动后的字符匹配为下面所示; 由于模式串下一个字符 b 与文本字符 a 不匹配,且 j=3 、已匹配字符数 j=3 、 next[j-1]=1 ;则下一次匹配是向后移动位数为j-next[j-1]=3-1=2;即忽略两位文本字符;文本字符串当前位置i不变,更新模式串当前字符的位置j = next[j-1]=1;
// x denotes a skip bacbababaabcbab xx||| abababca经过前一步的移动后得到下面的匹配; 由于模式串下一个字符 b 与文本字符 a 不匹配,且 j=1 、已匹配字符数 j=1 、 next[j-1]=0 ; 则下一次匹配是向后移动位数为j-next[j-1]=1-0=1;但是此时,模式串的字符长度大于待匹配的文本字符长度,所以,模式串匹配失败,即在文本字符串中不存在与模式串相同的字符串;
// x denotes a skip bacbababaabcbab xx| abababca
完整程序:
#include<iostream> #include<string> #include<stdlib.h> using namespace std; void computeNextArray(const string &pat, int M, int *next); void KMPSearch(const string &pat, const string &txt) { int M = pat.length(); int N = txt.length(); // create next[] that will hold the longest prefix suffix values for pattern int *next = (int *)malloc(sizeof(int)*M); int j = 0; // index for pat[] // Preprocess the pattern (calculate next[] array) computeNextArray(pat, M, next); int i = 0; // index for txt[] while(i < N) { if(pat[j] == txt[i]) { j++; i++; } if (j == M) { cout<<"Found pattern at index:"<< i-j<<endl; j = next[j-1]; } // mismatch after j matches else if(pat[j] != txt[i]) { // Do not match next[0..next[j-1]] characters, // they will match anyway if(j != 0) j = next[j-1]; else i = i+1; } } free(next); // to avoid memory leak } void computeNextArray(const string &pat, int M, int *next) { int len = 0; // lenght of the previous longest prefix suffix int i = 1; next[0] = 0; // next[0] is always 0 // the loop calculates next[i] for i = 1 to M-1 while(i < M) { if(pat[i] == pat[len]) { len++; next[i] = len; i++; } else // (pat[i] != pat[len]) { if( len != 0 ) {// This is tricky. Consider the example AAACAAAA and i = 7. len = next[len-1]; // Also, note that we do not increment i here } else // if (len == 0) { next[i] = 0; i++; } } } } int main() { string txt = "ABABDABACDABABCABAB"; string pat = "ABABCABAB"; KMPSearch(pat, txt); system("pause"); return 0; }
优化求出模式串的前缀和后缀的最大相同字符串长度数组next;下面先看下例子模式串pat=abab的优化数组next:index是数组下标标号,shift标志value值向右移一位之后,并把第一个值初始化为-1的值,next数组内的元素值是对shift值进一步优化;注意:next[i]是pat[0..i]的最长前缀和后缀相同的字符串,不包括当前位置i的字符,所以这里是优化之后的next数组。
char: | a | b | a | b | index: | 0 | 1 | 2 | 3 | value: | 0 | 0 | 1 | 2 | shift:| -1 | 0 | 0 | 1 | next: | -1 | 0 | -1 | 0 |下面通过例子讲解优化的过程,假设输入文本字符串和模式串分别为 txt = "abacababc",pat = "abab";
第一次匹配成功如下,若根据没有优化的数组进行匹配时,优化之前的数组为shift,则当前模式串字符b与文本字符c不匹配,当前匹配失败的字符位置是j=3;则模式串右移j-shift[j] = 3-1=2位,
abacababc ||| abab
经过上一步骤后,模式串字符b还是与文本字符c失配。而且失配对应的字符和上一步骤完全一样。事实上,因为在上一步的匹配中,已经得知pat[3] = b,与txt[3] = c失配,而右移两位之后,让pat[shift[3]] = pat[1] = b再跟txt[3]匹配时,必然失配。
//x denotes a skip abacababc xx| abab
我们重新看下模式串pat=abab的优化数组next;下面是优化数组next的操作过程:
___________________________________________________________________________________ |char: | a | b | a | b | |_________|_______________|___________________|_________________|_________________| |index: | 0 | 1 | 2 | 3 | |_________|_______________|___________________|_________________|_________________| |value: | 0 | 0 | 1 | 2 | |_________|_______________|___________________|_________________|_________________| |shift: | -1 | 0 | 0 | 1 | |_________|_______________|___________________|_________________|_________________| |reason: | The initial | p[1]!=p[shift[1]] | p[2]=p[shift[2]]| p[3]=p[shift[3]]| | |value unchanged| | | | |_________|_______________|___________________|_________________|_________________| |operator:|do nothing |do nothing | shift[2]= | shift[3]= | | | | | shift[shift[2]] | shift[shift[3]] | |_________|_______________|___________________|_________________|_________________| |next: | -1 | 0 | -1 | 0 | |_________|_______________|___________________|_________________|_________________|
下面给出优化后的程序:
#include <iostream> #include <string> #include<stdlib.h> using namespace std; void computeNextArray(const string &pat, int M, int *next) { int j=0,k=-1; next[0]=-1;//优化next,初始值为-1 while(j<M-1) { if(k==-1 || pat[j]==pat[k]) { ++j; ++k; if(pat[j]!=pat[k])next[j]=k; //因为不能出现pat[j] = pat[ next[j ]],所以当出现时需要继续递归 else next[j]=next[k]; } else k=next[k]; } } void kmpSearch(const string&txt,const string&pat) { int i=0,j=0; int N = txt.length(); int M = pat.length(); int *next = (int *)malloc(sizeof(int)*M); computeNextArray(pat, M, next); cout<<"The value of next are:"; for ( i = 0; i < M; i++) { cout<<next[i]<<" "; } cout<<endl; i = 0;//注意:i的值必须为0,因为从第一个字符开始比较 while(i<N && j<M) { if(j==-1 || txt[i]==pat[j]) { i++; j++; } else j=next[j]; } if(j==M)cout<<"Found pattern at index:"<< i-j<<endl; free(next); } int main() { string txt = "aacababc"; string pat = "abab"; kmpSearch(txt,pat); system("pause"); return 0; }
参考资料:
《算法导论》
http://jakeboxer.com/blog/2009/12/13/the-knuth-morris-pratt-algorithm-in-my-own-words/
http://www.geeksforgeeks.org/searching-for-patterns-set-2-kmp-algorithm/
http://blog.csdn.net/v_july_v/article/details/7041827
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm
http://www.cnblogs.com/gaochundong/p/string_matching.html
http://dsqiu.iteye.com/blog/1700312