KMP--字符串匹配
原創:http://blog.csdn.net/jiacongxu/article/details/9071491
以下內容參考了這個文章:http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm
這兩天重新看KMP,發現問題還蠻多的。以前知道KMP怎么用,復雜度如何,但是寫起來總是做不到bug-free。把這兩天重新看的,寫的關于KMP的東西,拿出來總結一下。
首先介紹下KMP:KMP的全名是Knuth-Morris-Pratt algorithm,好吧,其實就是三個作者的名字加起來。它是用來做什么的呢?字符串匹配。下面是摘下來的一段介紹:
“The algorithm of Knuth, Morris and Pratt?[KMP?77]?makes use of the information gained by previous symbol comparisons. It never re-compares a text symbol that has matched a pattern symbol. As a result, the complexity of the searching phase of the Knuth-Morris-Pratt algorithm is in?O(n).
However, a preprocessing of the pattern is necessary in order to analyze its structure. The preprocessing phase has a complexity ofO(m). Since?mn, the overall complexity of the Knuth-Morris-Pratt algorithm is in?O(n)”
?對于暴力的做法,我們枚舉母串的所有字符作為匹配的開始位置,對模式串進行匹配,假設母串長度n,模式串長度m,時間復雜度是O(nm)的。在這個暴力的過程中,我們每次匹配都“從零開始”,忽略了前面的匹配結果給我們帶來的信息,KMP算法巧妙的利用了前面的匹配結果,最大限度的避免重復的匹配,使得算法在最壞情況復雜度為O(n+m)。下面具體講講KMP算法。
KMP的第一步是利用O(m)時間,對模式串進行一個預處理。這一步完全是對于模式串的,跟母串沒有關系。前面提到,暴力做法慢的原因在于,當我已經知道模式串的前k個字符與母串匹配而第k+1個字符不匹配時,我們在母串中選用下一個字符作為匹配開始節點,對模式串從頭開始匹配。但是在前面一次失敗匹配中,我們已經知道有k個字符可以匹配,如果這k個字符符合某些特定條件,我們是不是不需要重新開始匹配,而只需要把模式串的匹配位置往前回滾幾個字符呢?答案當然是肯定的,下面先給出一個例子說明:
在上面的例子中,'d'出現了不匹配,而前面五個字符都是匹配的,按照暴力的做法,現在應該把模式串往后推一個字符,重新開始匹配。KMP算法注意到,對于模式串"abcabd",現在'd'跟母串'5'的位置出現了不匹配,而“abcab“是匹配成功的,那我們完全可以把模式串往后”推“不止一個字符,我們把它往后推三個字符,就像例子描述的,這樣,母串'5'的位置跟模式串的'c'對在一起了,而前面的"ab"仍然是匹配成功的,也就是說,用暴力做法,模式串應該往后移動一個字符,重新匹配,但KMP缺移動了多個字符,而且也不需要重頭匹配了。
為什么我們發現可以移動三個字符呢?因為對于匹配成功的串”abcab“最長的”前綴等于后綴“的串是”ab“(這里不把字符串本身當做自己的前后綴,不然最長的”前綴等于后綴“一定是串本身)。我假設大家都知道什么是前綴和后綴了(如果不知道,google一下就有了)。想象一下,既然對于現在成功匹配的串長度是len1,如果我知道最長的”前綴等于后綴“的長度為len2,那么我就可以保證把模式串往后推len1-len2個字符之后,模式串還是可以成功匹配到母串的當前位置的(在這個例子中,len1是5,len2是2,母串的當前位置是5)。為什么能保證到?因為前綴等于后綴。把模式串往后推了在匹配,就相當于拿前綴跟后綴匹配,所以,你懂的。
基本上kmp就是這個思路,我們知道當前匹配成功的字符串長度,又知道這個匹配成功的字符串的最長的”前綴等于后綴“的長度,我們就可以知道,我們應該把模式串后移多少個字符去匹配。要找到這個最長的”前綴等于后綴“的長度,KMP也告訴你了,可以用O(m)的復雜度來實現,一般來說,把這個結果存在一個叫next的數組里面。代碼如下:
[cpp]?view plaincopy
為了更直觀的看看next存了什么,用上面給的example,當模式串是”abcabd“時,對應的next為:
模式串:?a ? ?b ? ?c ? ?a ? ?b ? ?d
next: ? ? ?? -1 ? 0 ? ?0 ? ?0 ? ?1 ? ?2 ? ?0 ? ?
next指針告訴了我們,當模式串的第i個位置出現mismatch時,模式串應該跳回到那個位置(i=next[i])。也就是說,模式串后移了i-next[i]位。next[i]保存的是串p[0:i-1]最長的”前綴等于后綴“的長度。next[0] = -1可以很好的作為一個標志位,說明沒有前后綴符合要求,簡化編程難度。
拿到next數組之后,我們就需要利用next數組進行匹配,由于next數組告訴了我們當出現mismatch時要做的事情,我們只需要當match時,兩個串往后匹配,出現mismatch時,模式串的指針返回到next對應的位置即可。當模式串成功匹配到結束符時,說明找到了一個符合要求的子串。code:
[cpp]?view plaincopy
這個就是KMP的大體框架了,網上也有不少的代碼,寫法不一定相同,但是思路應該是差不多的。
最后就是證明一下為什么這個算法復雜度是O(n+m)的了(在GetNext中我們明明有兩個循環啊~~):
一. 首先看GetNext為什么是O(m)復雜度的:
首先,next[i+1]<=next[i]+1。這個用反證法很容易就證明到了,如果next[i+1]>next[i]+1,那么我取一個長度為next[i+1]-1的前綴,這個前綴自然可以作為串[0,i-1]的后綴。那么,next[i]就等于next[i+1]-1,這樣就矛盾了。
其次,對于每次while循環里面做的tmp=next[tmp],tmp的值至少減少1。這個從next本身的定義就可以推出,因為next[i]保存的是串p[0:i-1]最長的”前綴等于后綴“的長度,并且我說了,這里的前后綴不能等于串本身,所以串的前后綴的長度自然小于串本身了,所以next[tmp] < tmp。
? ? 最后,next[i]的值總是>=-1。這個結論比較容易得出了。
? ? ? ? 那么,程序的next從-1開始,每一次的for循環,next最多加1,也就是說next的最大值不超過m-1。另外,如果總的運行while循環的次數不能超過m,否則,next的值將會小于-1(這里得看到,每次tmp的值都初始化為next[i])。
所以,GetNext的while循環次數最多是m次,所以復雜度還是O(m)。
二. Search的復雜度是O(n)的:
?? 首先看到,跳出循環的條件是*s等于0,那么,如果每次循環都s++的話,循環最多運行n次。這里的問題在于,當進入else的時候,并沒有執行s++,那么,如果else運行了很多次,那么復雜度就可能不是O(n)的,我們需要證明的是,整個while循環運行過程中,else不會被進入超過n次。證明思路跟上面差不多,由于上面已經說了pos=next[pos]使得pos至少減1,而pos不會小于-1(因為等于-1時,if的條件就成立了),然后,每次進入if時,pos只會加1,這就說明,整個循環中,進入else的次數不會多于進入if的次數+1(因為pos初始化為0)。這樣,while循環最多執行2n次,復雜度是O(n)。
整個算法到這里就差不多了,貼一個code,是leetcode上面的題,標準的KMP題目:
Implement strStr()
參考代碼:
[cpp]?view plaincopy
另外要提一下,KMP可以支持wildcard,也就是說如果母串和模式串中含有類似'.'這樣,代表可以匹配任何字符的通配符,KMP也可以處理,復雜度不會有變化。其實思路很簡單,我們在判斷字符是否相等時,用的是*s==*p,如果有通配符,我們只需要把這個等價條件改成*s==*p||*s=='.'||*p=='.'即可。為了通用一點,可以傳入一個cmp函數來處理比較邏輯,不改變代碼其他部分框架:
[cpp]?view plaincopy
總結
以上是生活随笔為你收集整理的KMP--字符串匹配的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 跳槽到月薪三万的公司,但是不到半年就后悔
- 下一篇: hdu-1104-Remainder(B