一直打算寫KMP算法的筆記,但是對這個算法的推導過程着實有點恐懼,但是又不能停到這裏不往下去學,我也不想跳過,所以決定硬着頭皮認真寫下去,總有一天會寫完的。
KMP算法的作用用是儘可能簡單的方法比較兩個字符串,普通的字符串匹配算法有很多步都是多餘的。
先看一下普通的字符串匹配算法(也叫樸素的匹配算法):
//返回子串T在主串S中第pos個字符之後的位置。若不存在,則函數返回0
//T非空,1≤pos≤StrLength(s)
int Index(char *S, char *T, int pos)
{
int i = pos; //i用於主串S中當前位置下標,若pos不爲1
//則從pos位置開始匹配
int j = 1; //j用於子串T中當前位置下標值
while(i <= S[0] && j <= T[0]) //若i小於S的長度且j小於T的長度時循環
{
if(S[i] == T[j]) //兩字母相等則繼續
{
++i;
++j;
}
else //指針後退重新開始匹配
{
i = i-j+2; //i退回到上次匹配首位的下一位,
//i = i-(j-1)+1=i-j+2,減去循環
//的次數(j從1開始,所以循環了j-1次),
//加1回到上次匹配首位的下一位。
j = 1; //j退回到子串T的首位
}
}
if(j > T[0])
return i-T[0];
else
return 0;
}
普通的字符串匹配算法每次匹配到不相同的字符時子串都要回溯到主串上次匹配首位的下一位,這樣匹配的效率太低了。
KMP算法主要就是要掌握next數組和nextval數組的推導,next數組和nextval數組的推導過程都能看懂,但是要讓人看懂,很有必要花大量文字描述推導的過程,所以當我看到這裏的時候,感覺很頭大,當時也是我缺乏耐心,心情浮躁,推導過程雖多,但簡單易懂,只要認真看,肯定能看懂;靜不下心來看推導過程是一個問題,強行向將文字翻譯成代碼是我第二個耐心耗光的原因,這也是我鑽牛角尖了,這算法是算法大師寫出來經過了好多年的流傳,怎麼可能讓我一下子想出來,現在想想我想通過文字推導過程轉換爲簡單易懂的代碼就是個笑話,也許以後可以,但是現在肯定做不到。所以要看懂代碼最方便的方法就是找幾個例子帶到代碼中體驗一下算法的邏輯,這不難,就是要有耐心,要靜下心來慢慢理解。
那next數組和nextval數組是幹什麼的呢?
舉例說明:主串S=“abcdefgab”,子串T=“abcdex”,普通的字符串匹配過程是:
顯然,對於子串T來說,“abcdex"首字母"a"與後面的串"bcdex"中任意一個字符都不相等,所以過程①比較完後,過程②③④⑤的判斷都是多餘的。那如果子串後面的字符有的和前面的字符相等呢?
再看個例子:主串S=“abcabeabc”,子串T=“abcabx”。
上面的推導也都是根據樸素匹配算法推導的,和第一個推導類似,②③是多餘的,T的首位"a”,與第四位的"a"相等,第二位的"b"與第五位的"b"相等。而在①時,第四位的"a"與第五位的"b"已經與主串S中的相應位置比較過了,是相等的,因此可以斷定,T的首字符"a"、第二位的字符"b"與S的第四位字符和第五位字符也不需要比較了,肯定也是相等的——之前比較過了,所以④⑤兩個步驟也可以省略。
上面的兩個推導,在①中,i=6,在②③④⑤過程中,i值是2、3、4、5,到了⑥,i值才又回到了6,即在樸素的模式匹配算法中,主串的i值是不斷地回溯的,經分析,這種回溯其實可以不需要。
既然i值不回溯,那麼要考慮的變化就是j值了,j值的變化與主串沒什麼關係,關鍵就取決於T串的結構中是否有重複的問題。
所以next數組就用來保存T串各個位置的j值的變化。注意是j值的變化,j值和j值的變化是不一樣的。
這裏給幾個例子,但不描述詳細的推導過程:
由上述幾個例子可知,當前字符之前的串如果前綴和後綴有一個字符相等,next[j]就等於2,前綴和後綴有兩個字符相等,next[j]就等於3,n個相等next[j]就是n+1。
這裏還得再不厭其煩的介紹下前綴和後綴,比如,對於串T=“abcab”,其前綴和後綴相等的字符就是"ab"。再比如串T=“ababaaa”,其前綴和後綴相等的字符就是"a"。瞭解了這,判斷當前字符之前的串的前綴和後綴相等字符的數量就很容易了。
光有next數組的值還不能完全排除多餘的過程,還得需要nextval數組的值,這在後面再說,先來看看求next數組的代碼:
//通過計算返回子串T的next數組
void get_next(char *T, int *next)
{
int i,j;
i=1;
j=0;
next[1]=0;
while(i<T[0]) //此處T[0]表示串T的長度
{
if(j==0 || T[i]==T[j]) //T[i]表示後綴的單個字符,
//T[j]表示前綴的單個字符
{
++i;
++j;
next[i] = j;
}
else
j = next[j]; //若字符不相同,則j值回溯
}
}
理解代碼一個好方法就是將例子帶到程序中,可以將上面幾個已經求好next數組值的字符串帶到程序中自己在紙上寫出next數組值,看看是不是一樣。
再來看看將next數組應用到樸素匹配算法中的代碼:
//返回子串T在主串中第pos個字符之後的位置。若不存在,則函數返回值爲0
//T非空,1≤pos≤StrLength(S)
int Index_KMP(char *S, char *T, int pos)
{
int i = pos; //i用於主串S當前位置下標值,若pos
//不爲1,則從pos位置開始匹配
int j = 1; //j表示子串T中當前位置下標值
int next[255]; //定義next數組
get_next(T,next); //對串T作分析,得到next數組
while(i <= S[0] && j <= T[0]) //若i小於S的長度且j小於
//T的長度時,循環繼續
{
if(j==0 || S[i] == T[j]) //兩字母相等則繼續,與樸素算法
//相比,增加了j==0判斷
{
++i;
++j;
}
else //指針後退重新開始匹配
{
j = next[j]; //j退回合適的位置,i值不變
}
}
if(j > T[0])
return i-T[0];
else
return 0;
}
上面提到next數組不能完全排除多餘的過程,所以還有nextval數組,先看一下next數組不能排除多餘過程的例子:
主串S=“aaaabcde”,子串T=“aaaaax”,其next數組值分別爲012345,在開始時,當i=5、j=5時,"b"與"a"不相等,如下圖①,因此j=next[5]=4,如下圖②,此時"b"與j=4處的"a"依然不等,j=next[4]=3,如下圖的③,後面④⑤也是一樣,直到j=next[1]=0時,此時i++、j++,得到i=6、j=1,如下圖的⑥。
可以發現,②③④⑤步驟都是多餘的,T串的第二、三、四、五位置的字符都與首位的“a”相等,所以可以用首位next[1]的值取取代與它相等的字符後後續next[j]的值。
//通過計算返回子串T的next數組
void get_nextval(char *T, int *nextval)
{
int i,j;
i=1;
j=0;
nextval[1]=0;
while(i<T[0]) //此處T[0]表示串T的長度
{
if(j==0 || T[i]==T[j]) //T[i]表示後綴的單個字符,
//T[j]表示前綴的單個字符
{
++i;
++j;
if(T[i] != T[j]) //若當前字符與前綴字符不同
nextval[i] = j; //則當前的j爲nextval在i位置的值
else
nextval[i] = nextval[j]; //如果與前綴字符相同,
//則將前綴字符的nextval值
//賦值給nextval在i位置的值
}
else
j = nextval[j]; //若字符不相同,則j值回溯
}
}
再給出兩個個例子,可以帶進代碼中幫助理解代碼: