kmp演算法對bf演算法做了很大改進,是由克努特(knuth),莫里斯(morris),普拉特(pratt)同時發現。
簡單的模式匹配方法簡稱為bf演算法。
假設m,n是兩個串,m為主串,n為子串。在m中找到等於n的子串,則匹配成功,函式返回子串在主串首次出現的儲存位置,否則匹配失敗。(申明:本演算法中字串採用定長順序儲存,串的長度引數存放在0單元,串值從單元1開始儲存,字元序號與儲存位置一致)
簡單例項如下:
m:aacbcbaadbcbba
n:cbaa
用子串n的第乙個字元與主串m中的字元挨個對比,直到找到相同的字元,之後挨個比較是否相同,如果全都相同,那麼匹配成功,如果出現不相同的字元那麼再迴圈以上過程。
例項步驟如下:
第一趟:a
acbcbaadbcbba
(子串首字元在主串中挨個比較)
了l c
baa第二趟:a
acbcbaadbcbba
(上一趟比較不成功,則比較後乙個字元)
cbaa
第三趟:aa
cbcbaadbcbba
(比較成功,挨個比較後面的字元是否也相同)
cbaa
第四趟:aac
bcbaadbcbbacb
aa第五趟:aacb
cbaadbcbba
(後面的字元出現不行同的,則再次用子串首字元在主串中比較)cba
a第六趟:aacbc
baadbcbba
cbaa
第七趟:aacbcb
aadbcbba
cbaa
第八趟:aacbcba
adbcbbacb
aa第九趟:aacbcbaa
dbcbbacba
a第十趟:
aacbcbaa
dbcbba
(迴圈以上過程,最終找到匹配的子串)
cbaa
c語言實現如下:
int search_chuan(char m,char n)//模式匹配bf演算法,主串m,子串n
}if (j==n[0]+1)//兩種出迴圈的狀態,正常迴圈結束j指向子串末尾,非正常出迴圈j不會指向子串末尾}}
return 0;
}
bf演算法雖然簡單但是效率較低,造成bf演算法速度慢的原因主要在於,當子串後續匹配失敗時,將返回首字元匹配成功位置的下乙個位置,而這些回溯過程中有些是不必要的。
例如:在上面的例項中
第四趟:aac
bcbaadbcbbacb
aa第五趟:aacb
cbaadbcbbacba
a第六趟:aac
bcbaadbcbba
cbaa
第五趟匹配失敗後,回溯到首字元匹配成功位置的下一位置,第五趟表明子串前半部分cb是匹配的,所以回溯後首字元c必定再次與已知不匹配的b再比較一次,所以第六趟是不必要的,kmp演算法最大程度的避免了這樣的回溯,提高了執行效率。
綜上所述,在上述例項中我們希望在匹配失敗之後,不回溯,子串首字元比較的位置向後劃,跳過已知不會匹配成功的b字元。觀察上述例項可以發現,當子串中有相同字元段或相同字元時,若這一段字元已經匹配成功,那我們就知道主串上的相同位置的字元,我們希望利用這個已知資訊來避免回溯。
現在解決問題的關鍵在於比較的位置向後移動多少,在用例項進行推導時可以發現(這裡不再引入例項,有興趣可以自行研究),子串向後划動的長度與子串有關,這個演算法的關鍵就是確定上述關係,問題的數學模型建立如下。
設主串下標為i,子串下標為j,子串划動的最終位置為k,那麼k位置之前的字元段是相匹配的,所以有以下關係式成立
「n1,n2,n3,n4......nk-1」=「mi-k+1,mi-k+2,mi-k+3......mi-1」①
等號左邊是子串在主串中相匹配的字元段,等號右邊是匹配字元段在主串中的位置。
匹配失敗是在nj和mi處,所以得到的部分匹配結果為:
「n1,n2,n3,n4......nj-1」=「mi-j+1,mi-j+2,mi-j+3......mi-1」②
因為k「nj-k+1,nj-k+2,nj-k+3......nj-1」=「mi-k+1,mi-k+2,mi-k+3......mi-1」③
等號左邊為子串的前k-1項,等號右邊為主串的前k-1項
由①和③得:
「n1,n2,n3,n4......nk-1」=「nj-k+1,nj-k+2,nj-k+3......nj-1」④
結論:當在mi,nj時匹配失敗,如果子串滿足④式,那麼子串n可以向右划動至nk位置與主串對準,再繼續匹配。
上述過程實在太過於晦澀難懂了(我自己也是用例項推導了很多遍才勉強理解),並且很難理清(子串向後划動的長度與子串有關)這個關係,下面我引入另乙個研究過程和方法。
下面先解釋幾個詞彙:
字串字首:乙個字串除了最後乙個字元以外,其前面所有字元組成的字串的所有子字串的集合
字串:btboay
字串字首:「[b],[bt],[btb],[btbo],[btboa]」
字串字尾:乙個字串除了第乙個字元以外,其後面所有字元組成的字串的所有子字串的集合
字串:btboay
字串字尾:「[t],[tb],[tbo],[tboa],[tboay]」
部分匹配字元段:主串與子串相匹配的一段字元
主字串:btboay
子字串:boy
部分匹配字元段:bo bt
boayboy
部分匹配值:字串的字首和字尾最長的共有元素長度
字串:btboay,其子字串的部分匹配值為
——「b」的字首和字尾都為空集,共有元素長度為0
——「bt」的字首為[b],字尾為[t],共有元素長度為0
——「btb」的字首為[b,bt],字尾為[tb,b],共有元素長度為1
——「btbo」的字首為[b,bt,btb],字尾為[tbo,bo,o],共有元素長度為0
——「btboa」的字首為[b,bt,btb,btbo],字尾為[tboa,boa,oa,a],共有元素長度為0
——「btboay」的字首為[b,bt,btb,btbo,btboa],字尾為[tboay,boay,oay,ay,y],共有元素長度為0
移動位數=已匹配字元數-對應的部分匹配值
了解以上的基本概念後,下面列舉幾個例項來解釋上面的計算式的由來:
例項一:
主串m:abcdeabcdf
子串n :abcdf
我們知道按照bf演算法的思想,在比較到f處中斷:
abcd
eabcdf
abcd
f子串後移一位:ab
cdeabcdf
abcdf
但實際上我們可以將子串直接移到這裡:
abcd
eabcdf
abcdf
為什麼我們敢確定前面的字元不會出現匹配的情況:在第一輪的匹配中前面的abcd字元是匹配的,很明顯這中間不可能有字元與首字元a相匹配,那是不是我們直接將子串移到下乙個出現首字元a的地方就可以呢?
請看下面的例子:
例項二:
主串m:abadeabadf
子串n :abadf
第一輪失敗:
abad
eabadf
abadfab
adeabadf
abadf
這樣是可以的,簡化了bf演算法,但這樣是最佳嗎?在首輪匹配失敗後,已知匹配成功的字元為abad,在上述移動後,我們發現,其實我們已經知道第二個a後面是d,與b是不匹配的。
可以直接移動到這裡:
abad
eababf
ababf
看完這兩個例子,有人會想是不是直接移到匹配失敗的位置就好了?(呵呵,不要犯傻)
再看下面的例子:
例項三:
主串m:ababeababf
子串n: ababf
在字元f處匹配失敗後,子串可以直接移到這裡:
ababea
babf
abab
f主串m:abababf
子串n: ababf
在字元f處匹配失敗後,子串可以直接移到這裡:aba
babf
ababf
例項三的結果可以發現移動後有一些規律,可以用下圖來表示:
可以看到在已匹配的字元中a,b段是相同的,而這是已匹配字元段的字尾和字首的最長共有元素(也就是部分匹配段),而子字串移動位數k=已匹配字元長度-部分匹配值,這就是子字串移動位數計算式的由來。
如果我們提前將所有子字串的部分匹配值計算出來,就可以知道匹配失敗後的移動位數,所以在kmp演算法中我們建立next值表來儲存這些值(next值就是部分匹配值加1)。
下面我們利用例項詳細介紹求next值表的演算法:
子串(模式串):ababf
①:在字元a之前沒有字元,所以預設為0
②:在字元b之前只有字元a,無字首和字尾,所以next值為1
③:在字元a之前有字元ab,字首為a,字尾為b,無公共子串,所以next值為1
④:在字元b之前有字元aba,在步驟③中做了a和b的比較,所以不再需要比較,此時只需要比較新字元a和首字元a是否相同,在此時的例項中是相同的,所以next值為上乙個字元的next值加1,所以現在的next值為2
⑤:在字元f之前有字元abab,在步驟④已知有乙個相同的字元,此時移位與新字元b比較,發現依然相等,所以next值為3
依次類推
計算next值的函式c語言實現如下:
void next(char s,int next)
}if (j==n[0]+1)//兩種出迴圈的狀態,正常迴圈結束j指向子串末尾,非正常出迴圈j不會指向子串末尾
else}}
return 0;
}
模式匹配 KMP演算法
字串匹配演算法 include includeusing namespace std define ok 1 define error 0 define overflow 2 typedef int status define maxstrlen 255 使用者可在255以內定義最長串長 typed...
模式匹配KMP演算法
前些日子在為目前該學習什麼而苦惱,就問了一下已經從事多年軟體開發的表哥,他說乙個程式設計師要走的遠,就要學好資料結構和演算法,於是我就重新開始學習資料結構和演算法了 拿起以前上過的資料結構看,看到第四章串的模式匹配時,頗感興趣,就寫了一下程式,實踐了一下。感覺還蠻爽,於是就把以下幾個重要的函式放在此...
KMP模式匹配演算法
首先,這是由knuth morris和prattle三人設計的線性時間字串匹配演算法。這裡就不仔細的講解,網上有更好的講解,在這推薦幾個自己當時學習的kmp所看到的較好理解的 這裡附上自己所學的 includeusing namespace std s 是主串 p 是模式串 int next 100...