文章來源:c_cloud KMP
思想
KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,因此人們稱它爲克努特——莫里斯——普拉特操作(The Knuth-Morris-Pratt Algorithm,簡稱KMP算法)。KMP算法的關鍵是利用匹配失敗後的信息,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函數,函數本身包含了模式串的局部匹配信息
首先介紹幾個算法的術語:
- 前綴
前綴指除了最後一個字符以外,一個字符串的全部頭部組合
- 後綴
後綴指除了第一個字符以外,一個字符串的全部尾部組合
部分匹配值
部分匹配值就是”前綴”和”後綴”的最長的共有元素的長度。以”ABCDABD”爲例
“A”的前綴和後綴都爲空集,共有元素的長度爲0;
“AB”的前綴爲[A],後綴爲[B],共有元素的長度爲0;
“ABC”的前綴爲[A, AB],後綴爲[BC, C],共有元素的長度0;
“ABCD”的前綴爲[A, AB, ABC],後綴爲[BCD, CD, D],共有元素的長度爲0;
“ABCDA”的前綴爲[A, AB, ABC, ABCD],後綴爲[BCDA, CDA, DA, A],共有元素爲”A”,長度爲1;
“ABCDAB”的前綴爲[A, AB, ABC, ABCD, ABCDA],後綴爲[BCDAB, CDAB, DAB, AB, B],共有元素爲”AB”,長度爲2;
“ABCDABD”的前綴爲[A, AB, ABC, ABCD, ABCDA, ABCDAB],後綴爲[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度爲0
“部分匹配”的實質是,有時候,字符串頭部和尾部會有重複。比如,”ABCDAB”之中有兩個”AB”,那麼它的”部分匹配值”就是2(”AB”的長度)。搜索詞移動的時候,第一個”AB”向後移動4位(字符串長度-部分匹配值),就可以來到第二個”AB”的位置。
在KMP算法中,模式字符串向右移動的位數 = 已匹配的字符數 - 對應的部分匹配值
部分匹配的實現
下面給出模式串右移位數:
void makeNext(const char P[],int next[])
{
int q,k;//q:模式字符串下標;k:最大前後綴長度
int m = strlen(P);//模式字符串長度
next[0] = 0;//模版字符串的第一個字符的最大前後綴長度爲0
for (q = 1,k = 0; q < m; q++)//for循環,從第二個字符開始,依次計算每一個字符對應的next值
{
while(k > 0 && P[q] != P[k])//遞歸的求出P[0]到P[q]最大的相同的前後綴長度k
k = next[k-1];
if (P[q] == P[k])//如果相等,那麼最大相同前後綴長度加1
{
k++;
}
next[q] = k;
}
}
已知前一步計算時最大相同的前後綴長度爲k(k>0),即P[0]···P[k-1];
上面的代碼中
while(k > 0 && P[q] != P[k])//遞歸的求出P[0]到P[q]最大的相同的前後綴長度k
k = next[k-1];
if (P[q] == P[k])//如果相等,那麼最大相同前後綴長度加1
{
k++;
}
這幾步是比較難理解的,實際上這幾步就是上面我們手工對字符串”ABCDABD”進行的部分匹配值計算。
“ABCD”的前綴爲[A, AB, ABC],後綴爲[BCD, CD, D],共有元素的長度爲0;
“ABCDA”的前綴爲[A, AB, ABC, ABCD],後綴爲[BCDA, CDA, DA, A],共有元素爲”A”,長度爲1;
“ABCDAB”的前綴爲[A, AB, ABC, ABCD, ABCDA],後綴爲[BCDAB, CDAB, DAB, AB, B],共有元素爲”AB”,長度爲2;
“ABCDABD”的前綴爲[A, AB, ABC, ABCD, ABCDA, ABCDAB],後綴爲[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度爲0
截取這幾步,我們按照從左向右的順序進行模式匹配,字符串”ABCDABD”依序增加的話,因爲前綴與後綴相同的子字符串順序是一致的,所以部分匹配值增加的話也是根據上一字符串的部分匹配值基礎進行。
我們拿到一個字符串計算部分匹配值,如果上一字符串匹配值爲0,我們肯定會先將字符串的第一位與最後一位進行匹配,因爲此時字符串最多隻能可能有1的部分匹配值,否則的話上一字符串匹配值不會是0。
如果上一字符串部分匹配值爲k時,我們將字符串的前(k+1)位與後(k+1)位進行匹配,如果匹配成功,則next值爲k+1,
如果匹配失敗的話:
while(k > 0 && P[q] != P[k])
k = next[k-1];
P[k]已經和P[q]失配了,而且P[q-k] ··· P[q-1]又與P[0] ···P[k-1]相同。假設模式串長度length,失配也就是講模式串的前k位與P[length-k-1:length-2]相同,而P[k]與P[length-1]不同。看來P[0]···P[k-1]這麼長的子串是用不了了,那麼我要找個同樣也是P[0]打頭、P[k-1]結尾的子串,即P[0]···Pj-1,看看它的下一項P[j]是否能和P[q]匹配。循環進行,直到k=0,從頭再開始。
那麼爲什麼j==next[k-1]
呢?上一次k的匹配失敗,導致求下一個k值只能在模式串的前next[k-1]字符中尋找,而非next[k]-1。因爲前面的尋找依然是一次模式匹配,所以next數組的求值實際上就是一個遞歸。
“ABCABHABCAB” 部分匹配值爲5
“ABCABHABCABC”
繼續增加字符時出現失配,j=next[k-1],也就是求next[4]=2。因爲之前的”ABCABH”沒法使用了,但是”ABCAB”是匹配過的,”H”無法使用,就在”ABCAB”中尋找匹配度,目的是嘗試對下標next[4]與整個模式串的最後一位匹配。