zoukankan      html  css  js  c++  java
  • 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;
    }
    
  • 相关阅读:
    bzoj 1069 凸包+旋转卡壳
    bzoj 3203 凸包+三分
    bzoj 3779 重组病毒 好题 LCT+dfn序+线段树分类讨论
    bzoj 3881 [Coci2015]Divljak fail树+树链的并
    bzoj 4034 [HAOI2015]树上操作 入栈出栈序+线段树 / 树剖 维护到根距离和
    bzoj 2819 Nim dfn序+树状数组维护区间异或值
    bzoj 4031 [HEOI2015]小Z的房间 Matrix-tree定理
    BZOJ3676: [Apio2014]回文串
    BZOJ2434: [Noi2011]阿狸的打字机
    BZOJ2553: [BeiJing2011]禁忌
  • 原文地址:https://www.cnblogs.com/gtarcoder/p/4813092.html
Copyright © 2011-2022 走看看