【詳解】KMP算法
前言
KMP算法是學習數據結構 中的一大難點,不是說它有多難,而是它這個東西真的很難理解(反正我是這麽感覺的,這兩天我一直在研究KMP算法,總算感覺比較理解了這套算法,
在這裏我將自己的思路分享給大家,也是檢驗一下自己有沒有真正掌握這個算法,錯誤的地方也請大家指正。嚶嚶嚶~~~
註:可供參考的資料有很多,視頻的話個人推薦B站的UP主正月點燈籠,博客的話有很多,不過不要貪多,不然容易混亂,不同的人對這個算法也是有不同的理解的!
背景
了解一個算法你要明白它的出處,KMP算法是由三位大牛同時研究出來的,他們分別是D.E.Knuth、J.H.Morris和V.R.Pratt,好吧其實明白他的出處也沒太大用。。。(逃
那麽KMP算法主要用來解決哪一方面的問題呢? 主要是用來解決字符串中的模式串(通俗點說是關鍵字)在主串中的定位問題,比較通俗點說就是找你這個模式串在你這個主串的什麽位置,並把它表示出來。
暴力匹配思路
看到這你就會感覺,這不是很簡單嗎,然後我們就會萌生下面這種思路:
有兩個字符串:一個是“BDABCDBCDABCDAC”,另一個是“BCDAC”
直接從第一個字符開始比較,發現第一個字符相同,再往後找,發現第二個不相同,就把BCDAC往後移一位,從主串的第二個開始往後匹配,這不就完了??簡單粗暴!
但是其實這樣做浪費了很多的時間,簡單來說:
如果你的兩個字符串是這樣的:一個是“AAAAAAAAAAAAAAAAB”,另一個是“AB”
那麽你可以很清楚的發現一個問題,這種暴力匹配算法,真的太剛了,簡直就是鐵頭娃,那麽我們應該怎麽解決這個問題呢?這就要講到我們的重點:KMP算法
NEXT數組
求next數組是KMP算法裏面最為重要的一部分,求出這部分也就幾乎成功了一半,那麽我們求的這個數組到底是用來做什麽的呢?其實是為了找到模式串失配後的下一個匹配位置
從而省去一些不必要的操作。求解next數組就不得不說到最長前後綴問題:
next數組的各元素是用來存放模式串的最長前後綴的長度,比如ABCDABD這個模式串,我們可以把它分成:
"A”、“AB”、“ABC”、“ABCD”、“ABCDA”、“ABCDAB”、“ABCDABD”七部分,分別求得他們的最長前後綴(前後綴不包含自身)是:
“A”:0 ,“AB”:0 ,“ABC”:0 ,“ABCD”:0 ,”ABCDA“:1,”ABCDAB“:2,”ABCDABD“:0
所以我們得到的next數組為{0,0,0,0,1,2,0} 怎麽樣?很簡單易懂吧,那麽我們應該怎麽用代碼來實現呢?
一般來說我們都會把next數組的第一位設為0,因為第一個字符的最大前後綴始終為0,至於有的設為-1,雖然之前我都是按照-1做的,但那只是版本不同,我們這種的思路是比那種要清晰的。
代碼實現如下:
void get_next(const char P[],int next[]) { int i,len;//i:模式串下標;len:最大前後綴的長度 int m = strlen(P);//模式串長度 next[0] = 0;//模式串的第一個字符的最大前後綴長度為0 for (i = 1,len = 0; i < m; ++i)//從第二個字符開始,依次計算每一個字符對應的next值 { while(len > 0 && P[i] != P[len])//遞歸的求出P[0]···P[i]的最大的相同的前後綴長度len len = next[len-1]; if (P[i] == P[len])//如果相等,那麽最大相同前後綴長度加1 { len++; } next[i] = len;
} }
看完上面這段代碼以後,我們發現最難懂的地方就是上面的while循環了,至於為什麽要這樣寫呢? 你可以理解為:如果模式串ABCDABD中進行到A,我們要填next[1]時,
發現A後的這個B和前面的A不相同,那麽我們的len是不會變的,所以我們要確保它等於上一個字符的next值。
KMP算法
有了next數組,我們就可以很好地實現KMP算法了,下面給出代碼:
void kmp(const char T[],const char P[],int next[]) { int n,m; int i,q; n = strlen(T); m = strlen(P); get_next(P,next); for (i = 0,q = 0; i < n; ++i) { while(q > 0 && P[q] != T[i])//如果模式串和主串不匹配,看不懂下面會講 q = next[q-1]; if (P[q] == T[i])//如果二者匹配,q加一 { q++; } if (q == m)//如果全部匹配成功了,輸出位置 { printf("%d\n",i-m+2); } } }
那麽為什麽要寫while那一句呢?其實原因很簡單,我們的next數組是表示的每一段的最長前後綴的長度,如果失配了,我們就會返回與模式串失配位置前相同的後面那一部分,
比如說主串為”ABCABDCABCDABD“,模式串為”ABCDABD“,
當我們進行到ABC後我們發現q=4時失配了,這時我們應該返回的應該是next[q-1],即它前一位的next數組,即next[3],即標紅色的那一部分,從那再開始進行,
也就是應該進行 "ABCDABD",這樣以此類推,仔細想想是不是這樣,這一段和next數組都是比較難理解的,但也是最關鍵的。
總結
相信各位巨巨在看完以上講解以後也基本理解了KMP算法,把它從頭到尾想一遍,發現其實它也不是很難,無非就是找一個next數組和進行一次KMP查找而已,接下來我們分析一下KMP算法的時間復雜度:
假設現在主串T匹配到 i 位置,模式串P匹配到 q 位置
- 如果q>0並且P[q] ! = T[i],即匹配失敗那麽q=next[q-1],模式串也就相當於主串向右移動了q-next [q-1] 位。
- 如果P[q]==T[i],表示匹配成功,那麽q++,往後移。
我們發現如果某個字符匹配成功,模式串q++;如果匹配失配,i 不變(即 i 不回溯),模式串會跳過匹配過的next [q-1]個字符。
當然我們做最壞的打算,當模式串首字符位於i-(q-1)的位置時才匹配成功,算法結束。
所以,如果主串的長度為n,模式串的長度為m,那麽匹配過程的時間復雜度為O(n),加上計算next的O(m)時間,KMP的整體時間復雜度為O(m + n)。
代碼
#include<stdio.h> #include<string.h> void get_next(const char P[],int next[]) { int i,len;//i:模式串下標;len:最大前後綴的長度 int m = strlen(P);//模式串長度 next[0] = 0;//模式串的第一個字符的最大前後綴長度為0 for (i = 1,len = 0; i < m; ++i)//從第二個字符開始,依次計算每一個字符對應的next值 { while(len > 0 && P[i] != P[len])//遞歸的求出P[0]···P[i]的最大的相同的前後綴長度len len = next[len-1]; if (P[i] == P[len])//如果相等,那麽最大相同前後綴長度加1 { len++; } next[i]=len; } } void kmp(const char T[],const char P[],int next[]) { int n,m; int i,q; n = strlen(T); m = strlen(P); get_next(P,next); for (i = 0,q = 0; i < n; ++i) { while(q > 0 && P[q] != T[i])//如果模式串和主串不匹配,看不懂下面會講 q = next[q-1]; if (P[q] == T[i])//如果二者匹配,q加一 { q++; } if (q == m)//如果全部匹配成功了,輸出位置 { printf("%d\n",i-m+1); } } } int main() { int i; int next[20]={0}; char T[] = "ABCABDCABCDABD"; char P[] = "ABCDABD"; printf("主串:%s\n",T); printf("模式串:%s\n\n",P ); // get_next(P,next); printf("位置:"); kmp(T,P,next); printf("\n"); printf("next數組:\n"); for (i = 0; i < strlen(P); ++i) { printf("%d ",next[i]); } printf("\n"); return 0; }
後記
花了兩個多小時,終於是打完了,KMP的講解就到這裏了,關於KMP的各項優化這裏也就不再多說,感興趣的話可以去baidu搜索BM算法和Sunday算法,
如果發現上文有什麽錯誤之處,還請隨時指正,謝謝!
------------BY 孑、然---------------
--------2018.8.18 11:01-----------
--------------------------------------
【詳解】KMP算法