K(看)M(毛)P(片)算法最常用在字符串匹配。給定一個長的字符串(target string)和一個短的字符串(pattern string),要求判斷pattern string是否是target string的子串,如果是,則返回子串的首個字符的下標;如果否,則返回-1。
解決這個問題最常想到的辦法就是brutal force,即從target string第一個字符開始與pattern string比較,如果相等則比較target string和pattern string的下一個字符,如果不等則返回到target string中相等的字符的下一個字符。換句話說,假設我們用target和pattern分別表示兩個字符串的指針,那么每一次比較不管兩個string匹配到何種程度,只要不是完全匹配(即匹配完成),那么target永遠只能增加1,這個算法的復雜度為O(mn).(m=strlen(target string),n=strlen(pattern string)).
以下是這個算法的C代碼。
1 int strstr(char *target, char *pattern) 2 { 3 int i,j; 4 for(i=0;;i++) 5 { 6 for(j=0;;j++) 7 { 8 if(pattern[j]==0)return i; 9 if(target[i+j]==0)return -1; 10 if(target[i+j]!=pattern[j])break; 11 } 12 } 13 }
導致這個算法時間復雜的關鍵,在於我們每次只能將target指針加一,而不能充分利用之前已匹配部分的信息。一個很好的例子由http://kenby.iteye.com/blog/1025599 給出,事實上我們可以通過利用已匹配部分的信息,讓每次比較失敗后target跳過多個位置。
下面給出KMP的代碼再給出一些解釋。
1 int* overlay(char *str) 2 { 3 int *a,len=strlen(str),i,index; 4 a[0]=-1;//美好的約定 5 a=(int *)calloc(len,sizeof(int)); 6 for(i=1;i<len;i++) 7 { 8 index=a[i-1];//上一個 9 while(index>=0&&a[index+1]!=a[i]) 10 { 11 index=a[index]; 12 } 13 if(a[index+1]==a[i]) 14 { 15 a[i]=index+1; 16 } 17 else a[i]=-1; 18 } 19 return a; 20 } 21 22 int strstr(char *target,char *pattern) 23 { 24 int i,j,*a,len1=strlen(target),len2=strlen(pattern); 25 a=overlay(pattern); 26 for(i=0,j=0;i<len1&&j<len2;) 27 { 28 if(target[i]==pattern[j]) 29 { 30 i++; 31 j++; 32 } 33 else if(j==0)i++;//若第一個字符就不相等,則對target的指針加1即可 34 else j=a[j-1]+1; 35 } 36 if(j==len2)return i-j; 37 else return -1; 38 }
overlay函數是用遞推求出pattern串每一個位置對應的覆蓋值,算法在http://blog.csdn.net/power721/article/details/6132380有解釋,不贅述。覆蓋值數組的含義在上述博客中沒有解釋得很清楚,我舉個例子。
首先假設這個字符串名為str。首字符的覆蓋值是-1,這是約定。從第二個字符開始,我們看到b的覆蓋值是-1,什么意思呢,意思就是在b之前的子串(包括b),不存在k滿足str[0]str[1]...str[k-1]str[k]=str[1-k]str[2-k]...str[1]。接下來,第三個字符a的覆蓋值為0,表示在a之前的子串(包括a),存在k=0使得str[0]str[1]...str[k-1]str[k]=str[2-k]str[3-k]...str[2]。第五個字符b的覆蓋值為1,表示在b之前的子串(包括b),存在k=1使得str[0]str[1]...str[k-1]str[k]=str[4-k]str[5-k]...str[4]。
至於overlay函數的遞推求法,博客里有說明遞推過程,我也是看了好一會才看清楚orz...下面配個圖,圖里中括號里面是相同的字符串,小括號里面也是相等的字符串,以此第三次,第四次一直往下找...大家才思敏捷,一定可以看得懂。
其余的東西在以上兩篇博客都講得很清楚,我也不再贅述。算法導論里有關於KMP算法攤還分析的一些證明,在不同的地方,上文所述的覆蓋值的含義有可能有一點點差別,命名也有不同,但總的意思和算法都是差不多的。
部分代碼參考了https://leetcode.com/problems/implement-strstr/?tab=Description