数据结构与算法一篇帮助你吃下KMP算法
模式匹配
- 什么是模式匹配,我們用一個案例來說明:
- 當S = “s1,s2,s3,s4 …sn” T=“t1,t2,t3,t4 … tn” 在字符串S中尋找T字符串的過程就是模式匹配的過程,T就說模式串,S是主串
- 實現方案:
- 暴力破解,逐字符判斷,直到找到對應的全匹配
- 由暴力破解的缺點逐步優化,引出KMP算法
暴力匹配
-
思路如下:
- 令i 是目標串S的下標, j是模式串T的下標
- 當匹配到Si == Tj 的時候,我們做 i++,j++繼續匹配下一個
- 當匹配到Si != Tj 的時候,我們需要沖頭匹配,另 i = i - j + 1, j = 0
- 其中 i = i - j + 1 就說目標串S的回朔,我們匹配過j 個字符,那么回退 j個位置到當前這次匹配的最開始的位置,并且先前移動一位。
-
根據如上分析有如下代碼:
暴力破解算法分析
- 我們用一個特殊一點的案例來解釋暴力破解算法
- 依然用i 表示目標串S:ab abc abcac bab, 用j 表示模式串T:abcac, 有如下流程
- 第一步,當Si, Tj 匹配到2 位置的時候,匹配是吧,i回朔到位置 2-2 +1 = 1,j=0
- 第二次匹配第一個字符就失敗,j=1-0+1=2
- 第三次匹配,一直匹配到i= 6 的時候才匹配失敗,這個時候,i=6-4+1 = 3,從第二個b開始匹配
- 第四次從第二個b開始,顯然是不可匹配到
- 依次一直持續到第六次匹配,才匹配成功
- 總結
- 在如上流程中,每次匹配失敗我們都回將i 回朔到當前匹配批次的下一個位置,j又從0開始比較
- 在第三次匹配結束后,我們可以發現:i=3和j=0,i=4和j=0以及i=5和j=0是不必進行的
- 關鍵點在于,我們在第三次匹配的時候,已經知道了前面的串中有ab abcab,從中我們可以得出,主串中第3,4,5個字符必然是‘b’,‘c’,‘a’,與模式串T中的 1,2,3 字符是相同的,是可以沒必要在匹配的
- 可以在第三步驟的時候,就知道,我們應該直接移動到i = 6 的位置,因為i = 6 的位置與j = 0 的位置都是 a 是想匹配的,之前的匹配過的字符已經是已知不可匹配的
- 依照如上分析:如果有一個類似的數組,用來輔助挑轉位置,那么明顯會加快進程。這樣就引出了我們的KMP算法,不回溯i,加快匹配效率。
KMP算法
-
KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人們稱它為克努特—莫里斯—普拉特操作(簡稱KMP算法)。KMP算法的核心是利用匹配失敗后的信息,盡量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是通過一個next()函數實現,函數本身包含了模式串的局部匹配信息。KMP算法的時間復雜度O(m+n) [1] (自百度百科)
-
算法流程如下:
- 如果j = -1 ,i++,j++,繼續匹配下一個字符
- 如果Si == Tj,i++, j++,繼續匹配下一個字符
- 如果Si != Tj,且j != -1,i不變,j = next[j],意味著在匹配失敗的時候,接下來模式串T要相對于主串S向右移 j - next[j] 位置。
-
那么會出現如下幾個問題:
位移信息next的確認
- 要了解next的來源,先得知道一個名字:最長公共前后綴。假設有一個串P=“P0P1P2P3…Pj-1Pj”如果存在P0P1P2…Pk-1Pk = Pj-kPj-k+1…Pj-1Pj,我們就可以說在P串中最大長度為k+1 的公共前后綴
- 找最大公共前后綴的意義:
- 例如在意思的案例中:ab abcabcac bac,我們在第三步驟中匹配到第一個abc,到第六步驟中匹配到第二個abc,在中間四,五步驟都在確認該字符:abcab,其中他的最長公共前后綴長度是,也就是ab
- 我們完全可以看出,單我們匹配到第一個abc的時候,完全可以直接跳過接下來的bc的匹配直接跳到第二個ab去匹配,那么最長公共前后綴就是這個作用,他告訴我們,在已經匹配過的字符串中,有沒有這樣的字符,我們直接跳過中間的字符,區匹配后面對于的字符ab就行了
前后綴確認
-
還是用剛才那個案例字符串P = abaabca
-
這樣公共先后在最長長度會和P的每個字符有一個對應關系:
-
我們從這個表引出next數組的值,next數組的值是除當前字符外的公共前后綴的長度,相當于將上表中數據向右移動一位,并且在前補充-1,得到如下
-
接下來我們通過next數組來進行匹配推到,如下圖流程:
-
第一步,i =0 ,j=0開始,當i = 2 ,j= 2 時候匹配失敗,那么i不動,next[j]=next[2]=0,S右移j - next[j] = 2位 , j 回朔到 0
-
第二次匹配,i= 2開始,j=0, 當匹配到i= 6 j=4 時候,失敗,還是一樣 i不動 ,j = next[j]=next[4] = 1,也就是S向右移 j-next[j]=3位
-
第三次匹配,i = 6 ,j = 1 檔i = 10, j = 5 時候匹配成功,返回 i-j=5
結論:
- 當主串S 與模式串P 匹配失敗時候,j = next[j],也就是P 右移動j-next[j]。
- 當模式串P后綴Pj-kPj-k+1…Pj-1與 主串S的Si-kSi-k+1…Si-1匹配成功,但是Pj,Sj j匹配失敗時候,因為next[j] = k,相當于在不包含Pj的模式串中有最大長度為k的相同前后綴,也就是P0P1…Pk-1 = Pj-kPj-k+1…Pj-1
- 所以我們令j = next[j],讓模式串 右移 j - next[j]位,使模式串 P0P1…Pk-1與主串。Si-kSi-k+1…Si-1對齊,讓Pk 與Si 繼續匹配,如下圖所示:
代碼實現求next數組
-
之前我們是通過推到的方式得到的next數組,當我們用代碼實現時候,需要有一定的規則
-
當next[0] = -1,next[1] = 0這個容易得到
-
那么當通用情況,當j > 1 時候,如果我們已知next[j], 那么next[j+1]的值是我們需要求解的
-
有如下兩種情況:
-
當Pk = Pj,next[j+1] = next[j] + 1 = k + 1,代表字符E 前面的模式串中有長度為k+1 的最長公共前后綴
-
當Pk != Pj時候,說明p0p1…pk-1pk != pj-kpj-k+1…pj-1pj,這個時候ABC與ABD不相同,也就是E前面的模式串中沒有長度為k+1的最大公共前后綴,所以next[j+1] = next[j] + 1 不在適用,我們需要尋找更短的最大公共前綴。
-
因為next數組存儲的是之前字符中最長公共前后綴,那么當我們pk ! = pj 的時候,我們需要找k之前的一個匹配,那么就說next[k] 的位置就是上圖中的D,我們需要匹配的是 Pnext[k] = Pj 是否成立,也就讓k = next[k] 然后在做比較。如果成立,那么next[j+1] = next[next[k]]+ 1,如果不成了,next[j+1] = 0
-
-
如上步驟中我們找k 之前的位置next[k]處的前綴來判斷是否有更短前綴,那為什么我們將k = next[k]就能找到最短前后綴,如下圖中所示
- 如上圖,在Pk!=Pj時候,k = next[k] 用 Pnext[k] 去根Pj 匹配,
- 我們可以看到,Pj之前,有一段長度為k的已匹配串,在Pk之前也有一段藍色的已匹配串,說明Pk字符前有一段長度為next[k] 的最大公共前后綴(藍色標記)
- 如果Pk != Pj,說明P0P1…Pk-1Pk != Pj-kPj-k+1…Pj-1Pj,那么我們需要向前著更短的最大公共前后綴
- 因為此時Pk和Pnext[k] 前面的藍色串已經完成了匹配
- 如果Pnext[k]能和Pj匹配,那么我們就找到了最大公共前后綴,否則我們需要再次向前找 Pnext[next[k]] 去根Pj繼續匹配,直到找到更短的最大公共前后綴,或者字符串已經找到最開始位置,那么長度就是 0
代碼實現
- 經過如上分析有如下代碼:
參考文獻
- 大佬v_JULY_v 的文章,比我更詳細
前期文章
- 上一篇:數據結構與算法–求1~n能組成的所有二叉搜索樹的排列
- 下一篇:數據結構與算法–力扣108提將有序數組轉換為二叉搜索樹
總結
以上是生活随笔為你收集整理的数据结构与算法一篇帮助你吃下KMP算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面部刮痧前涂什么最好
- 下一篇: 数据结构与算法--力扣108题将有序数组