KMP算法的原理
這個算法理解起來比較複雜,看了網上很多帖子,寫的都很亂,不容易理解。現在結合看過的一些書和視頻寫一些好理解的筆記,希望能給大家帶來幫助:
總的思想還是想要回退的時候能儘量偷懶,利用已知的信息,阮老師講的很清楚:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
也就是說在每次不匹配發生回退的時候,儘量能讓之前比較過的一致的字符能夠不用再重複匹配。
KMP算法的實質是,有時候,字符串頭部和尾部會有重複。比如,"ABCDAB"之中有兩個"AB",那麼它的"部分匹配值"就是2("AB"的長度)。搜索詞移動的時候,第一個"AB"向後移動4位(字符串長度-部分匹配值),就可以來到第二個"AB"的位置。
這就引出了最長的真前綴和 真後綴匹配長度的問題,而基於模式串自身的自匹配性,這個長度在給出模式串的時候已經能算出來。
我們把模式串首先遍歷一遍,將每個位置之前字串的最大真前綴和真後綴長度事先存在next數組中,這樣每當發生一次失配時,在這個數組中查找這個最長的匹配長度,然後移動到這個最長匹配位置。舉個例子:
當上面的情況發生空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。查表可知,最後一個匹配字符B對應next數組中的"部分匹配值"爲2,即“AB”,因此按照下面的公式算出向後移動的位數:
移動位數 = 已匹配的字符數 - 對應的部分匹配值
因爲 6 - 2 等於4,所以將搜索詞向後移動4位。
怎麼求next數組?
先來考察這樣的一個場景,在字符串P[i]和模式串P[j]處發生了失配,KMP會去查表,取出next[j],即用P[t]去取代P[j],讓P[t]繼續與之前的P[i]相對齊。那麼爲什麼會選定這樣的t呢?或者說這樣的t又具備哪些必要條件呢?
經過分析我們不難發現,選擇這個t的必要條件是:在P[j]之前已經適配的子串中,必須有一個長度爲t的前綴和一個長度爲t的後綴完全匹配,也就是說這個字串的首部和尾部具有一定的相似性。選擇這個t必要條件也就可以表示爲:
P[0,t) == P[j-t,j)
將滿足下述條件的所有t篩選出來,也就可以得到一個候選集合:
也就是在P[j]的前綴P[0,j)中,所有匹配真前綴和真後綴的長度。
在發生一次失配時,也只有來自這個集合中的t,纔有資格來作爲下一輪的對齊位置。
而next表,其實就是:
那麼我們怎麼具體怎麼求解next數組呢?這裏不妨採用遞推策略:
分析不難得出如下結論:
當且僅當P[j]和它的替代者P[ next[j] ]相等時等號成立。
也就是說當模式串的某一字符和它的繼任者在這一位置相等時,如上圖,那麼next[j+1] = next[j] +1;(最長匹配長度變長了一位)
那麼如果二者不相等呢?怎麼去遞推?
我們還是按照原有思路,當一次失配發生,我們調用next數組中的對應值,往最大匹配的位置上滑動。那麼如果還是不匹配,我們就在新的模式串位置再次調用next中的值,即next[ next[ j ] ]。這個過程可能持續多步,直到匹配爲止。見下圖:
因爲next[ j ]是表示真前綴和真後綴中的最長匹配長度,故next[j]<j(嚴格小於) ,故上面這個候選序列只會嚴格遞減,直到下圖中的最後面的情況:
在這種情況下,通常會出現問題。因爲接下來和P[j]比對的那個字符根本就無從談起。這時候就是哨兵大顯身手的時候了,KMP巧妙的藉助了"哨兵"。即讓next[0]=-1;在模式串的最前面加上-1的通配符,它和任意字符都可以匹配。因此每當第一個字符不能匹配時,我們就用哨兵來匹配。
分析上述過程,我們發現這其實就是模式串不斷自匹配的過程。
代碼實現
分析完可以開始寫代碼了,事實上,求next數組的代碼和KMP代碼幾乎一模一樣。差別在於要設置個哨兵,以及只傳一個模式串參數。我們只需要記住其中一個就夠了。
根據《數據結構(C++版)》KMP算法的僞代碼可以用如下僞代碼表述:
1. 在串S和串T中分別設置比較的起始下標i和j;
2. 重複下述操作,直到S或T的所有字符均比較完畢;
2.1 如果S[i]等於T[j],繼續比較S和T的下一對字符;
2.2 否則將下標j回溯到next[j]的位置,即j = next[j];
2.3 如果j等於-1,則將下標i和j分別加1,準備下一趟比較;
3. 如果T中所有字符均比較完畢,則返回匹配的i-j;
否則返回-1;
至此,我們搞清楚了算法思路,以下是代碼:
//求Next數組
void getNext(char * p, int * next)//只需要傳入模式串,模式串不斷自匹配,既作爲母串,又作爲匹配串
{
next[0] = -1;//初始化哨兵
int i = 0;
int j = -1;//j代表下方模式串中的最右匹配位置,初始化爲哨兵位置
while (i < strlen(p))
{
if (j == -1 || p[i] == p[j])
{
++i;
++j;
next[i] = j;//當前位置匹配,next[j+1]=next[j]+1
}
else
j = next[j];//將當前位置更新爲next[j],再次比對
}
}
//KMP算法
int KMP(char * t, char * p)
{
int i = 0;
int j = 0;
while (i < strlen(t) && j < strlen(p))
{
if (j == -1 || t[i] == p[j])
{
i++;
j++;
}
else
j = next[j];
}
if (j == strlen(p))
return i - j;
else
return -1;
}
也可以參考下這篇文章:https://blog.csdn.net/x__1998/article/details/79951598