字串匹配任務,即從乙個主串中找到與模式串相同的部分,並且返回它的位置,可以通過遍歷的方法暴力實現,成為bf演算法,即 brute force ,暴力演算法。暴力演算法是這樣實現的 :如果主串中的一部分已經匹配好了模式串中的一部分,那麼當下乙個char不匹配的時候,就要從頭來過,也就是把主串和模式串對齊的位置,也就是模式串的第乙個char的位置往右移動一格,然後對齊,重新比較。這樣的話,對於主串中比較的那個指標來說,實際上有個回溯的過程。所謂的kmp演算法是對暴力求解的一種改進。kmp的基本想法是,既然我們已經匹配成功了很多字元,那麼即使後面的失配,前面得到的資訊還是有用的,它的用途就是讓我們在把模式串往右移動的時候可以避免錯過匹配點地較大幅度的移動。
kmp演算法得名於donald knuth、vaughan pratt、james h. morris三個人。暴力匹配的最壞的時間複雜度為o(m×n),其中m和n分別是子串和主串的長度,而kmp可以做到o(m+n)。比如說,下圖這種情況,
已經匹配了四個了,但是最後乙個失配,如果按照暴力演算法,則要從主串的5號位置開始,把主串的5號與模式串的0號對齊,然後依次向後比較。而我們發現,實際上並無需如此,因為前面的四個已經匹配好了,有沒有可能直接吧子串右移一定的數量,而不是僅僅移動一格,但是還能保證我們的移動沒有錯過可能匹配上的情況呢?這就引出了所謂字首和字尾的概念。
首先,介紹概念。比如對於乙個序列:
excited
它的字首包括:e, ex, exc, exci, excit, excite
它的字尾包括:d, ed, ted, ited, cited, xcited
當然這個詞的字首和字尾沒有公共的元素。另舉乙個栗子:
excitex這個串裡面,公共的前字尾就是ex,長度為2,而像這個:
eexee這個裡面,字首e和字尾e相同,ee和ee相同,eex和xee不同了,所以最長的長度為2
為什麼要討論乙個串中的前字尾有沒有相同呢?原因如下:由於當我們匹配了一部分的時候,實際上我們知道子串中的那一部分是和主串中的對應位置一樣的。那麼我們希望進行乙個預處理,即我們假設在某乙個位置失配了,那麼前面匹配的那部分,也就是子串的子串中,前字尾公共長度最長是多少我們可以預先計算並記錄下來。記錄這個值的作用在於,比如上圖的情況,abab,這個值為2,那麼我們就可以直接用主串的8號和子串的2號相匹配,如下圖:
原因很顯然,在這一段裡,我們跳過了模式串頭上的兩個元素,因為我們知道,主串中對應位置和模式串中的abab是一樣的,而模式串中字首和字尾的兩位也是一樣的,那麼主串中的6,7號位置也就是和模式串中的前兩位是一樣的。所以頭兩個已經匹配好了,直接從第三個開始匹配就行。那有個疑問,這樣一來沒有錯過其他可能的匹配點嗎?答案是沒有,因為假如中間有其他的,那麼子串應該有更長的字首和剛才已經匹配過的主串(綠色部分)的後幾位匹配,而主串的後幾位也就是已匹配的子串的後幾位,那麼換句話說,子串應該有更長的字首和自己的字尾相匹配。這是不可能的,因為我們移動的位置是最長公共前字尾的後一位,我們當前的子串上的指標之前的長度(即ab)已經是最長了。
由上可知,如果我們預先計算好模式串中每個點之前的子串的最大公共前字尾長度,並把它作為乙個和模式串等長的陣列存起來,那麼之後就會更快的匹配。這個陣列通常叫做next陣列,因為它表徵著在該位置失配後下乙個需要匹配的模式串的位置。
先看乙個next陣列的樣子:
可以看出,next陣列值是通過當前位置前子串的最長前字尾匹配長度計算的,第0號位置由於沒有前子串,所以next陣列值設定為-1,這是為了之後程式設計的方便。(所謂方便是指,假如在中間,非index=0的位置失配,那麼i不需要移動,j跳轉到next[j]繼續匹配即可,根據next陣列的定義這很顯然。但是如果第一位就失配了,那麼如果i不移動,j調到next[j],也就是next[0],假如還是0的話,則就永遠不匹配,死迴圈。因此我們要讓i在這種情況下,強行移動一格,將next[0]設為-1,然後判斷 j==-1 的時候直接進入右移,即 i++,j++,那麼j變成了0,指向了模式串開頭,i往後移動了一格,美滋滋~)
而next陣列的計算可以看做也是乙個字串模式匹配問題,其中主串就是整個模式串,子串是每乙個位置的前子串。這樣的好處就是,在計算後面的next陣列值時可以利用到前面的已經計算好的next值。演算法如下:
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""created on sat feb 3 12:03:10 2018
@author: chia
"""import numpy as np
defgetnextarray
(modestr):
# initialization
nextarray = np.zeros(shape=(len(modestr),),dtype='int32')
nextarray[0] = -1
i = 0
j = -1
# while not finish whole modestr
while i < len(modestr)-1:
if j == -1
or modestr[i] == modestr[j]:
# if first element of substring or match, check next
i += 1
j += 1
nextarray[i] = j
else:
# not match, jump to next j
j = nextarray[j]
# in the outer loop, j == -1 is a trivial condition
return nextarray
測試一下:
getnextarray('ababc')
out[96]: array([-1, 0, 0, 1, 2], dtype=int32)
和前面的結果一致。
有了next陣列,再用kmp演算法,**如下:
def
kmp_algorithm
(mainstr, modestr, nextarray):
i, j = 0, 0
# while not finish
while i < len(mainstr) and j < len(modestr):
# if first of modestr or match, check next
if j == -1
or mainstr[i] == modestr[j]:
i += 1
j += 1
else:
# if not match jump to next possible location of modestr
j = nextarray[j]
if j == len(modestr):
return i - len(modestr)
else:
return -1
用上面的栗子測試,結果為:
kmp_algorithm('bacbabababcbab','ababc',getnextarray('ababc'))
out[105]: 6
結果正確~
從上面的程式可以看出,指示主串的i是只增不減的,因此沒有回溯,因此對於從外設輸入的大資料有效,因為無需回頭重讀,一遍掃瞄即可。另外,將子串理解為右移一段距離不如直接理解成從某個點繼續匹配,這樣next陣列的意義就很明顯了~
the end
2018/02/03 15:39 pm
讓時間停著,想把話說完。 ——**人,陳粒
KMP演算法 字串匹配
kmp演算法基本思想 我們在用常規的思想做 字串匹配時候是 如 對如 字元如果 t abab 用p ba 去匹配,常規思路是 看 t 第乙個元素 a 是否 和p 的乙個 b 匹配 匹配的話 檢視各自的第二個元素,不匹配 則將 t 串的 第二個元素開始 和 p 的第乙個匹配,如此 一步一步 的後移 來...
KMP字串匹配演算法
kmp核心思想 計算模式串的next陣列,主串的索引在比較的過程中不回朔 ifndef kmp h define kmp h class kmp endif include kmp.h include include include using namespace std int kmp calcu...
KMP字串匹配演算法
在介紹kmp演算法之前,先介紹一下bf演算法。一.bf演算法 bf演算法是普通的模式匹配演算法,bf演算法的思想就是將目標串s的第乙個字元與模式串p的第乙個字元進行匹配,若相等,則繼續比較s的第二個字元和p的第二個字元 若不相等,則比較s的第二個字元和p的第乙個字元,依次比較下去,直到得出最後的匹配...