您的位置:首页 > 其它

KMP算法

2015-09-16 13:35 204 查看
KMP算法是字符串匹配处理中一种非常高效的算法,它的时间复杂度可以达到O(N+M),远优于普通匹配的O(NxM)。它最早是由Knuth,Morris,Pratt共同提出。

算法原理

普通的字符串匹配,假设从母串的A位置开始匹配,在某个位置B当母串和子串失配的时候匹配的起点会回溯到A+1处重新开始。而从A到B的中有一些位置的字符的信息已经获知,此时是可以提前知道从A之后的哪个位置重新开始匹配是有可能成功(或者哪些位置开始匹配必然失败).



比如,以上的字符串匹配,母串从index 0开始,当匹配到index = 5的时候失配,此时一般的字符串匹配是将母串回溯到index = 1重新开始,此时可以发现匹配的第一个即失效。其实,如果需要从index=1开始进行重新匹配,则需要子串中[0, 4]的部分和母串中[1,5]的部分相同,由于之前母串的[0,4]和子串的[0,4]匹配,这说明需要 子串中[0,4]的部分和[1,5]的部分相同,这只需要子串的信息即可获知。

那么,如果利用可以获知的子串的信息,就不需要母串回溯到index=1。如图中所示,如果知道失配之前的子串[0,4]部分中前缀DE和后缀DE相同(最长相同的前后缀),则只需要将母串指针回溯到 index=3(3 = 5 - 2)即可重新开始匹配,且能够保证从此处之前开始匹配肯定不能匹配成功
因为如果在index=i(i< 3)开始匹配能够成功,则说明子串中[0, 4 - i] = 母串中[i, 4] = 子串中[i, 4],而i < 3则可知区间[0, 4-i]的长度大于2,即大于已知的最长相同前后缀长度2,矛盾!

KMP思想
KMP的思想就是利用可以提前获知的子串中的前后缀信息,来减少回溯距离,得到下一次重新开始匹配的位置。这需要对子串中每个位置i都确定子串中[0, i]相同最长前缀和后缀的长度。在KMP中是使用一个next数组来保存。
KMP匹配流程
(1)获得子串每个位置i确定的子子串[0,i]中的最长相同前后缀长度,用next数组保存
(2)母串当前匹配点为pos(pos开始为0),pos对应子串的位置索引为 p,当匹配到子串位置p时候失配,则查找子串next数组的,更新下次的母串匹配点为 pos = pos + p - next[p-1] ,同时p变为 p = next[p-1]
(3)回溯之后,重新开始匹配,直到匹配成功或者到母串结束仍未成功,则返回失败

next数组
next数组保存每个位置确定的子串的最长前后缀的长度。其生成的时候按照index从0开始增加,当到达index = i的时候,可以根据index = 0, 1, 2.. i - 1的next相关信息得到。



如上图所示,当前index = i,可以知道sub_str中,[0, next[i-1]-1]部分和[i - next[i-1], i - 1]部分是相同的,即图中的黄色字体区域。此时若:
(1) sub_str[next[i-1]] == sub_str[i]
显然,next[i] = next[i-1] + 1
(2)sub_str[next[i-1]] != sub_str[i]
可以确定next[i]的值必定不大于next[i-1],记p = next[i] - 1,假设next[i] > next[i-1],p >= next[i-1],则字符串中[0, p]和[i-p, i]区域相同(即图中绿色方块区域),且[0, p]包含[0,next[i-1]-1],[i-p,i]包含[i-next[i-1], i-1],则[0, p-1]和[i-p, i-1]相同(即图中的[0, p-1]和[P'-i-1]的黄色字体区域),此时p> next[i-1],这与next[i-1]为[0,i]区域内最长相同前后缀长度矛盾!!
因此,next[i]位于[0, next[i-1]-1]中,即j = next[i-1]-1,还可以证明 next[i]不大于next[j].



假设next[i]大于next[j],记p = next[i]-1. 此时图中的括号标记起来的D区域和E区域相同。由于[0, next[i-1]-1]和[i-next[i-1], i-1]相同(即图中的F和G区域),则图中的A区域和B区域就相同(因为A和B分别相对于F和G只是减少了最后一个字符),而B和C是相同的,则A区域和C区域就是相同的。此时,在区域[0, j]中A和C相同,导致p称为最长相同前后缀长度,且p > next[j]。这与最大相同前后缀长度next[j]矛盾!!!

因此,next[i]的位置只能在[0, next[j]]之间,此时判断sub_str[next[j]]是否等于sub_str[i],若相同,则next[i] = next[j] + 1,否则继续将j移动到next[j-1]...

算法实现

(1) next数组的生成

void GenerateNext(const char* sub_str, int* next, int len){
next[0] = 0;								//next[i]表示sub_str[0, i]中最长相同前后缀的长度
for (int i = 1; i < len; i++){
int j = next[i - 1];
while (sub_str[j] != sub_str[i]){		//不断回溯,每次利用next数组来确定下一个需要判断的位置
if (j == 0){
j = -1;
break;
}
j = next[j - 1];
}
if (j < 0)
next[i] = 0;
else
next[i] = j + 1;
}
}


[b](2) kmp匹配[/b]

//返回母串中有多少个子串
int Kmp(char* mas_str, const char* sub_str, int* next, int len){
int count = 0;
char* pos = mas_str;	//母串中当前检查的位置
int i = 0;				//子串中当前检查的位置
char* end = mas_str + strlen(mas_str);
while (pos + len - i <= end){
while (i < len && *(pos + i) == sub_str[i]){
i++;
}
if (i == len){
count++;
}
if (i == 0){	//i == 0时,没有next[i-1],单独处理
pos++;
}
else{
pos += (i - next[i - 1]);	//利用next数组进行更新
i = next[i - 1];
}
}
return count;
}


(3) 更好的实现

//next[i] 为 pattern[0,1,2...i] 中最长的相同前后缀 pattern[0, 1, ...j] 和 pattern [k, k+1...i]
//的尾 index,即如果最长前缀长度为m,则 next[i] = m-1;

void GetNext(const char* pattern, int* next){
int j = -1;
next[0] = j;
int n = strlen(pattern);
for (int i = 1; i < n; i++){
while (j > -1 && pattern[i] != pattern[j + 1])
j = next[j];
if (pattern[i] == pattern[j + 1])
j++;
next[i] = j;
}
}

int kmp(const char* str, const char* pattern){
int j = -1;
int m = strlen(pattern);
int n = strlen(str);
if (n == 0 || m == 0)
return 0;

int* next = new int[m + 1];
GetNext(pattern, next);
for (int i = 0; i < n; i++){
while (j > -1 && pattern[j + 1] != str[i])
j = next[j];

if (str[i] == pattern[j + 1])
j++;
if (j == m - 1){
delete[] next;
return i - j;
}
}
delete[] next;
return -1;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: