KMP

基本思想

KMP 基本思想:匹配失败时,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,移到第一个可能匹配的位置。

比如:
母串:ABCDABC......
子串:ABCDABD......
当第 7 位 C 与 D 不匹配时,根据已知信息将子串移动到有可能可以匹配的位置,移动位数 = 已匹配的字符数 - 对应的部分匹配值

搜索词 A B C D A B D
部分匹配值 0 0 0 0 1 2 0
  • 已匹配的字符数 = 6
  • 对应的部分匹配值 = 2
  • 移动位数 = 已匹配的字符数 - 对应的部分匹配值 = 4

部分匹配值

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。

  • "A" 的前缀和后缀都为空集,共有元素的长度为 0;
  • "AB" 的前缀为 [A],后缀为 [B],共有元素的长度为 0;
  • "ABC" 的前缀为 [A, AB],后缀为 [BC, C],共有元素的长度 0;
  • "ABCD" 的前缀为 [A, AB, ABC],后缀为 [BCD, CD, D],共有元素的长度为 0;
  • "ABCDA" 的前缀为 [A, AB, ABC, ABCD],后缀为 [BCDA, CDA, DA, A],共有元素为 "A",长度为 1;
  • "ABCDAB" 的前缀为 [A, AB, ABC, ABCD, ABCDA],后缀为 [BCDAB, CDAB, DAB, AB, B],共有元素为 "AB",长度为 2;
  • "ABCDABD" 的前缀为 [A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为 [BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为 0。

部分匹配实质

有时候,字符串头部和尾部会有重复。比如,"ABCDAB" 之中有两个 "AB",那么它的"部分匹配值"就是 2("AB" 的长度)。搜索词移动的时候,第一个 "AB" 向后移动 4 位(字符串长度 - 部分匹配值),就可以来到第二个 "AB" 的位置。

部分匹配值的选取实质上是基于贪心思想

移动后的母串与子串的匹配部分("ABCDAB")的关系必须保证:母串的后缀等于子串的前缀,且尽量长。

比如,母串 ("ABCDABC") 与子串 ("ABCDABD") 在第 7 位不匹配时,要把子串移到第一个可能匹配的位置。在已匹配的 "ABCDAB" 中,第一个可能匹配的位置便是第 5 位 "A" 。

第一个可能匹配的位置实质上就是匹配部分(子串前缀 "ABCDAB")"前缀"和"后缀"的最长的共有元素的首部。推导如下:

移动后的母串与子串的关系必须保证:母串的后缀 X 等于子串的前缀 Y,且尽量长 --- (1)
既然属于匹配部分,那么母串的该后缀 X 也就等于子串的后缀 Z --- (2)
由 (1) (2) 联合可得,子串的前缀 Y 也就等于子串的后缀 Z
所以,匹配本身与母串无关,只与子串有关

KMP 实现

设 P[j] 为子串匹配部分 B[1 ... j] 的部分匹配值,则 P[j] 应该是所有满足 B[0 ... P[j] - 1] = B[j - P[j] + 1 ... j] 的最大值。由上面的推导可知,P[j] 与母串 A 无关,可单纯从子串 B 推导而来。

那么,如何快速地预处理 P 数组呢?我们可以通过 P[0],P[1],…,P[j - 1] 的值来获得 P[j] 的值。
对于 B = "ababacb",假如我们已经求出了 P[0],P[1],P[2] 和 P[3],看看我们应该怎么求出 P[4] 和 P[5]

    0 1 2 3 4 5 6
B = a b a b a c b
P = 0 0 1 2 ? ?

P[3] = 2,那么 P[4] 显然等于 P[3] + 1,因为由 P[3] 可以知道,B[0, 1] 已经和 B[2, 3] 相等了,现在又有 B[2] = B[4],所以 P[4] 可以由 P[3] 后面加一个字符得到。

    0 1 2 3 4 5 6
B = a b a b a c b
P = 0 0 1 2 3 ?

B[ P[4] + 1 ] != B[5] 。那么,我们要考虑“退一步”了。我们考虑 P[5] 是否有可能由 P[4] 的情况所包含的子串得到,即是否 P[5] = P[ P[4] ] + 1。P[4] = 3 是因为 B[0 ... 2] 和 B[2 ... 4] 都是 "aba";而 P[2] = 1 则告诉我们,B[0]、B[2] 和 B[4] 都是 "a" 。既然 P[5] 不能由 P[4] 得到,或许可以由 P[2] 得到(如果 B[1] 恰好和 B[5] 相等的话,P[5] 就等于 P[2] + 1 了)。显然,P[5] 也不能通过 P[2] 得到,因为 B[1] != B[5] 。事实上,这样一直推到 P[0] 也不行,最后,我们得到,P[5] = 0 。

  • 如果匹配成功,P[j] = P[j - 1] + 1
  • 如果匹配失败,j 倒退到 P[j - 1],再继续匹配,直到 j = 0 为止
// getNext P[i]: 满足 B[0 ... P[i] - 1] = B[i - P[i] + 1 ... i] 的最大值(即匹配值),P[i] = 0 时,表示匹配值为 0,不能再倒退
let j = 0, P = [];
P[0] = 0;
for (let i = 1; i < B.length; i ++) {
  while (j && B[j] !== B[i]) j = P[j - 1];
  if (B[i] === B[j]) j ++;
  P[i] = j;
}

// j 表示已经匹配的长度,即 A[i - j + 1 ... i] = B[0 ... j - 1],如果匹配失败,倒退到 P[j - 1]
j = 0;
for (let i = 0; i < A.length; i ++) {
  while (j && A[i] !== B[j]) j = P[j - 1];
  if (A[i] === B[j]) j ++;
  if (j === B.length) {
    console.log('Pattern occurs with shift ', i - B.length + 1);
    j = P[j - 1];
  }
}

时间复杂度

KMP 的时间复杂度分析可谓摊还分析的典型。我们从上述程序的 j 值入手。每一次执行 while 循环都会使 j 减小(但不能减成负的),而另外的改变 j 值的地方只有一行。每次执行了这一行,j 都只能加1;因此,整个过程中 j 最多加了 n 个 1。于是,j 最多只有 n 次减小的机会(j 值减小的次数当然不能超过 n,因为 j 永远是非负整数)。这告诉我们,while 循环总共最多执行了 n 次。按照摊还分析的说法,平摊到每次 for 循环中后,一次 for 循环的复杂度为 O(1) 。整个过程显然是 O(n) 的。这样的分析对于后面 P 数组预处理的过程同样有效,同样可以得到预处理过程的复杂度为 O(m) 。

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

推荐阅读更多精彩内容

  • 字符串匹配KMP算法详解 1. 引言 以前看过很多次KMP算法,一直觉得很有用,但都没有搞明白,一方面是网上很少有...
    张晨辉Allen阅读 2,385评论 0 3
  • 数据结构与算法--KMP算法查找子字符串 部分内容和图片来自这三篇文章: 这篇文章、这篇文章、还有这篇他们写得非常...
    sunhaiyu阅读 1,723评论 1 21
  • 数据结构 第8讲 KMP算法 讲这个算法之前,我们首先了解几个概念: 串:又称字符串,是由零个或多个字符组成的有限...
    rainchxy阅读 1,268评论 0 3
  • title: 串的模式匹配算法之kmptags: 数据结构与算法之美author: 辰砂tj 1.引言 首先我们需...
    tojian阅读 957评论 0 0
  • 文章大纲:1.KMP算法概念2.KMP算法中最核心的next[] 数组是如何生成的3.使用KMP算法 匹配字符串 ...
    柠檬乌冬面阅读 807评论 0 3