改良的 Boyer-Moore
我刚刚挖掘了一些旧的 Boyer-Moore 的 Python 实现,并修改了匹配循环(文本与模式进行比较的地方)。与其在两个字符串之间发现第一个不匹配时立即中断,而是简单地计算不匹配的数量,但记住第一个不匹配:
current_dist = 0
while pattern_pos >= 0:
if pattern[pattern_pos] != text[text_pos]:
if first_mismatch == -1:
first_mismatch = pattern_pos
tp = text_pos
current_dist += 1
if current_dist == smallest_dist:
break
pattern_pos -= 1
text_pos -= 1
smallest_dist = min(current_dist, smallest_dist)
# if the distance is 0, we've had a match and can quit
if current_dist == 0:
return 0
else: # shift
pattern_pos = first_mismatch
text_pos = tp
...
如果此时字符串没有完全匹配,则通过恢复值返回到第一个不匹配点。这样可以确保实际找到最小距离。
整个实现相当长(~150LOC),但我可以根据要求发布。核心思想如上所述,其他一切都是标准的 Boyer-Moore。
文本预处理
另一种加快速度的方法是预处理文本以在字符位置上建立索引。您只想在两个字符串之间至少出现一次匹配的位置开始比较,否则汉明距离为 |S|微不足道。
import sys
from collections import defaultdict
import bisect
def char_positions(t):
pos = defaultdict(list)
for idx, c in enumerate(t):
pos[c].append(idx)
return dict(pos)
此方法只是创建一个字典,将文本中的每个字符映射到其出现的排序列表。
比较循环与幼稚的O(mn) 方法或多或少没有变化,除了我们不是每次将比较开始的位置增加1,而是基于字符位置:
def min_hamming(text, pattern):
best = len(pattern)
pos = char_positions(text)
i = find_next_pos(pattern, pos, 0)
while i < len(text) - len(pattern):
dist = 0
for c in range(len(pattern)):
if text[i+c] != pattern[c]:
dist += 1
if dist == best:
break
c += 1
else:
if dist == 0:
return 0
best = min(dist, best)
i = find_next_pos(pattern, pos, i + 1)
return best
实际改进在find_next_pos:
def find_next_pos(pattern, pos, i):
smallest = sys.maxint
for idx, c in enumerate(pattern):
if c in pos:
x = bisect.bisect_left(pos[c], i + idx)
if x < len(pos[c]):
smallest = min(smallest, pos[c][x] - idx)
return smallest
对于每个新位置,我们找到 L 中出现 S 中的字符的最低索引。如果不再有这样的索引,算法将终止。
find_next_pos 确实很复杂,可以尝试通过仅使用模式 S 的前几个字符来改进它,或者使用一个集合来确保模式中的字符不会被检查两次。
讨论
哪种方法更快在很大程度上取决于您的数据集。你的字母表越多样化,跳跃就越大。如果 L 很长,则第二种预处理方法可能更快。对于非常非常短的字符串(如您的问题),天真的方法肯定是最快的。
DNA
如果您的字母表非常小,您可以尝试获取字符二元组(或更大)而不是一元组的字符位置。