轉自http://blog.csdn.net/starstar1992/article/details/54913261算法
也能夠參考http://blog.csdn.net/liu940204/article/details/51318281數組
KMP算法看懂了以爲特別簡單,思路很簡單,看不懂以前,查各類資料,看的稀裏糊塗,即便網上最簡單的解釋,依然看的稀裏糊塗。
我花了半天時間,爭取用最短的篇幅大體搞明白這玩意究竟是啥。
這裏不扯概念,只講算法過程和代碼理解:函數
字符串匹配。給你兩個字符串,尋找其中一個字符串是否包含另外一個字符串,若是包含,返回包含的起始位置。
以下面兩個字符串:測試
char *str = "bacbababadababacambabacaddababacasdsd"; char *ptr = "ababaca";
str有兩處包含ptr
分別在str的下標10,26處包含ptr。spa
「bacbababadababacambabacaddababacasdsd」;\
.net
問題類型很簡單,下面直接介紹算法code
通常匹配字符串時,咱們從目標字符串str(假設長度爲n)的第一個下標選取和ptr長度(長度爲m)同樣的子字符串進行比較,若是同樣,就返回開始處的下標值,不同,選取str下一個下標,一樣選取長度爲n的字符串進行比較,直到str的末尾(實際比較時,下標移動到n-m)。這樣的時間複雜度是O(n*m)。blog
KMP算法:能夠實現複雜度爲O(m+n)圖片
爲什麼簡化了時間複雜度:
充分利用了目標字符串ptr的性質(好比裏面部分字符串的重複性,即便不存在重複字段,在比較時,實現最大的移動量)。
上面理不理解無所謂,我說的其實也沒有深入剖析裏面的內部緣由。字符串
考察目標字符串ptr:
ababaca
這裏咱們要計算一個長度爲m的轉移函數next。
next數組的含義就是一個固定字符串的最長前綴和最長後綴相同的長度。
好比:abcjkdabc,那麼這個數組的最長前綴和最長後綴相同必然是abc。
cbcbc,最長前綴和最長後綴相同是cbc。
abcbc,最長前綴和最長後綴相同是不存在的。
**注意最長前綴:是說以第一個字符開始,可是不包含最後一個字符。
好比aaaa相同的最長前綴和最長後綴是aaa。**
對於目標字符串ptr,ababaca,長度是7,因此next[0],next[1],next[2],next[3],next[4],next[5],next[6]分別計算的是
a,ab,aba,abab,ababa,ababac,ababaca的相同的最長前綴和最長後綴的長度。因爲a,ab,aba,abab,ababa,ababac,ababaca的相同的最長前綴和最長後綴是「」,「」,「a」,「ab」,「aba」,「」,「a」,因此next數組的值是[-1,-1,0,1,2,-1,0],這裏-1表示不存在,0表示存在長度爲1,2表示存在長度爲3。這是爲了和代碼相對應。
下圖中的1,2,3,4是同樣的。1-2之間的和3-4之間的也是同樣的,咱們發現A和B不同;以前的算法是我把下面的字符串往前移動一個距離,從新從頭開始比較,那必然存在不少重複的比較。如今的作法是,我把下面的字符串往前移動,使3和2對其,直接比較C和A是否同樣。
void cal_next(char *str, int *next, int len) { next[0] = -1;//next[0]初始化爲-1,-1表示不存在相同的最大前綴和最大後綴 int k = -1;//k初始化爲-1 for (int q = 1; q <= len-1; q++) { while (k > -1 && str[k + 1] != str[q])//若是下一個不一樣,那麼k就變成next[k],注意next[k]是小於k的,不管k取任何值。 { k = next[k];//往前回溯 } if (str[k + 1] == str[q])//若是相同,k++ { k = k + 1; } next[q] = k;//這個是把算的k的值(就是相同的最大前綴和最大後綴長)賦給next[q] } }
這個和next很像,具體就看代碼,其實上面已經大概說完了整個匹配過程。
int KMP(char *str, int slen, char *ptr, int plen) { int *next = new int[plen]; cal_next(ptr, next, plen);//計算next數組 int k = -1; for (int i = 0; i < slen; i++) { while (k >-1&& ptr[k + 1] != str[i])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配) k = next[k];//往前回溯 if (ptr[k + 1] == str[i]) k = k + 1; if (k == plen-1)//說明k移動到ptr的最末端 { //cout << "在位置" << i-plen+1<< endl; //k = -1;//從新初始化,尋找下一個 //i = i - plen + 1;//i定位到該位置,外層for循環i++能夠繼續找下一個(這裏默認存在兩個匹配字符串能夠部分重疊),感謝評論中同窗指出錯誤。 return i-plen+1;//返回相應的位置 } } return -1; }
char *str = "bacbababadababacambabacaddababacasdsd"; char *ptr = "ababaca"; int a = KMP(str, 36, ptr, 7); return 0;
注意若是str裏有多個匹配ptr的字符串,要想求出全部的知足要求的下標位置,在KMP算法須要稍微修改一下。見上面註釋掉的代碼。
next函數計算複雜度是(m),開始覺得是O(m^2),後來仔細想了想,cal__next裏的while循環,以及外層for循環,利用均攤思想,實際上是O(m),這個之後想好了再寫上。
………………………………………..分割線……………………………………..
其實本文已經結束,後面的只是針對評論裏的疑問,我嘗試着進行解答的。
看了評論,你們對cal_next(..)函數和KMP()函數裏的
while (k > -1 && str[k + 1] != str[q]) { k = next[k]; }
和
while (k >-1&& ptr[k + 1] != str[i]) k = next[k];
這個while循環和k=next[k]很疑惑!
確實啊,我開始看這幾行代碼,至關懵逼,這寫的啥啊,爲啥這樣寫;後來上機跑了一下,慢慢了解到爲什麼這樣寫了。這幾行代碼,可謂是對KMP算法本質得了解很是清楚才能想到的。很牛逼!
直接看cal_next(..)函數:
首先咱們看第一個while循環,它到底幹了什麼。
在此以前,咱們先回到原程序。原程序裏有一個大的for()循環,那這個for()循環是幹嗎的?
這個for循環就是計算next[0],next[1],…next[q]…的值。
裏面最後一句next[q]=k就是說明每次循環結束,咱們已經計算了ptr的前(q+1)個字母組成的子串的「相同的最長前綴和最長後綴的長度」。(這句話前面已經解釋了!) 這個「長度」就是k。
好,到此爲止,假設循環進行到 第 q 次,即已經計算了next[q],咱們是怎麼計算next[q+1]呢?
好比咱們已經知道ababab,q=4時,next[4]=0(k=0,表示該字符串的前5個字母組成的子串ababa存在相同的最長前綴和最長後綴的長度是1,因此k=0,next[4]=0。這個結果能夠理解成咱們本身觀察算的,也能夠理解成程序本身算的,這不是重點,重點是程序根據目前的結果怎麼算next[5]的).,那麼對於字符串ababab,咱們計算next[5]的時候,此時q=5, k=0(上一步循環結束後的結果)。那麼咱們須要比較的是str[k+1]和str[q]是否相等,其實就是str[1]和str[5]是否相等!,爲啥從k+1比較呢,由於上一次循環中,咱們已經保證了str[k]和str[q](注意這個q是上次循環的q)是相等的(這句話本身想一想,很容易理解),因此到本次循環,咱們直接比較str[k+1]和str[q]是否相等(這個q是本次循環的q)。
若是相等,那麼跳出while(),進入if(),k=k+1,接着next[q]=k。即對於ababab,咱們會得出next[5]=2。 這是程序本身算的,和咱們觀察的是同樣的。
若是不等,咱們能夠用」ababac「描述這種狀況。 不等,進入while()裏面,進行k=next[k],這句話是說,在str[k + 1] != str[q]的狀況下,咱們往前找一個k,使str[k + 1]==str[q],是往前一個一個找呢,仍是有更快的找法呢? 一個一個找必然能夠,即你把 k = next[k] 換成k- -也是徹底能運行的。可是程序給出了一種更快的找法,那就是 k = next[k]。 程序的意思是說,一旦str[k + 1] != str[q],即在後綴裏面找不到時,我是能夠直接跳過中間一段,跑到前綴裏面找,next[k]就是相同的最長前綴和最長後綴的長度。(這個解釋能懂不?)
以上就是這個cal_next()函數裏的
while (k > -1 && str[k + 1] != str[q]) { k = next[k]; //k--;//也能運行 }
最難理解的地方的一個個人理解,有不對的歡迎指出。
複雜度分析:
分析KMP複雜度,那就直接看KMP函數。
int KMP(char *str, int slen, char *ptr, int plen) { int *next = new int[plen]; cal_next(ptr, next, plen);//計算next數組 int k = -1; for (int i = 0; i < slen; i++) { while (k >-1&& ptr[k + 1] != str[i])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配) k = next[k];//往前回溯 if (ptr[k + 1] == str[i]) k = k + 1; if (k == plen-1)//說明k移動到ptr的最末端 { //cout << "在位置" << i-plen+1<< endl; //k = -1;//從新初始化,尋找下一個 //i = i - plen + 1;//i定位到該位置,外層for循環i++能夠繼續找下一個(這裏默認存在兩個匹配字符串能夠部分重疊),感謝評論中同窗指出錯誤。 return i-plen+1;//返回相應的位置 } } return -1; }
這玩意真的很差解釋,簡單說一下:
從代碼解釋複雜度是一件比較可貴事情,咱們從
這個圖來解釋。
咱們能夠看到,匹配串每次往前移動,都是一大段一大段移動,假設匹配串裏不存在重複的前綴和後綴,即next的值都是-1,那麼每次移動其實就是一整個匹配串往前移動m個距離。而後從新一一比較,這樣就比較m次,歸納爲,移動m距離,比較m次,移到末尾,就是比較n次,O(n)複雜度。 假設匹配串裏存在重複的前綴和後綴,咱們移動的距離相對小了點,可是比較的次數也小了,總體代價也是O(n)。 因此複雜度是一個線性的複雜度。