【重学数据结构与算法(JS)】字符串匹配算法(二)——KMP算法

前言

在上一篇文章字符串匹配算法(一)——BF算法
提到过,字符串匹配的思路是固定的:

  1. 模式串主串进行比较
    • 从前往后比较
    • 从后往前比较
  2. 匹配时,比较主串模式串的下一个位置
  3. 失配时,
    • 模式串中寻找一个合适的位置
      • 如果找到,从这个位置开始与主串当前失配位置进行比较
      • 如果未找到,从模式串的头部与主串失配位置的下一个位置进行比较
    • 主串中找到一个合适的位置,重新与模式串进行比较

优化在于其中的步骤,而KMP算法就是优化第3步失配时寻找模式串合适位置的操作

算法介绍和分析

那么如何寻找模式串中所谓合适的位置呢?可以先来看个栗子:

QQ20200114-214316.png
QQ20200114-214756.png

……

QQ20200114-215525.png
QQ20200114-220336.png

上面是 BF 匹配过程中从Nk到Nk+mm 次匹配过程,从中我们可以发现,从第 k 步到第 k+m 步时,指针 ij 又回到了相同的位置,且 第 k+m 步 更具有匹配的可能性,所以我们思考一下,是不是可以由第 k 步直接跳到第 k+m 步呢?如果可以,就可以减少 m-1 次比较,大大提升效率。再进一步思考,如果将整个匹配过程再看作是重复地由Nk直接到Nk+m的推进,那么每次重复时,模式串开始比较的位置就是我们所要找的合适的位置

如何寻找这些位置呢?我们可以把这个问题转化为求next数组的过程。

求 next 数组

我们再仔细观察下 Nk 和 Nk+m 两个状态

QQ20200114-214316.png
QQ20200114-220336.png

由于 Nk 状态下,模式串主串具有完全匹配的部分,且要达到 Nk+m 状态所需移动到的位置信息也存在于匹配的部分,因此我们可以无视掉主串,只看模式串即可得到next数组

再认真观察我们还能发现,Nk 状态不匹配时,Nk+m 状态本质上是将模式串中的另外一对 AB主串 达成之前的已匹配状态。所以求next数组的问题又可以转化为当m位置不匹配时,求m位置之前的子串的最大相同前后缀的问题。

首先要建立一个规则,具有前后缀的字符串长度至少为2,所以我们定义如果长度为0,则对应next数组值为-1,如果长度为1,值为0。下面举个栗子:

ABABABD

QQ20200115-204949.png

手工求这么看其实没什么难度,自己多写几个串练一遍就会了。

代码

学会如何手工求next数组之后,整个KMP算法的代码如何写呢?
还记得最开始提到要记住的一点吗?匹配思路是一样的,只是优化了失配后的操作。根据这一点,我们可以把BF算法的框架先搬过来:

carbon.png

这样是不是可以接下来去补全 getNext() 方法就可以了呢?我们来看一个特殊情况:

QQ20200115-211138.png
QQ20200115-211437.png

当处在Nk+m状态时,发现失配位置前的 AB 没有最长公共前后缀,于是只能退回到BF算法的做法,也就是i++;j=0。但是这和我们上面的框架代码不符,需要进行改造:

  • 每当 j = next[j] === -1时,也需要进入第一个分支,使得 i++;j++(-1 + 1 = 0),变相达到效果。

得到最终的框架代码:

carbon的副本.png

接下来就是进行对next数组的求解——完善 getNext()。这时候有的同学可能就会想对上述手工求法进行代码转化,可是万一模式串很长的话,那么这个时间复杂度就会变得相当的高,所以需要采用迭代法,利用每次所得的结果来求下一个结果,从而拼凑出next数组

我们假设某一时刻有一个状态Sk

QQ20200116-214003.png

此时我们已经求完了next[j]的值,如何去求next[j+1]呢?仔细观察状态图,发现:

  • 若Pk === Pj,则 Pj+1 前有next[j] + 1 = 4个相同的前后缀 P0P1Pk-jPk 和 Pj-kPj-k+1Pj-1Pj,也就是 next[j+1] = next[j] +1 = k + 1

再来看一个状态


QQ20200118-151300.png

同样是求完了next[j]的值,

  • 若Pk === Pj,对比 Pnext[k] 是否 等于 Pj;如果 Pnextn[k] === Pj,则next[j+1] = Pnextn[k] + 1 = k + 1

如果 Pnextn[k] !== Pj呢?

QQ20200118-151336.png

可以看到,

  • 如果Pnextn[k] !== Pj,则不断地递归前缀索引 k = next[k] 直到回到前缀第一个位置,则表示没有相同的前后缀,此时 j = -1,则 next[j+1] = Pnextn[k] + 1 = k + 1 = 0

根据以上分析,我们可以补充完 getNext()

carbon的副本2.png

再优化一下写法

carbon的副本3.png

至此,一个完整的KMP算法就写好了。

思考是否还有优化的空间

我们来看一个特殊的例子:

QQ20200118-155025.png

这是一个前缀相同的一个模式串,且我们已经求得了next数组,接下来我们模拟一下上面写好的程序进行的操作:

  1. j = 5,needle[5] !== haystack[i];next[j] = 4,j = next[j];
  2. j = 4,needle[4] !== haystack[i];next[j] = 3,j = next[j];
  3. j = 3,needle[3] !== haystack[i];next[j] = 2,j = next[j];
  4. j = 2,needle[2] !== haystack[i];next[j] = 1,j = next[j];
  5. j = 1,needle[1] !== haystack[i];next[j] = 0,j = next[j];
  6. j = 0,needle[0] !== haystack[i];next[j] = -1,j = next[j];
  7. j = -1, j++;i++;

我们发现由于前缀都是相等的,当第1步发现失配时,直接 j = -1 就可以了,也就是 next[5] = -1 即可。所以,优化点其实是体现在对next数组的优化,我们称之为nextVal数组

求nextVal数组

如何求nextVal数组呢?我们还是以上面的特殊情况为例,看两个状态:

QQ20200118-163223.png
QQ20200118-164922.png

此时我们已经求完了nextVal[j]的值,仔细观察状态图,发现:

  • 根据求next数组的过程,next[j + 1] = k + 1
    • 若Pj+1 !== Pnext[j + 1],在Pnext[j + 1]发生失配时,只要跳到Pj+1就有可能解决失配问题,则此时的 nextVal[j + 1] = next[j + 1]即可
    • 若Pj+1 === Pnext[j + 1],在Pnext[j + 1]发生失配时,跳到Pj+1就并不能解决失配问题,则此时应该继续回溯nextVal的next[j + 1]的位置上(由于是迭代求法,此时nextVal[next[j + 1]]上的值一定是通过nextVal[next2[j + 1]]求得了),即 nextVal[j + 1] = nextVal[next[j + 1]]

可以在 getNext() 的基础上得到以下代码:

carbon的副本4.png

next数组现在就已经是一个可有可无的工具人了,我们把去掉,得到下一版代码:

carbon (1).png

再进行以下优化得到最终代码:

carbon (2).png

总结

总的来说,KMP算法BF算法的字符串匹配思路在大方向上是没有区别的,只是引入了一个next数组nextVal数组来求得模式串合适的位置。只要理解了这两个数组的求法,也就基本理解了KMP算法

后记

“字符串匹配算法”是“重学数据结构与算法”系列笔记:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342