近日被朋友問到了字符串匹配算法,讓我想起了大二上學期在一次校級編程競賽中我碰到同樣的問題時,爲自己寫出了暴力匹配算法而沾沾自喜的經歷。
現在想來,着實有點羞愧,於是埋頭去學習了一下KMP算法,爲了讓自己不至於那麼快忘記,也希望小夥伴們能從我的理解中收穫一點自己的感悟!
文章伴有精心雕琢的動畫以便理解。
我們首先來分析一下暴力算法,爲鮮花的誕生獻上綠葉!
以下文中統一將需要被匹配的字符串(長的那段)稱爲待匹配串 ,把用來匹配的字符串(短的那段)稱爲模式串。
暴力匹配算法的思路很簡單,就是每一次都首先將待匹配串和模式串的首字母對齊,然後比對是否相同,若相同則繼續比對兩個串的下一個位置,如果不相同的話就將模式串向右移動一位,然後再重新開始從頭匹配,就像下面這樣⬇️⬇️
從上面的動畫我們可以直觀的看出來,下面的模式串在匹配失敗之後都只會移動一格,傻里傻氣的,這就導致它的時間複雜度是,其中M是模式串的長度,N是待匹配串的長度。
對於這個時間複雜度,我不滿意!它太傻了,不符合我聰明睿智的氣質!
那就來分析一下爲何它這麼傻。我們可以看到,在第一次匹配失敗的時候,我們肯定希望它向右移動至少兩格,因爲模式串的第一格和第三格都爲a,既然第三格已經匹配成功了,那麼把第一格對上第三格匹配的位置,那麼無疑肯定也是可以成功的,我們的算法本該知道並且利用這一點的!但是它沒有,它太傻了。
嗯,這麼一說,好像是感覺應該是要把它向着動態規劃的方向改(即利用已有信息爲下一步提供便利)。
PS:字符串問題百分之八十以上都可以使用動態規劃思想達到較低的時間複雜度。
我們大都聽過一句老話:人啊,貴在有自知之明。
同時我們肯定也聽別人說過:人只有深刻的認識了自己,才能找對位置,迅速地向目標前進!
這兩句話用在KMP算法中再合適不過了!
KMP算法的核心便在於,模式串對自己的自我認知!
想一想,我們人對自己的認知是如何的:男,19歲,陽光帥氣聰明機智,這些自我認知都存放在我的腦袋裏面。
那麼,模式串對自己的認知應該存放在哪呢?
對,就是next數組裏面!字符串沒有大腦,所以它需要額外的空間來存儲它對自己的認知並籍此作出高效準確的判斷。
那麼字符串對自己的認知是怎樣的呢?其實很容易理解,就是知道自己身上哪些地方是相同的,這樣的話在匹配失敗之後就能迅速找準下次開始的點。這裏是不是有點模糊了?圖來!
以上就是KMP算法的動畫,如果覺得動畫稍微有點快的話可以多觀看幾次,在這個動畫裏我還沒有放出next數組的部分,只是用擬人化的手法展現出來。希望大家能夠理解,爲什麼第一次匹配失敗可以直接移動兩格。
是因爲模式串中第三格的a,它知道在第一格有與自己相同的字符,並且把這個信息告訴下一格的字符,讓它在匹配失敗之後直接把第一格的a移動到它的那個位置上去。
我這裏爲了大家容易理解,只放出了一個字符相同的情況,大家不妨可以擴展想一下,假如,第一格和第三格的a不是一個字符,而是一個字符串呢?怎麼?有點打腦殼?圖來!
來看看模式串與其對應的next自我認識數組吧。
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
next | -1 | 0 | 0 | 0 | 1 | 2 | 3 |
string | a | b | c | a | b | c | d |
不要去在意next數組的第一個爲什麼是-1,這是爲了代碼寫的方便,暫且就給它當成0.
在動畫中,當一個字符發出“直接移動”的語句的時候,其實是告訴後一個字符,如果你匹配失敗了的話,就直接移動,同時後一個字符對應的next數組值爲0,當後一個字符匹配失敗了,就移動模式串的長度-這個匹配失敗的字符對應的next值
個長度。
從第四個字符(i=3)起,它們都在不斷告訴後面一個字符:“將i=0移動到i=3的位置”,這句話對於i=4的字符來說,是移動4-1
格, 對於i=5的字符來說,是移動5-2
格,對於i=6的字符來說,是移動6-3格
:後面那個減數恰好就是這個字符對應的next數組的值!
因爲模式串足夠了解自己,所以它能夠在匹配失敗的時候不用回退,不用每次只移動一格,而是跟隨着待匹配串一起移動。待匹配字符串的指針從未回退過,以線性的速度向前一步步越進。
最終:KMP算法的時間複雜度是
這裏我們不禁發出了感嘆!原來認識自己真的這麼重要啊!
接下來是求出給定模式串的next數組:
python3代碼奉上⬇️⬇️
def get_next_lst(ss: str) -> list:
length = len(ss)
next_lst = [0 for _ in range(length)]
next_lst[0] = -1
i = 0
j = -1
while i < length - 1:
if j == -1 or ss[i] == ss[j]:
i += 1
j += 1
next_lst[i] = j
else:
j = next_lst[j]
return next_lst
這段代碼最難理解的就是j=next_lst[j]
這句話,其實這句話也是動態規劃的一個思想,看我爲你剖析一下。
已知藍色區域相等且長度都爲len,那麼很明顯,next[i] == len
,若此時模式串pattern[i] != pattern[j]
(兩個灰色區域不相等)。那麼看下圖:
若此時next[j] == len(粉色部分)
那麼S1==S2
,又因爲next[i] == next[j]
,所以S1==S3 且 S3 == S4
,則可以推出S1 == S4
,這樣我們就利用前面所獲得的信息,推出了S1 == S4
這個信息,然後將J移動到S1後一格,只要再次比較patter[i] 與 patter[j]
的相等情況,就可以得出next[i+1]
的值。這裏因爲i始終向後移動,所以也是線性時間複雜度的算法。
ohhhhhhhhh~
到這裏,大家就明白了爲啥KMP算法的時間複雜度是了。
KMP匹配字符串的完整代碼附上!
class KMP():
def __init__(self, ss: str) -> list:
self.length = len(ss)
self.next_lst = [0 for _ in range(self.length)]
self.next_lst[0] = -1
i = 0
j = -1
while i < self.length - 1:
if j == -1 or ss[i] == ss[j]:
i += 1
j += 1
self.next_lst[i] = j
else:
j = self.next_lst[j]
self.pattern = ss
def match_str(self, ss:str):
ans_lst = []
j = 0
for i in range(len(ss)):
if ss[i] != self.pattern[j]:
j = self.next_lst[j] if self.next_lst[j] != -1 else 0
if ss[i] == self.pattern[j]:
j += 1
if j == self.length:
return i + 1 - self.length
return -1
tmp_kmp = KMP('iabc')
print(tmp_kmp.match_str('adosjfoiajsoifjasiofjoiasdjoiabc'))
看到這裏,如果你覺得這篇文章對你理解KMP算法有幫助的話呢,不妨關注我,我會持續更新各種有用的東西。我的個人公衆號是【程序小員】,也歡迎你的關注哦!
我是落陽,謝謝你的到訪~