【原創】通俗易懂的講解KMP算法(字符串匹配算法)及代碼實現


一、本文簡介

本文的目的是簡單明了的講解KMP算法的思想及實現過程。

網上的文章的確有些雜亂,有的過淺,有的太深,希望本文對初學者是非常友好的。

其實KMP算法有一些改良版,這些是在理解KMP核心思想后的優化。

所以本文重點是講解KMP算法的核心,文章最后會有涉及一些改良過程。

 

二、KMP算法簡介

KMP算法是字符串匹配算法的一種。它以三個發明者命名,Knuth-Morris-Pratt,起頭的那個K就是著名科學家Donald Knuth。

 

三、KMP算法行走過程

首先我們先定義兩個字符串作為示例,被匹配串 S = "abcabdabcabeab",匹配串 T = "abcabeab"。

我們的目標就是確定S中是否包含T。

 

KMP算法的核心是分析匹配串 T 的特征,看看匹配串 T 能告訴我們什么信息。我把 T 着色,如下

T = "abcabeab"

現在看起來似乎比較明顯了,三個着色點都是重復的 "ab",似乎這個 T 能告訴我們它有重復的子串"ab"可以利用。那么它們到底怎么用?先不講具體怎么用,先走一遍KMP這個過程,但是大家需要留意這個"ab"。

1.  

我們發現T在匹配成功 "abcab" 后到"e"時和S子串的"d"匹配不成功了。

這個時候我們可以得到的先驗就是目前T匹配過的S子串,就是"abcab"。而T本身就是"ab"重復的。所以"abcab"可以直接跳到第二個重復"ab"的位置,因為"abcab"中其它字符串開頭不可能產生和T對應的匹配,這是很直觀的。

因此T應該直接后移三個位置,並且用第三位的"c"和S剛才不匹配的"d"進行比較。

 

可以發現,S匹配的過程是不會回退的。因此匹配過程是S從頭到尾的一遍掃描(中間可能因為匹配成功退出),所以這個查找過程是O(N)的復雜度。

 

2.

此時的匹配串是 "ab", 它告訴我們目前匹配的是"ab"。這個不像"abcab"出現了重復"ab",所以我們知道包括S[5]的"d"之前的子串是垃圾串。因此跳過S[5]重新開始匹配T。

最后完成匹配,可見,這樣一次對S的掃描完成了對T的查找。

那么對於機器如何實現?第四節會分析。

 

四、KMP算法核心解析

對於上面的過程,我們抽離出來的話,問題的根本就是對T串重復情況的一個判定。不管S是什么串,只要對T的構造模式分析清楚就可以完成上述跳轉過程。

因此需要一個數組記錄這個T的 模式函數。

這里先給出這個T的模式函數

1.每個字母對應模式函數的值就是匹配到當前位置 i 后,下一次T開始進行比較的下標。

2.而S的移動長度為 i - F(i-1)。

對應上面兩個問題

1.比如上面的匹配到的 "abcab" ,是T匹配到 T[5] 即"e"的時候出錯的。那么我們需要查看上一個字符的模式函數值,因為上一個函數值才代表了已經匹配的串。

發現F(4)=2,說明下次比較從T[2]即"c"開始。因為"abcab"有重復"ab",第一個"ab"不需要比較。

2.而S下標移動多少呢?S下標的移動即找到T的初試位置對應的S的下標。對應上面第二張圖,S[3]和T的移動對應起來了。3的獲得就是通過上面公式 5-F(4)得到的。其實這個結果和1得到匹配位置的思路是一樣的,不過又向前移動對齊了開頭。

 

為什么如此構造這個模式函數?

就是因為F值表示了對於位置 i, T[i]有無重復,並且重復下標的位置在哪(F[i])。既然我們獲取了重復下標的位置,那么其它的相關值可以推出來了。

 

基於這個思路,再給出另外兩個T串的模式函數值,幫助大家思考。

1.

2.

如何快速構造這個模式函數?

這個留給大家思考一下了,應該也比較直接了,注意是查找重復位置。如果不大明白,可以參考下面的代碼。

 

五、KMP算法實現

 再添加個例子幫助大家思考:

S1 = "aaaaaaaaaaaaaaaab"  S2 = "aaaaafaaaaaaaaab"   T = "aaaab"

對於S1和S2兩種情況,應如何匹配。

代碼如下:

 1 /*  2  return val means the begin pos of haystack  3  -1 means no matching substring  4 */  5 int KMP(char *haystack, char *needle) {  6 // pre-process  7 if(haystack[0] == 0 && needle[0] == 0)  8 return 0;  9 10 int i, j, k, min, cur; 11 12 //construct F(t) in vector len 13 vector<int > len; 14 len.push_back(0); 15 for(i=1; needle[i] != 0; i++){ 16 if(len[i-1] == 0){ 17 if(needle[i] == needle[0]) 18 len.push_back(1); 19 else 20 len.push_back(0); 21 }else{ 22 if(needle[i] == needle[len[i-1]]) 23 len.push_back(len[i-1]+1); 24 else 25 len.push_back(0); 26  } 27  } 28 // KMP finder 29 j = 0; 30 for(i=0; haystack[i] != 0; ) { 31 // matching 32 for(; needle[j] != 0; j++) { 33 if(haystack[i+j] != needle[j]) 34 break; 35  } 36 //finded 37 if(needle[j] == 0) 38 return i; 39 else{ // jump 40 if(j){ 41 cur = j - len[j-1]; 42 i += cur; 43 j = len[j-1]; 44 }else{ 45 j = 0; 46 i++; 47  } 48  } 49  } 50 //match failed 51 return -1; 52 }

 

六、KMP算法改進

KMP算法有一些改進版本加速查找,一般可以通過S串中的一些信息加速匹配過程。

比如若  S = "aaaaaaaafaaaaaaaaaaaab", T = "aaaaaaab"。

在查找過程中,S中間的 "f" 起到了阻擋作用。但是由於我們只是考慮T的先驗信息,遇到"f" 不匹配會導致T每次后移一步進行新的匹配,直到T的開頭碰到了"f"。

但是如果我們加入"f"這個原串S的信息,由於"f" != "b" && "f" != "a" && i-1 = F(i-1) ,所以直接跳到"f"后進行新的匹配會更快速的查找。

 

但是這些改進都是基於KMP基礎算法之上的,因此把握核心要點不僅省時省力,更能有效擴展。

 

七、參考

[1] 《字符串匹配的KMP算法》 http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

[2] 算法導論

 

轉載請注明出處~   http://www.cnblogs.com/xiaoboCSer/p/4237941.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM