KMP演算法詳解

2022-07-12 08:15:10 字數 3270 閱讀 7476

串的模式匹配可以說是非常常見的演算法了,我們首先分析最簡單易懂的蠻力演算法(brute-force,簡稱bf),然後分析蠻力演算法存在的問題以及如何改進,引出kmp演算法,然後對kmp演算法進行分析,關於蠻力演算法和kmp演算法我都會給出**,這些**都是我經過測試可以正確執行的,當然如果有疏漏或者沒有考慮到的地方還請讀者批評指正。

這裡要引入三個概念:

對於t中的每乙個字元,都要作為一次對齊位置進行比較,當t[i]p[j]時,i和j同時加一,當不相等時,要返回當前對齊位置的下乙個對齊位置進行比較。我知道這樣說有點繞,接下來我們看乙個實際例子。

我們設t="chillchia",p="chia",那麼最初r=0,i=0,j=0。因為t[0]p[0],於是i++和j++,然後t[1]==j[1],以此類推,知道t[3]!=p[3],注意,此時需要從r的下一位置作為對齊位置重新進行比較,那麼需要執行r++,i=r處開始比較,直到匹配成功或者t串到達末尾,匹配失敗,上述思想對應的**如下:

int match(char *t,char *p){

int i=0,j=0;

for(i=0;i接下來我們分析一下這個演算法的時間複雜度和空間複雜度,由於這個演算法並沒有以陣列或者迴圈的形式申請大量的空間,所以它的空間複雜度只是o(1),就不再深入討論了。

在最壞的情況下,我們假設t="000……00001",p="00001",設m=strlen(p),顯然,這裡m=5。每一次比較都需要經歷m-1次成功和一次失敗,所以對於p中的前(n-m)個字元都需要經過m次對比,由於t中共有n=strlen(t)個字元,而且一般情況下,我們認為n>>m>>2(這裡的2代表常數),所以n-m和n可以近似相等(在封底意義下),所以前n-m個字元和n個字元是相同量級的。所以時間複雜度應該為o((n-m)m)=o(nm);

這裡我們討論乙個記憶力和預知力的問題,事實上,當p串經過m-1次成功的比較之後,它已經記下了一些t串和p串中的對應資訊,因為是成功的比較,所以此時t串和p串對應的值相等,我們只考察p串就可以了。我們需要考慮這樣一種情況,當p串中的某乙個位置和t串比較發現不相等時,是否需要從下乙個對齊位置開始,從頭比較?事實上,這時我們已經記下了一些資訊,完全不必從頭開始,那麼我們應該從何處開始呢?這就是說我們如何有效利用我們已經記下的這些資訊呢?kmp演算法正是為了解決上述問題應運而生的,下面我們進入kmp演算法。

上面已經分析過,由於t串和p串已經比較的部分是相等的,這裡我們只需考察p串即可。這裡我們通過乙個實際例子來分析,設t="chichichichia",p="chichia",可以看到,當i=6,j=6時,c和a進行比較,不相等,那麼按照蠻力演算法,需要從j=0開始從頭比較,但是根據kmp演算法,這裡我們可以從j=3,也就是p串的'c'處和i=6進行比較,這樣的話,實際上i處不需要返回到對齊位置重新比較,可以一次縮排多個位置同時不需要很多重複比較。那麼這裡的第乙個問題就是,我們如何知道當j=6也就是p串的p[6]發生比較失敗的失敗返回到j=3處繼續執行呢?這裡設計乙個查詢表,我們稱之為next表,那麼next表如何構造呢?這其實是p串的乙個自相似問題,以當前的p串為例,對於發生故障的點,也就是j=6處,需要保證它的字尾和字首完全相等,而且在相等的字首和字尾中取最長的,這樣可以保證沒有遺漏,也就是演算法的正確性得以保證,比方說這裡的p串,當j=6時發生比較失敗,它的字首是chi,字尾也是chi,而且是最長的字首和字尾完全相等的情況,所以我們可以從字首+1的位置,也就是j=3的位置繼續比較(因為j從0開始,所以字首+1是j=3)。

通過上面的分析我們知道,kmp演算法的乙個核心就是構建字首表,在給出具體**之前,我們先分析一下實現的原理,按照遞推的方式,假設我們已經知道next表對應0到j的值,那麼可不可以據此算出next[j+1]呢?事實上有這樣的不等式成立:next[j+1]<=next[j]+1,這個不等式的證明讀者可以自己思考下,這裡我就不細緻介紹了。看到這個不等式,我們首先要問,什麼時候取等號?不難發現,當p[j+1]==p[next[j]+1]時,字尾和字首在相等的基礎上長度延長了乙個單位,那麼當這2者不相等呢?那就無法繼續延長,甚至要從j的上乙個字首處繼續比較直至可以延長,具體來說就是去試探p[j+1]與p[next[next[j]]+1]是否相等,讀者可以畫乙個圖就比較清晰了,如果讀者能夠理解這部分內容,那麼接下來的**也就很好理解了。

int* buildnext(char *p){

int s=strlen(p);

int j=0;

int *n=new int[s];

int t=n[0]=-1;

while(j有了字首表的構建演算法,我們接下來分析kmp演算法的總體框架。其實關於設計思路我在前面已經介紹過了,這裡不再贅述,直接看**。

int match(char *t,char *p){

int* next=buildnext(p);

int i,j;

int n=strlen(t);

int m=strlen(p);

for(i=0,j=0;i這個**我在測試的時候發現乙個bug,這裡一併記錄一下,當時我沒有引入變數m和n,而是在m和n處分別用strlen(t)和strlen(p)代替,然而結果是當j<0時迴圈就退出了,這顯然和我期望的不一致,其實這是乙個很簡單的問題,我自己編碼經驗不足才沒發現。事實上,參看c庫的文件可以得知,strlen這個函式返回乙個size_t型別的變數,這個size_t其實是typedef unsigned int size_t,所以在進行不等式判斷j考察這樣一種特殊情況,我們假設t="0001……00001",p="00001",這裡構造的next表為"-1,0,1,2,3,4",當p[3]和t[3]第一次進行比較時,回逐漸調到p[2],p[1],p[0],結果不出預料,都會比較失敗,這裡的問題是,kmp演算法不斷的在犯相同的錯誤,也就是當1與0不相等時,繼續拿上乙個0與1比較,或者我們可以說,kmp演算法沒有在錯誤中吸取經驗。改進的策略是,在構造字首表時,只有當當前字元與p[t]對應的字元不相等時,才設定對應的字首,否則繼續向上一級next表項探索,**如下所示。

int* buildnext(char *p){

int s=strlen(p);

int j=0;

int *n=new int[s];

int t=n[0]=-1;

while(j這裡我們看到,i指向當前t中的字元,對於t中的每乙個字元,i始終沒有後退,不像蠻力演算法那樣,i還要回退到對齊位置,這裡每乙個位置的i都至多進行2次比較。再考慮構建next的時間開銷,可以得知它的演算法和kmp的主演算法類似,而且文字的長度為n,因此我們可以得知演算法的時間開銷不會超過2n,在封底估算的情況下,我們可以得知演算法的時間複雜度為o(n)。其實回到蠻力演算法,通過大量的統計資料表明,蠻力演算法的時間開銷一般也就是n這個量級,只有當演算法對應的字符集較小時,蠻力演算法的效能才下降得很明顯,所以我們可以看到,很多書在舉例的時候都採用0-1構成的串。

ok,至此,本篇部落格就結束了。

KMP演算法詳解

模式匹配的kmp演算法詳解 這種由d.e.knuth,j.h.morris和v.r.pratt同時發現的改進的模式匹配演算法簡稱為kmp演算法。大概學過資訊學的都知道,是個比較難理解的演算法,今天特把它搞個徹徹底底明明白白。注意到這是乙個改進的演算法,所以有必要把原來的模式匹配演算法拿出來,其實理解...

KMP演算法詳解

kmp演算法即knuth morris pratt演算法,是模式匹配的一種改進演算法,因為是名字中三人同時發現的,所以稱為kmp演算法。因為偶然接觸到有關kmp的問題,所以上網查了一下next陣列和 nextval陣列的求法,卻沒有找到,只有在csdn的資料檔案裡找到了next陣列的簡單求法 根據書...

KMP演算法詳解

相信很多人 包括自己 初識kmp演算法的時候始終是丈二和尚摸不著頭腦,要麼完全不知所云,要麼看不懂書上的解釋,要麼自己覺得好像心裡了解kmp演算法的意思,卻說不出個究竟,所謂知其然不知其所以然是也。經過七八個小時地仔細研究,終於感覺自己能說出其所以然了,又覺得資料結構書上寫得過於簡潔,不易於初學者接...