KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,所以人們稱它爲克努特——莫里斯——普拉特操做(簡稱KMP算法,由他們的名字首字母組成)。html
KMP算法的關鍵是利用已經部分匹配的信息,儘可能減小模式串與主串的匹配次數以達到快速匹配的目的。算法
在介紹KMP以前先說一說樸素解法,也就是最簡單的暴力解法,樸素解法是採用窮舉的方式一個一個對比達到查找的功能,模式串與主串匹配失敗後又要從新從模式串的開頭繼續匹配,不考慮模式串已經判斷過的字符,因此效率差。數組
下面是樸素解法的實現代碼:數據結構
int sub_str_index(const char* s, const char* p) // s是主串,p是模式串 { int ret = -1; // ret記錄返回值,初始化爲-1表示沒有找到 int sl = strlen(s); int pl = strlen(p); int len = sl - pl; // len記錄主串的查找邊界,避免主串剩餘字符不足,提升效率 for(int i=0; (ret<0) && (i<=len); i++) // 若是沒有找到匹配的,且主串不會到邊界 { bool equal = true; // equal用於記錄臨時的匹配狀況,爲了下面的循環,默認爲真 for(int j=0; equal && (j<pl); j++) // 判斷當前位置主串是否與模式串徹底匹配 equal = (s[i + j] == p[j]); ret = (equal ? i : -1); // 若是模式串所有匹配成功就返回匹配主串首字符的下標,不然返回-1表示失配 } return ret; }
接下來介紹KMP算法:函數
KMP算法是利用已知的信息減小無效匹配判斷的一種算法。spa
這是阮一峯的講解,我以爲很是好,供參考:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html3d
另外youtube上的黃浩傑也講的不錯,方便的朋友能夠去看看。指針
下面這張圖來自阮一峯博客,我作了一些修改,用它來舉例子:code
通過幾回查找後,這裏模式串的D和主串的空格失配了,爲了提升查找效率,咱們發現直接把模式串向後移動4位能夠更高效的匹配,由於主串當前的3個字符都不能和模式串匹配,從而省去了前面3次匹配的過程,實現效率的提高。htm
這個例子是要右移4位,4是怎麼來的呢,是經過模式串已經匹配的最後一個字符的位置(這裏就是模式串的第6個字符B,數組下標爲5)減去模式串最長匹配數2獲得的,那2又是怎麼來的呢,2是模式串前綴與模式串後綴最多能匹配的字符個數,也就是部分匹配值
首先咱們要先搞懂什麼是前綴、後綴、部分匹配值以及部分匹配表
拿上面的模式串"ABCDABD"來講
前綴就是取從開頭到任意一個字符的子串;後綴則是取任意一個字符到末尾的子串;固然長度爲0或者爲模式串的總長度就沒有意義了,因此這兩個長度不計算
下面這幅圖是在部分匹配成功後又匹配失敗的部分匹配值計算演示圖;
如今假設須要匹配第7個字符D(數組下標爲6),注意D左邊的B和A都匹配成功了,這時候直接使用最後一個匹配字符也就是前一個字符B(第6個字符)在前綴中的位置的部分匹配值;最後一個匹配字符是第6個字符B,它所對應的前綴位置經過它本身的部分匹配表記錄了(也就是2),這就是說這個字符B和模式串第2個字符(數組下標爲1)是對應的,第二個字符B的部分匹配值爲0,因此不能匹配的第7個字符的部分匹配值也是0;
最長匹配字符數就是最終的部分匹配值
部分匹配值就是某個後綴最多與前綴匹配的字符個數,上面這張圖能夠看到前綴是A開頭,因此後綴也得是A開頭才能匹配,這時候有ABD這個後綴能匹配,再貪婪一點,還能匹配更多嗎,發現最多隻能匹配2個字符,因此它的部分匹配值就是2;
部分匹配表是記錄部分匹配值的一個數據結構,由於咱們能匹配的模式串長度是不肯定的,因此須要針對每一個位置生成一個部分匹配值
這裏就要用到KMP算法的部分匹配表了,部分匹配表用於記錄模式串前綴與模式串後綴最多能匹配的字符個數;經過這個計數咱們就能知道移動的位數了,由於它記錄了當前字符最長匹配的字符個數,因此得出下面的公式:
移動位數 = 已匹配的字符數 - 對應的部分匹配值
使用計算公式咱們就能夠計算出右移的位數,已匹配字符數爲6(已匹配ABCDAB),最後一個匹配的字符B的部分匹配表爲2,因此右移6-2=4位.
網上不少人把這個部分匹配表寫成next數組,我這裏根據老師的代碼寫成了int類型的指針,用堆空間記錄,長度與模式串長度一致,每一個單位爲int類型,等效於int數組。
int* make_pmt(const char* p)· // 部分匹配表生成函數,給定模式串指針做爲參數,返回int*記錄部分匹配表,可充當數組使用,長度爲模式串長度;參數合法性由調用者判斷 { int len = strlen(p); int* ret = static_cast<int*>(malloc(sizeof(int) * len)); // 申請堆空間用於記錄部分匹配表,使用者須要釋放 if( ret != NULL ) // 只有堆空間申請成功才能操做 { int ll = 0; // ll==>longest length,最長部分匹配值,初始化最長部分匹配值爲0 ret[0] = 0; // 第一個元素沒有匹配的因此直接寫爲0 for(int i=1; i<len; i++) // 從第二個元素開始遍歷 { while( (ll > 0) && (p[ll] != p[i]) ) // 若是已經有匹配過的字符,可是下一個字符不匹配 { ll = ret[ll-1]; // 把ll重置爲模式串的第ll個字符的部分匹配值,數組下標從0開始因此要減1,p[11-1]的部分匹配值存儲於ret[ll-1]; } if( p[ll] == p[i] ) // 每匹配成功一個字符ll就遞增 { ll++; } ret[i] = ll; // 進入下一輪循環前要把ll值保存進部分匹配表,即ret[i]記錄p[i]的最長部分匹配表 } } return ret; }
下面是kmp函數:
int String::kmp(const char* s, const char* p) // s主串,p模式串 { int ret = -1; int sl = strlen(s); int pl = strlen(p); int* pmt = make_pmt(p); // 獲取子串的部分匹配表,注意該函數返回的是堆空間,用完須要釋放 if( (pmt != NULL) && (0 < pl) && (pl <= sl) ) // 只有當部分匹配表獲取成功、模式串長度大於0且不超過源串長度的時候才須要計算 { for(int i=0, j=0; i<sl; i++) // for初始化i和j變量,i用於遍歷每一個主串的字符,j用於記錄已匹配的模式串字符數 { while( (j > 0) && (s[i] != p[j]) ) // 有已匹配字符可是後續字符又匹配失敗時 { j = pmt[j-1]; // 移動模式串,使模式串第一個字符對齊到主串下一個匹配的字符位置,這是一次性移動,再也不是暴力搜索中的每次只移動一次 } if( s[i] == p[j] ) // 若是匹配成功,記錄匹配模式串字符個數的j加1 { j++; } if( j == pl ) // 若是當前已匹配的模式串字數與模式串長度相等說明匹配成功 { ret = i + 1 - pl; // 返回匹配源串的第一個字符的下標並跳出循環結束尋找;此時i指向的是匹配的主串的最後一個字符,減去子串長度後等於第一個匹配的字符的前一個,因此要加1日後挪一個 break; } } } free(pmt); // 記得釋放部分匹配值生成函數申請的堆空間 return ret; }