串匹配问题
根据具体应用的要求不同,串匹配问题可以多种形式呈现。
- 有些场合属于**模式检测(pattern detection)**问题:我们只关心是否存在匹配而不关心具体的匹配位置,比如垃圾邮件的检测。
- 有些场合属于**模式定位(pattern location)**问题: 若经判断的确存在匹配,则还需确定具体的匹配位置,比如带毒程序的鉴别与修复。
- 有些场合属于**模式计数(pattern counting)**问题:若有多处匹配,则统计出匹配子串的总数,比如网络 热门词汇排行榜的更新。
- 有些场合则属于**模式枚举(pattern enumeration)**问题:在有多处匹配时,报告出所有匹配的具体位置,比如网络搜索引擎。
note: 本文只讨论模式定位问题——子串定位。
子串定位问题
在主串
text
中,定位模式串pattern
在字符串的位置。若pattern
存在于text
中,返回最先匹配的位子,否则返回-1
蛮力算法
蛮力算法是朴素的想法,我们用python代码来表示蛮力算法的思想。
from typing import List
def brute_force_match(text:str,pattern:str)->int:
for i in range(len(text)-len(pattern)):
if text[i:i+len(pattern)] == pattern:
return i
return -1
复杂度分析
蛮力算法时间复杂度:
O
(
n
∗
m
)
O(n*m)
O(n∗m)其中
n
n
n,
m
m
m分别是主串text
和模式串pattern
的长度。
空间复杂度为:O(1)
KMP算法
KMP是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。KMP适用于解决模式串定位问题的高效算法。
回顾蛮力算法
从蛮力算法中可以看出,每次在比较子串失败时,主串都要回到上次开始的下一个字符。然后在
text
与pattern
比较到第 t t t个字符时,说明text[i:i+t] = pattern[:t]
,也就是说主串的这部分子串与模式串的这部分子串相同。既然如此,我们可以利用模式串自身的关系来避免不必要的比较。
记忆=经验=预知力
举个例子:
text = abababc
pattern = ababc
我们可以发现:pattern[:2] = pattern[2:4]
所以在主串与pattern [4]
比较失败时,主串不用回退,子串只需要回退到pattern[2]
,然后再比较。
也就是说,如果再模式串中
pattern[:k] = pattern[j-k:j]
,则再pattern[j]
比较失败时,主串游标不需要大幅的回退(蛮力算法需要大幅回退,而这里主串游标不回退),而将模式串的游标回退到指定位置 j − k j-k j−k即可。我们用next_tb
数据记录pattern中的每个字符的回退位置。我们为了效率,我们需要求出满足
pattern[:k] = pattern[j-k:j]的最大
k`值。
我们将上述思想转换为python如下:
from typing import List
def KMP(text:str,pattern:str,next_tb:List[int])->int:
n,m = len(text),len(pattern)
while i < n and j < m:
if j < 0 or text[i]==pattern[j]:
i+=1
j+=1
else:
j = next_tb[j]
return i-j if j==m else -1
上述代码已描述KMP
算法的主体代码,下面我们将讨论next_tb
如何得到。
build_next 讨论
直觉实现
from typing import List
def build_next0(pattern:str)->List[int]:
next_tb = [-1 for i in range(len(pattern))]
for i in range(1,len(pattern)):
for j in range(i):
if pattern[:j] == pattern[i-j:i]:
next_tb[i] = j
return next_tb
我们将求next_tb
列表的过程,用build_next0
函数来封装。从上面的代买来看,build_next0
函数的时间复杂度为
O
(
m
3
)
O(m^3)
O(m3),KMP的主体时间复杂度为
O
(
n
+
m
)
O(n+m)
O(n+m),所以总体的时间复杂度为
O
(
n
+
m
3
)
O(n+m^3)
O(n+m3),从上述代码也容易分析出其空间复杂度为
O
(
m
)
O(m)
O(m)。
第一次改进
根据对蛮力算法的分析,我们可以将
next_tb
描述如下:
next_tb[j]
记录的是满足pattern[:k]==pattern[j-k:j]
的最大 k k k. (p[:k] 和p[j-k:j]),因为对于不同 j j j都有相同子问题,所以我们 k k k可以自底而上的构建next_tb
。下面我们归纳的讨论各个next_tb[j]
的取值:当 j = 0 j=0 j=0时:k不存在,我们不妨设
next_tb[0]=-1
当j=x+1时:此时
next_tb[x] = k
,故pattern[:j]
自匹配的真前缀和真后缀的最大长度为 k k k,故可以得出,n e x t _ t b [ x + 1 ] ≤ n e x t _ t b [ x ] + 1 next\_tb[x+1] \le next\_tb[x] +1 next_tb[x+1]≤next_tb[x]+1,当
pattern[j]==pattern[k]
时取等号。当pattern[j]!=pattern[k]
时, n e x t _ t b [ x + 1 ] = { n e x t _ t b [ n e x t _ t b [ x ] ] + 1 , n e x t _ t b [ n e x t _ t b [ n e x t _ t b [ x ] ] ] + 1 , ⋯ } next\_tb[x+1] =\{next\_tb[next\_tb[x]] + 1, next\_tb[next\_tb[next\_tb[x]]] + 1, \cdots\} next_tb[x+1]={next_tb[next_tb[x]]+1,next_tb[next_tb[next_tb[x]]]+1,⋯},可以证明这个集合是有限的( ∣ p a t t e r n ∣ = m , m 有限 |pattern|=m,m有限 ∣pattern∣=m,m有限)
上述方法的python
实现如下:
from typing import List
def build_next_kmp(pattern:str) -> List[int]:
next_tb = [-1 for i in range(len(pattern))]
j,k = 0,-1
while j < len(pattern)-1:
if k==-1 or pattern[j]==pattern[k]:
k += 1
j += 1
next_tb[j] = k
else:
k = next_tb[k]
return next_tb
build_next_kmp
与KMP
的实现一致,时间复杂度一致,所以构造next_tb
的时间复杂度为
O
(
m
)
O(m)
O(m),从上述代码,不难看出,空间复杂度为
O
(
m
)
O(m)
O(m),故KMP
的整体时间复杂度为
O
(
n
+
m
)
O(n+m)
O(n+m),整体空间复杂度为
O
(
m
)
O(m)
O(m).
第二次改进
首先我们看一个具体的例子:
text = 0001000010
pattern = 000010
next_tb = [-1,0,1,2,3,0]
对上述例子中,
i
=
j
=
3
i=j=3
i=j=3时,j会回退一个单位,然而我们知道pattern
的前4
个字符都是0,而失配的i
对应的主串的字符为1,所以这里存在大量无效的回退。为了解决这个无效回退,我们从新定义next_tb
如下:
next_tb[j]
记录满足pattern[:k]==pattern[j-k:j]
且pattern[k]!=pattern[j]
的最大
k
k
k。
记忆 = 教训 = 预知力
python
实现代码如下:
from typing import List
def build_next_improve(pattern:str):
next_tb = [-1 for _ in range(len(pattern))]
j,t = 0,-1
while j < (len(pattern)-1):
if(t<0 or pattern[j]==pattern[t]):
j += 1
t += 1
next_tb[j] = t if pattern[j]!=pattern[t] else next_tb[t]
else:
t = next_tb[t]
return next_tb
自此,关于KMP
算法的介绍就结束了。KMP的build_next
与KMP
主题的思想是很相似的,都是利用经验做出推断。同时KMP
也是牺牲较少空间来换取时间的典型案例。
BM算法
待续…
Karp-Rabin算法
待续…
联系邮箱: antarm@outlook.com