kmp算法--通俗易懂

         今天花了好幾個小時學習這個算法,擔心之後忘記,所以在這裏做些總結。也方便其它人學習借鑑。

         學習理解的過程中也看了很多帖子,但感覺說的都不是特別清楚,也對照了課本,但是大量的推理證明並不到好理解,沒有與程序結合起來講,明明已經明白的原理在教材中還要費力理解。所以文末我會推薦一篇寫的特別好的博文,希望能幫到大家。

 正文:

1. kpm算法的原理 

本部分內容轉自:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

舉例來說,有一個字符串(主串)"BBC ABCDAB ABCDABCDABDE",我想知道,裏面是否包含另一個字符串(模式串)"ABCDABD"?

許多算法可以完成這個任務,Knuth-Morris-Pratt算法(簡稱KMP)是最常用的之一。它以三個發明者命名,起頭的那個K就是著名科學家Donald Knuth。

這種算法不太容易理解,網上有很多解釋,但讀起來都很費勁。直到讀到Jake Boxer的文章,我才真正理解這種算法。下面,我用自己的語言,試圖寫一篇比較好懂的KMP算法解釋。

1.

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一個字符與搜索詞"ABCDABD"的第一個字符,進行比較。因爲B與A不匹配,所以搜索詞後移一位。

2.

因爲B與A不匹配,搜索詞再往後移。

3.

就這樣,直到字符串有一個字符,與搜索詞的第一個字符相同爲止。

4.

接着比較字符串和搜索詞的下一個字符,還是相同。

5.

直到字符串有一個字符,與搜索詞對應的字符不相同爲止。

6.

這時,最自然的反應是,將搜索詞整個後移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因爲你要把"搜索位置"移到已經比較過的位置,重比一遍。

7.

一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是"ABCDAB"。KMP算法的想法是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。

8.

怎麼做到這一點呢?可以針對搜索詞,算出一張《部分匹配表》(Partial Match Table)。這張表是如何產生的,後面再介紹,這裏只要會用就可以了。

9.

已知空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。查表可知,最後一個匹配字符B對應的"部分匹配值"爲2,因此如果把搜索詞放到一個一維數組P[]中,(我也看過許多其它的帖子,他們一維數組有的是從0開始,有的是從1開始,誤導了我好久,最後我會把兩種代碼都列出來,大家可以對比學習,這也是我的一個弱點,咱們這裏按照從0開始,後邊求next數組是爲了對照方便,也會從0開始)應該主串不移動,恰巧搜索詞移動到P[2]位置上繼續進行匹配

10.

因爲空格與C不匹配,搜索詞還要繼續往後移。這時,已匹配的字符數爲2("AB"),對應的"部分匹配值"爲0。於是將搜索詞移動到P[0]位置上。

11.

因爲空格與A不匹配,沒有已匹配的字符,繼續後移一位。//j=next[j-1],j不能等於0

12.

逐位比較,直到發現C與D不匹配。於是繼續從搜索詞中的P[2]開始匹配。

13.2

逐位比較,直到搜索詞的最後一位,發現完全匹配,於是搜索完成。如果還要繼續搜索(即找出全部匹配),再從搜索詞的P[0]開始匹配,這裏就不再重複了。

14.

下面介紹《部分匹配表》是如何產生的。

首先,要了解兩個概念:"前綴"和"後綴"。 "前綴"指除了最後一個字符以外,一個字符串的全部頭部組合;"後綴"指除了第一個字符以外,一個字符串的全部尾部組合。

15.

"部分匹配值"就是"前綴"和"後綴"的最長的共有元素的長度。以"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。

16.

"部分匹配"的實質是,有時候,字符串頭部和尾部會有重複。比如,"ABCDAB"之中有兩個"AB",那麼它的"部分匹配值"就是2("AB"的長度)。搜索詞移動的時候,第一個"AB"向後移動4位(字符串長度-部分匹配值),就可以來到第二個"AB"的位置。


2.next數組的求解思路與後續優化(這裏我就不做過多的贅述,我們就一步到位,在求解next數組的同時一步到位,進行優化)

char p[8]="ABCDABD";
int next[7]={0};
void makenext(const char p[],int next[])//注意這裏的const,與strlen函數有關
{
	int j,k=0;//模板字符串下標j,最大前後綴長度
	int m=strlen(p);//模板字符串長度
	next[0]=0;//模板字符串的第一個字符的最大前後綴長度爲0
	for(j=1,k=0;j<m;j++)
		{
		    while(k>0&&p[k]!=p[j]) k=next[k-1];//k>0保證next[k-1]有意義
		    if(p[k]==p[j]) k++;//看下圖解釋
	            next[j]=k;
	         }
}


      如果藍色的部分相同即p[k]==p[j],則當前 next 數組的值爲上一個 next 的值加一,如果不相同,就是我們下面要說的!

如果不相同,用一句話來說,就是:

從前面來找子前後綴

1如果要存在對稱性,那麼對稱程度肯定比前面這個的對稱程度小,所以要找個更小的對稱,這個不用解釋了吧,如果大那麼就繼承前面的對稱性了。

2要找更小的對稱,必然在對稱內部還存在子對稱,而且這個必須緊接着在子對稱之後。

如果看不懂,那麼看一下圖吧!

正文結束,下面來看代碼:

模式串數組從1開始:

p[1].........p[k] p[k+1]  ..............      p[j-k]........p[j-1]p[j]

模式串數組從0開始:

p[0].........p[k-1] p[k]    ............      p[j-k]........p[j-1] p[j]


//該代碼從0開始
<span style="color: rgb(51, 51, 51); font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);">  #include <iostream></span>
#include <string>
using namespace std;

void makenext(const char p[],int next[])//注意這裏的const,與strlen函數有關
{
	int j,k=0;//模板字符串下標j,最大前後綴長度
	int m=strlen(p);//模板字符串長度
	next[0]=0;//模板字符串的第一個字符的最大前後綴長度爲0
	for(j=1,k=0;j<m;j++)
		{
			while(k>0&&p[k]!=p[j]) k=next[k-1];//k>0保證next[k-1]有意義
		    if(p[k]==p[j]) k++;//如果前後
	        next[j]=k;
	     }
}
int kmp(char s[],char p[],int next[],int pos)
{
	int n=strlen(s);//主串s的長度
	int m=strlen(p);//模式串p的長度
	makenext(p,next);
	for(int i=pos,j=0;i<n;i++)
	{
		if(j>0&&p[j]!=s[i]) j=next[j-1];
		if(p[j]==s[i]) j++;//在這裏如果最後一個字符也相等,仍j++
		if(j==m)  return i-m+1;//(這裏修改後可以找出主串中所有符合條件的位置)
	}
}
int main()
{
	char s[]="ababxbababcdabddsss";
	char p[8]="abcdabd";
    int next[7]={0};
	int pos;//從主串的pos位置起
	cin>>pos;
	cout<<kmp(s,p,next,pos);
	return 0;
}
寫博文還挺花時間 的,這篇博文用了3個多小時。。。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章