KMP算法

    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;
	}
}

(2) kmp匹配

//返回母串中有多少个子串
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;
}
原文地址:https://www.cnblogs.com/gtarcoder/p/4813092.html