字符串模式匹配是字符串处理中的一类经典问题,即给定长度为
n
n
n的字符串text
以及长度为
m
m
m的模式串pattern
,期望先确定pattern
是否为text
的一个子串,如果是,则返回text
中pattern
开始的最小索引j
,使得text[j:j+m]
等于pattern
,或者返回所有满足条件的索引。
实际上,在Python内置的 str
模块中, t.find(p)
、t.index(p)
、t.count(p)
以及 t.partition(p)
、t.split(p)
、t.replace(p, q)
等方法的实现都依赖于字符串的模式匹配。
本文将分别介绍并实现两种种常见的字符串模式匹配算法:暴力算法、BM算法(Boyer-Moore)。
一、暴力匹配算法
1. 介绍
暴力匹配法是最直观的一种字符串模式匹配算法,这种方法的主要思想是先枚举出所有可能的情况,然后找出符合要求的结果。具体地,将模式串 pattern
从索引为 0 的位置依次和 text
索引在 0 、1 、2 直到
n
−
m
n-m
n−m 的索引处进行对齐,然后迭代
m
m
m 次判断是否可以在 text
中找出和 pattern
匹配的子串。
2. 实现
下面给出暴力法实现字符串模式匹配的具体代码:
def find_brute(text, pattern):
"""如果子串pattern存在text中,则返回pattern在text中起始位置索引,否则返回-1"""
n, m = len(text), len(pattern)
for i in range(n - m + 1): # 从索引0至n - m处依次尝试开始匹配
k = 0
while k < m and text[i + k] == pattern[k]: # 第k个字符匹配
k += 1
if k == m: # 判断此轮for循环是否成功匹配
return i # 子串text[i:i + m]和pattern匹配
return -1
3. 复杂度
如暴力法的思想和实现一样,其时间复杂度分析同样直观。该算法包括两个嵌套的循环,外层循环依次迭代每个可能的子串起始索引,内层循环判断此次外层循环的迭代是否成功匹配。显然,在最坏的情况下,外层循环最多执行 n − m + 1 n-m+1 n−m+1 次,内层循环最多执行 m m m 次,因此该方法的最坏时间复杂度为 O ( n m ) O(nm) O(nm) 。
二、Boyer-Moore算法
1. 介绍
上述暴力匹配算法虽然直观,但是在最坏情况下,需要在穷举 text
的所有可能子串,然后和模式串 pattern
逐字符比对,因此效率较低。下面将介绍的 Boyer-Moore(BM)算法则可以跳过 pattern
和 text
的子串比对时相当部分的字符。该方法主要基于下列两个手段:
镜像试探
:在将模式串pattern
和text
的某子串进行比对时,从右到左进行比较,而不是从左到右(这也是这里镜像二字的含义);字符跳跃试探
:在将模式串pattern
和text
的某子串进行比对时,当该子串中某字符text[i]=c
和模式串对应位置字符pattern[k]
不匹配时,根据两种不同情况,分别做如下操作:
- 如果字符
text[i]=c
不在模式串pattern
中,则将模式串pattern
向右整体移过text[i]
所在位置;- 如果字符
text[i]=c
在模式串pattern
中,为确保不漏掉可能成功匹配的情况,只能将模式串pattern
整体向右移动,直到pattern
中的最后一次1出现的字符c
和text[i]
对齐。
为便于读者理解,结合下图对上述两个过程进行详细解释:
- 一开始,模式串
'sushi'
和text
在最左侧对齐,然后将二者从右往左进行比对; - 接着,由于
text
中的字符'e'
和模式串对应位置的字符'i'
不匹配,且字符'e'
不在模式串'sushi'
中,因此将模式串整体向右移过字符'e'
处; - 然后,继续将模式串和
text
从右往左进行比对,虽然此时text
中的字符's'
和模式串对应位置字符'i'
不匹配,但是字符's'
包含在模式串中,因此将模式串向右移动,直到模式串中右起第一个's'
字符和text
中的该字符对齐。
上图仅描绘了当 pattern
的最后一个字符和 text
对应位置字符不匹配时应该进行的操作。一般地,当二者匹配时,此时算法会接着从 pattern
的倒数第二个字符开始,尝试继续扩大发生匹配的字符数。此过程会一直继续,直到整个 pattern
成功匹配或发生某个字符不匹配。
如果在上述继续扩大发生匹配字符数的过程中确实发生某字符不匹配,则需要根据下列两种情况做不同处理:
- 如果
text
中当前不匹配的字符不存在pattern
中,则类似上图,将模式串pattern
整体移过该字符; - 如果
text
中当前不匹配的字符(假设此时text
该位置为'a'
,pattern
的该位置为'b'
)存在pattern
中,则根据text
中字符'a'
最后一次出现在pattern
中的位置,分别做如下处理:- 如果在
pattern
中,字符'a'
最后一次出现的位置在字符'b'
之前,则向右整体移动pattern
,使得二者在字符'a'
处对齐; - 如果在
pattern
中,字符'a'
最后一次出现的位置在字符'b'
之后,则向右整体移动pattern
一个字符。
- 如果在
为便于后续的编码实现,下面结合示意图对上述两种情况进行量化分析:
为便于分析,下图2中的索引 i
, k
,j
的含义分别为:
i
:表示继续扩大匹配字符数的过程中,匹配失败的字符'a'
在text
中的索引;k
:表示和上述字符'a'
对齐的字符(此处为'b'
)在pattern
中的索引;j
:表示pattern
中最后一次出现的字符'a'
的在pattern
中的索引。
因此:
- 当
j
<
k
j\lt{k}
j<k ,即
pattern
中字符'a'
最后一次出现的位置在字符'b'
之前,则将pattern
整体向右移动 k − j k-j k−j 个字符,此时索引i
需递增 ( k − j ) + ( m − 1 − k ) = m − ( j + 1 ) (k-j)+(m-1-k)=m-(j+1) (k−j)+(m−1−k)=m−(j+1) ; - 当
j
>
k
j\gt{k}
j>k ,即
pattern
中字符'a'
最后一次出现的位置在字符'b'
之后,则将pattern
整体向右移动1个字符,此时索引i
需递增 1 + ( m − 1 − k ) = m − k 1+(m-1-k)=m-k 1+(m−1−k)=m−k 。
在实现字符串匹配的 BM 算法之前,为了使读者对上述一系列分析有一个集中的直观了解,下面给出通过BM 算法以模式串 pattern = 'abacab'
匹配字符串 text = 'abacaabadcabacabaabb'
的详细过程:
pattern
处于第一个位置时,由于第一次即发生不匹配,且字符'a'
存在pattern
中,将pattern
向右平移一个字符到达第二个位置,累计进行了 1 次匹配操作;pattern
处于第二个位置时,直到累计发生的第 4 次匹配才出现不匹配现象,由于此时text
中和当前位置pattern
中字符'c'
对齐的字符'a'
在pattern
中, 且pattern
中最后一个'a'
在字符'c'
后面,则将pattern
整体向右移动一个字符;pattern
处于第三个位置时,发生第 5 次匹配操作,此时过程和pattern
处于第一个位置的情况类似;pattern
处于第四个位置时,发生第 6 次匹配操作,由于此时text
中的字符'd'
不在pattern
中,因此将pattern
整体移过字符'd'
;pattern
处于第五个位置时,发生第 7 次匹配操作,此时此时过程和pattern
处于第一、三个位置的情况类似;pattern
处于第六个位置时,发生第8、9、10、11、12、13匹配,最终匹配成功。
2. 实现
下面是基于BM算法实现的字符串匹配:
def find_boyer_moore(text, pattern):
"""如果模式串pattern存在text中,则返回pattern在text中起始位置索引,否则返回-1"""
n, m = len(text), len(pattern)
if m == 0:
return 0
last = {}
for k in range(m): # 以pattern中字符为键索引为值创建字典
last[pattern[k]] = k
# 初始化索引辅助变量,使得pattern最右侧字符和text索引m - 1处对齐
i = m - 1
k = m - 1
while i < n:
if text[i] == pattern[k]:
if k == 0: # 判断是否连续完成了len(pattern)次成功匹配
return i # 成功将pattern和text某子串进行匹配后,text中和pattern相同的子串起始于索引i
else: # 继续从右向左比对pattern和text对齐位置字符相同
i -= 1
k -= 1
else:
j = last.get(text[i], -1)
if j < k: # text[i]不存在pattern中即j = -1时,该条件及其操作依然成立
i += m - (j + 1)
if j > k:
i += m - k
k = m - 1 # 重新从右开始对pattern和text进行匹配
return -1
if __name__ == '__main__':
text = 'qwertyqazxswedcvfrtgb'
pattern = 'zx'
print(find_boyer_moore(text, pattern)) # 8