前言
上一篇我用動畫的方式向大家詳細說明了KMP算法(沒看過的同學可以回去看看)。
這次我依舊採用動畫的方式向大家介紹另一個你用一次就會愛上的字符串匹配算法:Sunday算法,希望能收穫你的點贊關注收藏與轉發喲!
KMP算法是一個里程碑似的算法,它的出現宣告了人類是找到線性時間複雜度的字符串匹配算法的。在這之後,出現了許多的字符串匹配算法,比如BM算法和Sunday算法。
這些算法在時間複雜度上都已經達到了線性時間。但是在實際應用的時候所耗費的時間卻還是有所不同。
BM算法在實際應用中的效率已經達到了KMP算法的四五倍。
而Sunday算法的效率甚至猶在BM算法之上。
並且若是兩種算法都瞭解的同學會明白:
Sunday算法比起BM算法來,真的極其容易理解。
正文
行,咱對Sunday算法的吹捧先到這爲止,下面開始正戲!
PS:以下將帶匹配字符串稱爲文本串,將用來匹配的字符串稱爲模式串。
爲什麼說Sunday算法極易理解呢?
因爲它比暴力匹配算法只多了一個步驟而已!
話不多說,直接上我精心製作的GIF動態圖:
可以看到,我們只移動了三次,就直接找到了最終的結果。
Sunday算法是從前往後匹配的算法(BM算法是從後向前的),在匹配失敗時重點關注的是文本串中參加匹配的最末位字符的下一位字符。
-
如果該字符沒有在模式串中出現則直接跳過,即移動位數 = 模式串長度 + 1。
-
否則,其移動位數 = 模式串長度 - 該字符最右出現的位置(以0開始) = 模式串中該字符最右出現的位置到尾部的距離 + 1。
Sunday算法最巧妙的地方,就在於它發現匹配失敗之後可以直接考察文本串中參加匹配的最末尾字符的下一個字符。
在python代碼中,我們利用字典來存儲模式串中每個字符最後出現的索引,這樣在前期只需O(M),M爲模式串長度的時間即可做完前期準備,然後再進行查詢都是O(1)的時間。
同時爲了防止越界,我在下面貼出來的python代碼中手動在字符串末尾加上了一個'\0'字符
。
代碼
class Sunday(object):
def __init__(self, pattern:str):
# 模式串和其長度
self.pattern, self.length = pattern, len(pattern)
# 根據模式串構建的偏移字典
self.shift_dict = {}
# 構建字典
for index, value in enumerate(pattern):
self.shift_dict[value] = self.length - index
def match(self, text:str):
i = 0
text_length = len(text)
text += '\0'
while i <= text_length - self.length:
j = 0
while self.pattern[j] == text[i + j]:
j += 1
if j >= self.length:
return i
offset = self.shift_dict[text[i+self.length]] if text[i+self.length] in self.shift_dict else self.length + 1
i += offset
return -1
s = Sunday('nihao')
print(s.match('dasoijfoasjdoifjasdoifjoinihao'))
代碼十分的簡單,同時,我構造了一個類,是爲了在同一個模式串下能夠複用它的位置字典,簡化代碼。
Sunday算法與KMP算法大比拼
在寫完代碼之後,我對KMP算法和Sunday算法的匹配時間進行了一個粗略的檢測,檢測結果如下:
amazing!,Sunday算法的平均匹配速度達到了KMP算法的七倍左右!
對KMP和Sunday各自構造了一個對象,然後每次生成一個隨機的十萬個字符長度的字符串讓它們倆分別開始匹配。
生成-->匹配
這個過程循環一百遍,最終計算平均時間。如果有大佬覺得不放心的,我在下方放出檢測代碼,大家可以自行修改測試,拿去即可用!
檢測代碼如下
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
if ss[i] == ss[j]:
self.next_lst[i] = self.next_lst[j]
else:
self.next_lst[i] = j
else:
j = self.next_lst[j]
self.pattern = ss
def match(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
class Sunday(object):
def __init__(self, pattern:str):
# 模式串和其長度
self.pattern, self.length = pattern, len(pattern)
# 根據模式串構建的偏移字典
self.shift_dict = {}
# 構建字典
for index, value in enumerate(pattern):
self.shift_dict[value] = self.length - index
def match(self, text:str):
i = 0
text_length = len(text)
text += '\0'
while i <= text_length - self.length:
j = 0
while self.pattern[j] == text[i + j]:
j += 1
if j >= self.length:
return i
offset = self.shift_dict[text[i+self.length]] if text[i+self.length] in self.shift_dict else self.length + 1
i += offset
return -1
import random
import time
sunday = Sunday('helloworld')
kmp = KMP('helloworld')
kmp_average_time = 0
sunday_average_time = 0
for i in range(100):
ss = ''.join([chr(random.randint(97, 122)) for _ in range(100000)])
st = time.process_time()
sunday.match(ss)
ed = time.process_time()
sunday_average_time += ed - st
st = time.process_time()
kmp.match(ss)
ed = time.process_time()
kmp_average_time += ed - st
print('kmp平均時間: {}'.format(kmp_average_time / 100))
print('sunday平均時間: {}'.format(sunday_average_time / 100))
最後
最後,如果你覺得這篇文章對你有幫助的話呢,給我點個關注,收藏吧!
我的個人公衆號是【程序小員】,歡迎你的關注,你的認可是我最大的動力!
我會持續更新對你有幫助的文章!
我是落陽,謝謝你的到訪!