需要的先验知识:动态规划,有限状态机,搜索算法(就是含有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能抽象出哪些算法框架级别的套路:

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

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

T=“abaababac” P=“abac”
for i=0 to m-1:
        i=0,看到T[i] = ‘a’. state=1
        i=1,看到T[i] = ‘b’. state=2
        i=2,看到T[i] = ‘a’. state=3
        i=3,看到T[i] = ‘a’. state=1
        i=4,看到T[i] = ‘b’. state=2
        i=5,看到T[i] = ‘a’. state=3
        i=6,看到T[i] = ‘b’. state=2
        i=7,看到T[i] = ‘a’. state=3
        i=8,看到T[i] = ‘c’. state=4 ← Bingo!

每个state和上一步的state以及当前步新看到的一个字符有关。像不像search里面的state 和 action?再想想还像啥?对!Finite State Machine!
而且Finite State Machine的转移图只跟P有关跟T无关。不信你看↓ ( P=“abac”)
于是现在思路就有了,先拿P建好Finite State Machine, 再扫描一遍T, 每个扫描步根据新看到的字符(action),上一步的状态(state)和FSM (policy)更新状态。这样扫描T的时候不走回头路,在O(n)内就能完成。实现FSM用到的具体数据结构是

Map<Integer, Map<Character, Integer>>
              ↑                     ↑                ↑

上一步状态     看到的新字符   新状态

由于state是从0开始编号的,所以也可以用ArrayList<Map<Character, Integer>>. ArrayList的index代表上一步状态。

Finite State Machine该怎么建 →_→

我们还是从举例中找规律:

State3 看到b转移到2是因为“aba”和“a”有相同的后缀,即“右对齐”。因此state3看到b跟State1看到b的去向是一致的。事实上,state3除了看到c之后能成功向前推进一个state,看到其他任意一个字符都跟state1看到同样字符的去向一致。
再试验一个例子:state3看到a转移到1是因为“aba”与“a”有相同的后缀,因此除了看见“c”, state3看见其他任意字符与state1看见这个字符的去向一致。我们给state1起一个名字叫做state3的“影子状态”(之前看到某一篇大神的干货分享见到的这个名字)

state1是怎么找出来的呢?从直观上讲,我们希望找一个尽可能长的前缀与“aba”有相同的后缀。

再举例:eg. P = “aaab”.

“aaa” 与 “aa” 有common suffix,那么上一步是state3时,若当前看到的字符不是“b”, 去向就和state2看到同样字符的去向一致。虽然“aaa” 与 “a”也有相同的后缀,但不是最长的。

漫漫地我们的目的开始明晰了。找状态 i 的“影子状态 k ”就是要找与P的前 i 个字符具有公共后缀的P的最大前缀的长度。若到了状态 i 以后没有见到继续向前匹配的字符,就退回状态k,按照状态 k 的转移图决定去向。

补课

刚才看到各种前缀后缀以及有公共后缀的最长前缀是不是要被整蒙了?下面先补补课吧~ (读完上一段感觉依旧爽歪歪的可以跳过这一段哈~)

  1. Prefix

P[0,…m-1] is a pattern. Pk = P[0,…k-1] is the k-th prefix of P.
eg. P=“abac”. P0 = “”, P1 = “a”, P2 = “ab”, P3 = “aba”, P4 = “abac”

  1. Suffix Function

Given pattern P and text x, σ\sigmaσ(x) is the largest k such that Pk is a suffix of x (or Pk and x have common suffix). σ\sigmaσ(x) = 跟x有公共后缀的P的最大前缀长度。上例子:
eg. P = “abac”
x = “abaaaba” → σ\sigmaσ(x) = 3
x = “abaa” → σ\sigmaσ(x) = 1
x = "abab → σ\sigmaσ(x) =2

---------------------我是宣告补课结束的分割线---------------------

终于要拨云见日啦!

纵观这么长的知识总结,到现在可以梳理出很清晰的字符串匹配算法思路了

Step 1: 算每一个状态 i 的影子向状态 shadow[i]. (注意shadow[i] < i).

用补课里的suffix function 来描述shadow[i]代表啥就是:Regard P[0,…i-1] as text, and regard P[0,…i-2] = pattern. Then shadow[i] = σ\sigmaσ(text) given pattern.

eg. P=“abaaba”, i=5. At i=5, regard P[0,…4] = “abaab” as text, and regard “abaa” as pattern. Text and P2 = “ab” have common suffix and k=2 is the largest number such that Pk and text have common suffic. So, shadow[5] = k = σ\sigmaσ(P[0,…4]) = 2

具体实现:


shadow[0] = 0; shadow[1] = 0;
// 必须先填好shadow[1].
// 因为如果放到循环里的话,第一个字符将会等于状态1的影子状态(0)的下一个字符,
// 于是shadow[1]就会被写成1,随后每个shadow[i]都会被写成i。
for i=2 to m-1 {// 注意这块的i-1容易弄迷糊。// i代表状态i,就是已经匹配了P开头的多少个字符shadow[i-1],不是index。// shadow[i-1]代表上一个状态的“影子状态”,是在上一步循环算好的。// 代表在上一步已经匹配了shadow[i-1]个字符,所以接下来要看第shadow[i-1]+1 个字符了,它的index又刚好是shadow[i-1]// P[i-1]其实是即将算状态i的影子的时候刚刚见到的第i个字符,它的index是i-1。if (P[i-1]==P[shadow[i-1]]{shadow[i] = shadow[i-1] + 1;}else{int X = shadow[i-1];while(true){X = shadow[X];if(needle.charAt(i-1)==needle.charAt(X)){shadow[i] = X+1; break;}if(X==0) {if(needle.charAt(i-1)==needle.charAt(0)){shadow[i] = 1; break;}else {shadow[i] = 0; break;}}}}
}

影子状态的算法框架是动态规划,相当于填一个一维的shadow 表。不过每一次循环的时候,并不是靠表格前一个以及新看到的字符就可以了的,有可能按照“前任影子”往前跳好几格才能填完当前格。

因此,填每个格子的时候最坏的时间复杂度都是O(m), 填完整个shadow表需要O(m^2)。

Step2:根据shadow表建立Finite State Machine

依旧遵循动态规划的思想,假设已经到达了状态i, 新看到了字符 c,我们要建立的 ArrayList<Map<Character, Integer>> 叫 FSM。这个代码只写了其中一个循环步,完整代码可以看 Leetcode Implement strStr 题解与代码实现的文章。

FSM[i] = new HashMap();
if(P[i] == c) {FSM[i].put(c, i+1);}
else {int k = shadow[i];int new_state = FSM[k].get(c);FSM[i].put(c, new_state);
}

FSM的创建需要对每个状态,以及在这个状态基础之上遇到每个可能的新字符进行迭代,需要的时间是O(m|Σ\SigmaΣ|)。

Step3: 拿着FSM把Text扫描一遍

定义table[i] = 读到text[i] 时的state,即加上text[i],已经匹配了P开头的几个字符。每读一个新字符,就根据FSM定义的转移方式得到更新后的state。table[i] = m 代表text[i-m+1,…i]完全匹配。这个时候就已经不需要P了哟~ 全部信息FSM都已经保存啦!

int state = 0;
for i=0 to n-1{state = FSM[state].get(text[i])if (state==m) {return text[i-m+1,...i]; }
}
return null; // 没有找到匹配

看完“补课”的小伙伴可以发现,table[i] = σ\sigmaσ(x), where x = Text[0,…i]. 填完table以后,有多少个 i such that table[i] = m, 就代表多少个完全匹配。table[i] = m ⇐⇒ i=m+1 is a valid shift.

Step3需要的时间是O(n)。跟O(n)比,前两步的时间复杂度O(m^2)和O(m|Σ\SigmaΣ|)都可以忽略,因为在实际中,n可能是好几页(或一整个文档)的长度,而m一般就是一个单词或者短语的长度,字母表Σ\SigmaΣ的长度也就是几十,并不会再大了(说的是英语,中文的话字典size确实挺大的)。

Bingo!万!事!大!吉!啦!!!现在又有了一种吸饱精神食粮的成就感(^-^)!

总结:All things 趋于大同

详细整理这个算法的时候意识到了可以以一概全的大抽象框架 ⇒ 其实动态规划的思想,跟 “Reuse Information" 的策略如出一辙呀!

动态规划就是根据以前已经填好的格子 (state) 的信息,以及当前step看到的新内容 (action) ,根据一种 policy 决定当前格子的内容 (new state) 。简单的动态规划填每一格估计只需要与它相邻的前一格的信息就可以了,但是今天这个问题需要回退到之前某个格子,比如建立FSM的时候;或者回退好几格,比如在算影子状态这个“子问题”的时候。这个“回退”的路线怎么走要么提前算好,要么就是一点一点往前迭代(有人会担心迭代浪费时间,但前面说了是在算子问题的时候,这时候问题的scale不会很大)。迭代虽然存储很多格子的信息会耗费空间,但是会保证最后解决“主问题”的时候填每一格的时间都是O(1)!

读者看到上一段我把state,action,policy和new state标亮了,就大概能意识到,原来动态规划和search也扯到一起了!可以说动态规划其实就是一种search。并且今天这个算法感觉绕来绕去步骤这么多,其实是因为这次的动态规划的policy比较复杂!一般的动态规划问题在自己脑子里想想就想出来了,无非就是这种感觉:

if (i==0 && j==0) { table[i][j] = ...; }
else if (i==0) { ... }
else if (j==0) { ... }
else { table[i][j] = Math.min(table[i-1][j-1] ...); }

但今天我们“主问题”的policy需要用FSM,然后这个FSM还得自己建立,而建FSM的时候,policy得用到shadow表,这个shadow表也还得自己先建好。这就是为啥String matching算法一开始看的云里雾里觉得纷繁缭绕23333.

今天这个问题的三步:算shadow表,算FSM, 最后解决主问题,其实全都是动态规划,有一种兜兜转转最后还是回到的最初的起点的感觉~ 不过,以后再也不敢说动态规划很简单了红红火火恍恍惚惚。

写在结尾

这是芝麻挞第一篇以自己的思路为主的知识整理,以后希望还要多多学习然后总结归纳。把知识学厚的过程时自己积累、丰富自身的过程,再把知识学薄就是梳理、抽象、输出知识的过程。

本文如果有哪些不严谨的地方,欢迎在评论区指正!如果小伙伴有哪里觉得讲的不清楚或者太啰嗦的也欢迎指出!

嘻嘻第一篇自己写的技术博客要发布了美滋滋~

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

  1. Boyer-Moore 字符串匹配算法

    字符串匹配问题的形式定义: 文本(Text)是一个长度为 n 的数组 T[1..n]: 模式(Pattern)是一个长度为 m 且 m≤n 的数组 P[1..m]: T 和 P 中的元素都属于有限的字 ...

  2. 数据结构与算法之美笔记——基础篇(下):图、字符串匹配算法(BF 算法和 RK 算法、BM 算法和 KMP 算法 、Trie 树和 AC 自动机)

    图 如何存储微博.微信等社交网络中的好友关系?图.实际上,涉及图的算法有很多,也非常复杂,比如图的搜索.最短路径.最小生成树.二分图等等.我们今天聚焦在图存储这一方面,后面会分好几节来依次讲解图相关的 ...

  3. 字符串匹配算法——JavaScript

    字符串匹配算法--javascript 文章目录 字符串匹配算法--javascript 字符串匹配 BF算法 (暴力匹配) √ KMP算法 √ BM算法 **坏字符规则** 好后缀规则 Trid树( ...

  4. 这可能是全网最好的字符串匹配算法讲解

    点击上方 好好学java ,选择 星标 公众号重磅资讯,干货,第一时间送达 今日推荐:14 个 github 项目!个人原创100W +访问量博客:点击前往,查看更多 为保证代码严谨性,文中所有代码均 ...

  5. 字符串匹配算法 -- BM(Boyer-Moore) 和 KMP(Knuth-Morris-Pratt)详细设计及实现

    文章目录 1. 算法背景 2. BM(Boyer-Moore)算法 2.1 坏字符规则(bad character rule) 2.2 好后缀规则(good suffix shift) 2.3 复杂度 ...

  6. 字符串匹配算法KMP算法

    数据结构中讲到关于字符串匹配算法时,提到朴素匹配算法,和KMP匹配算法. 朴素匹配算法就是简单的一个一个匹配字符,如果遇到不匹配字符那么就在源字符串中迭代下一个位置一个一个的匹配,这样计算起来会有很多 ...

  7. 文本字符分析python_Python实现字符串匹配算法代码示例

    字符串匹配存在的问题 Python中在一个长字符串中查找子串是否存在可以用两种方法:一是str的find()函数,find()函数只返回子串匹配到的起始位置,若没有,则返回-1:二是re模块的find ...

  8. 字符串匹配算法之KMP

    目录 需求 基础知识 逻辑解析 源码实现 需求 先简单描述溪源曾经遇到的需求: 需求一:项目结果文件中实验结论可能会存在未知类型.转换错误.空指针.超过索引长度等等.这里是类比需求,用日常开发中常出现 ...

  9. ZZL字符串匹配算法

    分类: 算法艺术2009-12-31 13:25 2579人阅读 评论(4) 收藏 举报 算法standards存储数据结构搜索引擎语言 转载一篇关于字符串匹配算法ZZL的论文, 图片有点问题,将就着 ...

最新文章

  1. 安卓如何实现多级结构树_数据结构-树(树基本实现C++)
  2. [转]Git忽略提交规则 - .gitignore配置运维总结
  3. 熟悉常用的HBase操作
  4. Qt中rcc工具简介
  5. ansible提权操作
  6. python基础——错误处理
  7. python求散点曲线下方面积
  8. CodeIgniter 合作 Authorize.net
  9. 《王者荣耀》宣布将推独立女子电竞赛事:跟进奥运会
  10. Swiper 滚动插件
  11. 【C/C++】【VS开发】结构体存储空间数据对齐说明
  12. 【大数据部落】 17年房贷市场数据调研报告
  13. PMP培训机构哪家好,求推荐?
  14. 修航片调卫片,不会PS的GISer不是一个好“美工“
  15. 常见工具识别集锦---Windows应急响应工具
  16. 宋代词人前十名都有谁?第一名更是震铄古今最全能的大文豪
  17. WPF 设置窗口不跟随触摸惯性拖动抖动
  18. 基于Spring Boot的讲师积分管理系统(毕业设计,毕设)
  19. chrome 浏览器开发者工具之网络面板
  20. Linux 磁盘- 存储

热门文章

  1. 安全威胁无孔不入:基于Linux系统的病毒(转)
  2. ssm框架通用代码生成工具
  3. linux系统之网络防火墙(firewalld服务和iptables服务)
  4. 机器学习中,对于数据的预处理是否是测试集和训练集一起进行?
  5. H5C3新特性简单总结
  6. DoS网络攻击的类型
  7. 东南大学计算机专业工程博士,东南大学计算机考博 - 考博 - 小木虫 - 学术 科研 互动社区...
  8. UML 类图关系-图解(转自《大话设计模式》插图)
  9. 详 mpls option a b c产生背景 及实验
  10. python 归一化feed-dict程序代码_深度学习-中国大学mooc-题库零氪