KMP字符串匹配算法(看完必懂!!!)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章