String Matching 字符串匹配算法——干货从头放到尾

需要的先验知识:动态规划,有限状态机,搜索算法(就是含有state,action和policy)的模型,java。上面这些不需要知道很细,大概懂这些都是啥就可以读懂本文。

写这篇技术博客的动机是因为做 Leetcode “Implement strStr” 一题学会了KMP算法,觉得这个第一次学还挺绕的就想记录一下解题思路,不过后来又补充了好多好多前前后后关于字符串匹配的算法知识,这篇文章就变成了一篇干货分享啦hhh。

Problem Definition

在网页和文档的中的所搜功能肯定大家都用过,Ctrl+F,输入要搜索的关键词,文本中出现的搜索词就都给你标出来了。这个其实用到的就是字符串匹配。

  1. 定义字母表(Alphabet) Σ \Sigma Σ。Size of Alphabet = | Σ \Sigma Σ|。
    eg: Σ \Sigma Σ = { a, b, …, z }, | Σ \Sigma Σ| = 26

  2. 定义文本(Text) T [1, … n]. T[i] 是字母表中的字符。|T| = n (文本长度)

  3. 定义模式(Pattern): P [1, … m]. P[i]是字母表中的字符。|P| = m

  4. 定义术语 “shift”:If T[i+s] = P[i] for i=0,…, m-1, then s is a valid shift. We say: P occurs with shift s in T.

String Matching 算法将回答两个问题:1)Does P occur in T? If yes, find its first occurrence. 2)Find all occurrences of P in T.
用刚刚定义的shift来描述这两个问题就是:1)Does P occur in T? If yes, find the first valid shift. 2)Find all valid shifts with which P occurs in T.

暴力破解法

for s=0 to n-m: 检查 if T[i+s] = P[i] for i=0,…,m-1. 这个算法复杂度为O(mn).

改善一丢丢的解法:Rabin-Karp

思路:Treat alphabet characters as numbers. Treat strings as polynomials.
eg. " abcdz" → 26 + 4×10 + 3×100 + 2×1000 + 1×10000 → 12366
由此一来可以用Hash给每个string算一个指纹(fingerprint)。
eg. Hash function is "Taking the value of polynomial modulo some prime q. Let’s say q=13.
“abcdz” → 12366 → 12366%13 = 3. “abcdz”的指纹就是3.

于是很快就可以想到基于Hash指纹的算法:
算 fingerprint (Pattern)

for s=0 to n-m:
	算 fingerprint (T[s:s+m-1]). 
	如果它和 fingerprint(Pattern)相等,再花O(m)时间比较具体T[s:s+m-1] 
	和 P这两个string。(因为不同的string可能对应同一个指纹)

不过这样简单粗暴的运用Hash还是too young too simple! 仔细想一下,每一轮循环算一个T[s:s+m-1]的指纹需要O(m), 共n轮循环因此总时间还是O(mn)!

Hash高段位玩法:将上一轮的fingerprint记下来,巧妙利用Hash的特性,将每次算指纹的时间降至O(1).

eg. Text=“abcdeg”
s=0:“abcde” → 12345 假如已经算完12345%13 = 8
s=1:“bcdeg” → 23457 s=0的结果为计算23457%13提供了不少信息有木有??

23457%13 = ((12345 - 10000)× 10 + 7)%13 = ((12345%13 - 1×(10000%13))× 10 + 7)%13 = ((8-1×3)× 10 + 7)%13 = 57%13 = 5
上面的式子中标亮的第一块正好是上一步算的指纹,第二块我们可以在拿到Pattern的时候算好10^m %13.

于是,Find s such that T[s:s+m-1] & P[0:m-1] have the same fingerprint 这一步的复杂度降至O(m+n). +m是因为s=0的时候没有上一步提供的信息帮忙,需要O(m),此外算P的指纹也需要O(m).
整体的时间复杂度是O(m+n) + mM. M代表有多少指纹能对上。若Hash function选得好的话,极少会出现不同string对应同样指纹的情况,此时Rabin-Karp算法的时间复杂度逼近O(m+n) + mM*. (M* = # of real matches)

Rabin-Karp 小结
  • 将string的比较转变为指纹的比较,可以快速排出invalid shift。
  • 算指纹时可利用上一步提供的信息,在O(1)内完成一步(相较于对比string的O(m)省时不少。
  • 算指纹用的Hash相当于“初筛”, 筛出少量的候选人再每个花O(m)时间比较。初筛的效率跟Hash function设计的好不好有关。
  • 唯一小缺陷是,当M*逼近n的时候,或者是P在T当中频繁出现使得real match很多的时候,这个算法就丧失了省时的功效,因为“初筛”并不会筛掉很多。

终极大招:KMP

汲取从Rabin-Karp学到的套路 —— Reuse Information! 最大化利用从上一步循环算出的信息。
我们来看看从Rabin——karp能抽象出哪些算法框架级别的套路:

String_matching_csdn

  • 扫描单位是长度为m的窗户。
  • 窗户每向右移一格,根据右边新看到的字符和上一个窗户的信息即可算出当前窗户的信息(这里说的信息比较抽象,下面具体化)

定义每个扫描位置的“状态(state)”为:已经从左到右匹配了P里的多少个字符。state=m代表完全匹配成功。这个思路在T包含多个重复出现的字符串模式时比较容易理解。来看看这个例子:

T=“abaababac” P=“abac”
for i=0 to m-1:
        i=0,看到T[i] = ‘a’. state=1
       

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值