字符串——子串匹配

串匹配问题

根据具体应用的要求不同,串匹配问题可以多种形式呈现。

  1. 有些场合属于**模式检测(pattern detection)**问题:我们只关心是否存在匹配而不关心具体的匹配位置,比如垃圾邮件的检测。
  2. 有些场合属于**模式定位(pattern location)**问题: 若经判断的确存在匹配,则还需确定具体的匹配位置,比如带毒程序的鉴别与修复。
  3. 有些场合属于**模式计数(pattern counting)**问题:若有多处匹配,则统计出匹配子串的总数,比如网络 热门词汇排行榜的更新。
  4. 有些场合则属于**模式枚举(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(nm)其中 n n n, m m m分别是主串text和模式串pattern的长度。

空间复杂度为:O(1)

KMP算法

KMP是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。KMP适用于解决模式串定位问题的高效算法。

回顾蛮力算法

从蛮力算法中可以看出,每次在比较子串失败时,主串都要回到上次开始的下一个字符。然后在textpattern比较到第 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 jk即可。我们用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_kmpKMP的实现一致,时间复杂度一致,所以构造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_nextKMP主题的思想是很相似的,都是利用经验做出推断。同时KMP也是牺牲较少空间来换取时间的典型案例。

BM算法

待续…

Karp-Rabin算法

待续…


联系邮箱: antarm@outlook.com

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ufy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值