常见字符串匹配算法Python实现
class StringMatching(object):
"""常见字符串匹配算法"""
@staticmethod
def bf(main_str, sub_str):
"""
BF 是 Brute Force 的缩写,中文叫作暴力匹配算法
在主串中,检查起始位置分别是 0、1、2…n-m 且长度为 m 的 n-m+1 个子串,看有没有跟模式串匹配的
"""
a = len(main_str)
b = len(sub_str)
for i in range(a - b + 1):
if main_str[i:i + b] == sub_str:
return i
return -1
@staticmethod
def generate_bc(sub_str):
"""
:param sub_str: 是模式串
:return: 返回一个散列表 散列表的下标是 b_char 中每个字符的 ascii 码值,下标对应的值是该字符在b_char中最后一次出现的位置
"""
ascii_size = 256
ascii_list = [-1] * ascii_size # 初始化一个散列表
for i in range(0, len(sub_str)):
ascii_val = ord(sub_str[i]) # 计算 b_char中每个字符的 ascii 值
ascii_list[ascii_val] = i # 存每个字符在 b_char 最后一次出现的位置
return ascii_list
@staticmethod
def move_by_gs(j, m, suffix, prefix):
"""
:param j: 表示坏字符对应的模式串中的字符下标
:param m: m 表示模式串的长度
:param suffix: suffix 数组中下标k,表示后缀子串的长度,下标对应的数组值存储的是后缀子串在字符串sub_str的位置
:param prefix: 而 prefix 数组中下标k,表示后缀子串的长度,对应的值 True 或者 False 则表示该后缀子串跟相同长度的前缀子串是否相对
:return: 应该往后滑动的距离
"""
k = m - 1 - j # k 好后缀长度
if suffix[k] != -1: # 如果后缀在字符串中的所有前缀子串中存在
return j - suffix[k] + 1
for r in range(j + 2, m): # 如果有后缀等于前缀
if prefix[m - r]:
return r
return m # 如果前面两个规则都不使用,则直接往后滑动 m 位
@staticmethod
def generate_gs(sub_str):
"""
假如字符串sub_str = cabcab, 那么后缀子串有[b,ab,cab,bcab,abcab]
suffix 数组中下标k,表示后缀子串的长度,下标对应的数组值存储的是后缀子串在字符串sub_str的位置
如:suffix[1] = 2, suffix[2] = 1, suffix[3] = 0, suffix[4] = -1, suffix[5] = -1
而 prefix 数组中下标k,表示后缀子串的长度,对应的值 True 或者 False 则表示该后缀子串跟相同长度的前缀子串是否相对
如 prefix[1] = False, prefix[2] = False, prefix[3] = True (表示 后缀cba = 前缀cba), prefix[4] = False, prefix[5] = False,
"""
m = len(sub_str)
suffix = [-1] * m
prefix = [False] * m
for i in range(0, m - 1):
j = i
k = 0 # 公共后缀串长度,
while j >= 0 and sub_str[j] == sub_str[m - 1 - k]: # 求公共后缀子串,先计算第一个跟模式子串最后一个字符相匹配的位置
j -= 1 # 然后依次比较前面的字符是否相等,比如:cabcab,先从前往后历遍,计算得到 b 字符是符合要求的,
k += 1 # 此时再从 b 字符前一个位置与模式串倒数第二个位置的字符去比较,如果还是相等,则继续,循环了 k 次说明匹配的字符长度就是 k
suffix[k] = j + 1 # j+1 表示公共后缀子串在 sub_str[0:i]中的起始下标
if j == -1: # 如果公共后缀子串也是模式串的前缀子串
prefix[k] = True
return suffix, prefix
def bm_simple(self, main_str, sub_str):
"""
:param main_str: 主字符串
:param sub_str: 模式串
:return:
仅用坏字符规则,并且不考虑 si-xi 计算得到的移动位数可能会出现负数的情况的代码实现如下
"""
bc = self.generate_bc(sub_str) # 记录模式串中每个字符最后出现的位置,也就是坏字符的哈希表
i = 0 # 表示主串和模式串对齐的第一个字符
n = len(main_str) # 表示主串的长度
m = len(sub_str) # 表示子串的长度
while i <= n - m: # i 最多滑动到两个字符右对齐的位置
j = m - 1 # 数组下标从0开始算,这里实际从子字符串的最后一个位置开始找坏字符
while j >= 0:
if main_str[i + j] != sub_str[j]: # 坏字符对应模式串中的下标是 j
break
j -= 1
if j < 0: # 没有坏字符,全部都匹配上了
return i # 匹配成功,返回主串和模式串第一个匹配的字符的位置
bad_str_index = bc[ord(main_str[i + j])] # 坏字符在子模式串中的位置
i += (j - bad_str_index) # 这里等同于将模式串往后滑动 j - bc[ord(main_str[i + j]) 位
return -1
def bm(self, main_str, sub_str):
"""
:param main_str: 主字符串
:param sub_str: 模式串
:return:
"""
bc = self.generate_bc(sub_str) # 记录模式串中每个字符最后出现的位置,也就是坏字符的哈希表
suffix, prefix = self.generate_gs(sub_str)
i = 0 # 表示主串和模式串对齐的第一个字符
n = len(main_str) # 表示主串的长度
m = len(sub_str) # 表示子串的长度
while i <= n - m:
j = m - 1
while j >= 0:
if main_str[i + j] != sub_str[j]: # 坏字符对应模式串中的下标是 j
break
j -= 1
if j < 0:
return i # 匹配成功,返回主串和模式串第一个匹配的字符的位置
x = j - bc[ord(main_str[i + j])] # 计算坏字符规则每次需要往后滑动的次数
y = 0
if j < m - 1: # 如果有好后缀的话
y = self.move_by_gs(j, m, suffix, prefix) # 计算好后缀规则每次需要往后滑动的次数
i = i + max(x, y)
return -1
@staticmethod
def get_next_list(sub_str):
"""计算如: abababzabababa 每个位置的前缀集合与后缀集合的交集中最长元素的长度
输出为: [-1, -1, 0, 1, 2, 3, -1, 0, 1, 2, 3, 4, 5, 4]
计算最后一个字符 a 的逻辑如下:
abababzababab 前缀后缀最大匹配了 6 个(ababab),次大匹配是4 个(abab),次大匹配的前缀后缀只可能在 ababab 中,
所以次大匹配数就是 ababab 的最大匹配数,在数组中查到出该值为 3。第三大的匹配数同理,它既然比 3 要小,那前缀后缀也只能在 abab 中找,
即 abab 的最大匹配数,查表可得该值为 1。再往下就没有更短的匹配了。来计算最后位置 a 的值:既然末尾字母不是 z,
那么就不能直接 5+1=7 了,我们回退到次大匹配 abab,刚好 abab 之后的 a 与末尾的 a 匹配,所以 a 处的最大匹配数为 4。
"""
m = len(sub_str)
next_list = [None] * m
next_list[0] = -1
k = -1
i = 1
while i < m:
while k != -1 and sub_str[k + 1] != sub_str[i]: # k+1 是每个前缀子串的最后一个字符,i 是每个后缀子串的第一个字符
k = next_list[k] # 求次大匹配数,次大匹配数如 abab 是上层 ababab 的最大匹配数
if sub_str[k + 1] == sub_str[i]: # 如果前缀子串的最后一个字符和后缀子串的第一个字符相等,则增加前缀子串和后缀子串的长度再比较
k += 1
next_list[i] = k # sub_str[0:i] 的前缀集合与后缀集合的交集中最长元素的长度
i += 1
return next_list
def kmp(self, main_str, sub_str):
n = len(main_str)
m = len(sub_str)
next_list = self.get_next_list(sub_str)
i = 0
j = 0
while i < n and j < m:
if j == -1 or main_str[i] == sub_str[j]: # 如果子串的每个字符和主串的每个字符相等就继续循环
i += 1
j += 1
else: # 在模式串j处失配,需要找到sub_str[0:j-1]的前缀集合与后缀集合的交集中最长元素的长度, 然后直接将j更新
j = next_list[j] # 为next_list[j],本来是需要将main_str[i] 处的字符依次与 sub_str[0:j]中的每个元素依次再比较的
if j == m:
return i - j
return -1