KMP(The Knuth-Morris-Pratt Algorithm)算法用于字符串匹配,从字符串中找出给定的子字符串。但它并不是很好理解和掌握。而理解它概念中的部分匹配表,是理解 KMP 算法的关键。

这里的讨论绕开其背后晦涩难懂的逻辑,着重从其运用上来理解它。

字符串查找

比如从字符串 abcdef 中找出 abcdg 子字符串。

朴素的解法,我们可以这样做,

  • 分别取出第一位进行匹配,如果相同再取出各自的第二位。
  • 如果不同,则将索引后移一位,从总字符串第二位开始,重复步骤一。

这种朴素解法的弊端在于,每次匹配失败,索引只后移一位,有很多冗余操作,效率不高。

在进行第一轮匹配中,即索引为 0 时,我们能够匹配出前四个字符 abcd 是相等的,后面发现想要的 g 与真实的 e 不符,标志着索引为 0 的情况匹配失败,开始查看索引为 1 时,但因为我们在第一轮匹配中,已经知道了总字符串中前四个字符的长相,但还是需要重复地挨个进行匹配。

部分匹配表/Partial Match Table

以长度为 8 的字符串 abababca,为例,其部分匹配表格为:

char:  | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |

其中 value 行便是部分匹配表的值。

子集

对于上面示例字符串,假如我们观察第 index 为 2 的位置,那么我们得到了字符串的一个子集 aba,如果我们观察 index 为 7 的位置,那得到的是整个字符串,这点是很显然的。当我们观察的位置不同时,表示我们关注的字符串中的子集不同,因为子字符串发生了变化。

前缀 & 后缀

对于给定的字符串,从末尾开始去掉一个或多个字符,剩下的部分都叫作该字符串的真前缀(Proper prefix),后面简称前缀。这里「真」不是「真·前缀」的意思,联想一下数学里面集合的「真子集」。比如 banana,其前缀有:

  • b
  • ba
  • ban
  • bana
  • banan

同理,从首部开始,去掉一个或多个字条,剩下的部分是该字符串的真后缀(Proper suffix)。还是 banana,其后缀有:

  • anana
  • nana
  • ana
  • na
  • a

部分匹配值

可以看到,所有前缀和后缀在数量上是对称的,那么我们可以从前缀中找出一个,与后缀进行匹配,先不关心做这个匹配的意义。以最开始的文本 abababca 为例。

假如我们观察 index 为 2 的位置,此时子字符串为 aba,其前后缀分别为:

  • 前缀:aab
  • 后缀:baa

将前缀依次在后缀中去匹配,这里前后缀列表中能够匹配上的只有 a 这个子字符串,其长度为 1,所以将这个观测结果填入表中记下来,与开始看到的部分匹配表吻合了。

再比如来观察 index 为 3 的位置,此时得到的子字符串为 abab,此时的前后缀为:

  • 前缀:aababa
  • 后缀:bababb

此时可观察出其匹配项为 ab,长度为 2,也与上面部分匹配表中的值吻合。

再比如来观察 index 为 5 的位置,此时子字符串为 ababab,前后缀为:

  • 前缀:aababaababababa
  • 后缀:bababababbababb

然后拿前缀中每个元素与后缀中的元素进行匹配,最后找出有两个匹配项,

  • ab
  • abab

我们取长的这个 abab,其长度为 4。

所以现在再来看上面的部分匹配表,一是能理解其值是怎么来的,二是能理解其表示的意义,即,所有前缀与后缀的匹配项中长度最长的那一个的长度。

当我们继续,进行到 index 为 6 时,子字符串为 abababc,可以预见,前后缀中找不到匹配。因为所有前缀都不包含 c,而所有后缀都包含 c。所以此时部分匹配值为 0。

再继续就到字符串末尾了,即整个字符串 abababca。也可以预见,因为所有前缀都以 a 开始,并且所有后缀都以 a 结尾,所以此时的部分匹配值最少为 1。继续会发现,因为后面的后缀开始有 c 的加入,使得后缀都包含 ca,而前缀中能够包含 c 的只有 abababc,而该长度 7 与同等长度的后缀 bababca 不匹配。至此就可以得出结论,匹配结果就是 1,没有更长的匹配了。

部分匹配表的使用

利用上面的部分匹配值,我们在进行字符串查找时,不必每次失败后只移动一位,而是可以移动多位,去掉一些冗余的匹配。这里有个公式如下:

If a partial match of length partial_match_length is found and table[partial_match_length] > 1, we may skip ahead partial_match_length - table[partial_match_length - 1] characters.

如果匹配过程中,匹配到了部分值为 partial_match_length,即目前找出前 partial_match_length 个字符是匹配的,将这个长度减一作为部分匹配表格中的 index 代入,查找其对应的 valuetable[partial_match_length-1],那么我们可以向前移动的步长为 partial_match_length - table[partial_match_length - 1]

下面是本文开始时的那个部分匹配表:

char:  | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |

假设需要从 bacbababaabcbab 中查找 abababca,根据上面的公式我们来走一遍。

首次匹配发生在总字符串的第二个字符,

bacbababaabcbab
|
abababca

此时匹配的长度为 1,部分匹配表中索引为 1-1=0 的位置对应的部分匹配值为 0,所以我们可以向前移动的距离是 1-0 1。其实也相当于没有跳跃,就是正常的本次匹配失败,索引后移一位的情况。这里没有节省任何成本。

继续直到再次发生匹配,此时匹配到的情况如下:

bacbababaabcbab
|||||
abababca

现在匹配到的长度是 5,部分匹配表中 5-1=4 对应的部分匹配值为 3,所以我们可以向前移动 5-3=2,此时一下子就可以移动两位了。

    上一次的位置
| 最新移动到的位置
| |
bacbababaabcbab
xx|||
abababca

此时匹配到的长度为 3, 查找到 table[partial_match_length-1] 即 index 为 2 对应的值为 1,所以可向前移动的距离为 3-1=2。

bacbababaabcbab
xx|
abababca

此时我们需要查找的字符串其长度已经超出剩余可用来匹配的字符串了,所以可直接结束匹配,得到结论:没有查找到结果。

JavaScript 中的实现

以下是来自 trekhleb/javascript-algorithms 中 JavaScript 版本的 KMP 算法实现:

/**
* @see https://www.youtube.com/watch?v=GTJr8OvyEVQ
* @param {string} word
* @return {number[]}
*/
function buildPatternTable(word) {
const patternTable = [0];
let prefixIndex = 0;
let suffixIndex = 1; while (suffixIndex < word.length) {

if (word[prefixIndex] = word[suffixIndex]) {

patternTable[suffixIndex] = prefixIndex + 1;

suffixIndex += 1;

prefixIndex += 1;

} else if (prefixIndex = 0) {

patternTable[suffixIndex] = 0;

suffixIndex += 1;

} else {

prefixIndex = patternTable[prefixIndex - 1];

}

} return patternTable;

} /**

* @param {string} text

* @param {string} word

* @return {number}

*/

export default function knuthMorrisPratt(text, word) {

if (word.length === 0) {

return 0;

} let textIndex = 0;

let wordIndex = 0; const patternTable = buildPatternTable(word); while (textIndex < text.length) {

if (text[textIndex] = word[wordIndex]) {

// We've found a match.

if (wordIndex = word.length - 1) {

return (textIndex - word.length) + 1;

}

wordIndex += 1;

textIndex += 1;

} else if (wordIndex > 0) {

wordIndex = patternTable[wordIndex - 1];

} else {

wordIndex = 0;

textIndex += 1;

}

} return -1;

}

时间复杂度

因为算法中涉及两部分字符串的线性对比,其时间复杂度为两字符串长度之和,假设需要搜索的关键词长度为 k,总字符串长度为 m,则时间复杂度为 O(k+m)。

相关资源

理解 KMP 算法的更多相关文章

  1. 深入理解KMP算法

    前言:本人最近在看<大话数据结构>字符串模式匹配算法的内容,但是看得很迷糊,这本书中这块的内容感觉基本是严蔚敏<数据结构>的一个翻版,此书中给出的代码实现确实非常精炼,但是个人 ...

  2. KMP算法详解 --- 彻头彻尾理解KMP算法

    前言 之前对kmp算法虽然了解它的原理,即求出P0···Pi的最大相同前后缀长度k. 但是问题在于如何求出这个最大前后缀长度呢? 我觉得网上很多帖子都说的不是很清楚,总感觉没有把那层纸戳破, 后来翻看 ...

  3. 从头到尾测地理解KMP算法【转】

    本文转载自:http://blog.csdn.net/v_july_v/article/details/7041827 1. 引言 本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP ...

  4. 深入理解KMP算法之续篇

    前言: 纠结于KMP已经两天了,相较于本人之前博客中提到的几篇博文,本人感觉这篇文章更清楚地说明了KMP算法的来龙去脉. http://www.cnblogs.com/goagent/archive/ ...

  5. 真正理解KMP算法

    作者:jostree 转载请注明出处 http://www.cnblogs.com/jostree/p/4403560.html 所谓KMP算法,就是判断一个模式串是否是一个字符串的子串,通常的算法当 ...

  6. 理解KMP算法

    母串:S[i] 模式串:T[i] 标记数组:Next[i](Next[i]表示T[0~i]最长前缀/后缀数) 先来讲一下最长前缀/后缀的概念 例如有字符串T[6]=abcabd接下来讨论的全部是真前缀 ...

  7. KMP算法 --- 深入理解next数组

    在KMP算法中有个数组,叫做前缀数组,也有的叫next数组. 每一个子串有一个固定的next数组,它记录着字符串匹配过程中失配情况下可以向前多跳几个字符. 当然它描述的也是子串的对称程度,程度越高,值 ...

  8. 从有限状态机的角度去理解Knuth-Morris-Pratt Algorithm(又叫KMP算法)

    转载请加上:http://www.cnblogs.com/courtier/p/4273193.html 在开始讲这个文章前的唠叨话: 1:首先,在阅读此篇文章之前,你至少要了解过,什么是有限状态机, ...

  9. KMP算法的一次理解

    1. 引言 在一个大的字符串中对一个小的子串进行定位称为字符串的模式匹配,这应该算是字符串中最重要的一个操作之一了.KMP本身不复杂,但网上绝大部分的文章把它讲混乱了.下面,咱们从暴力匹配算法讲起,随 ...

随机推荐

  1. Robot framework之元素定位实战

    1.1  id 和name 定位 Web页面都是由许多标签和元素组成的,每个标签或元素都是很多属性,好比一个人   id 和name 可以看作一个人的身份证号和姓名.下面看下教育局招生系统的用户名输入 ...

  2. dubbo实现原理简单介绍

    一.什么是dubbo   Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合).从服务模型的角度来看,     ...

  3. Mysql中外键的 Cascade ,NO ACTION ,Restrict ,SET NULL

    外键约束对子表的含义: 如果在父表中找不到候选键,则不允许在子表上进行insert/update 外键约束对父表的含义: 在父表上进行update/delete以更新或删除在子表中有一条或多条对应匹配 ...

  4. java位 、字节 、字符的梳理

    1字节(byte)=8位(bit) char=2字节(这是因为char是Java中的保留字,Java用的是Unicode,所以char在Java中是16位即2个字节的.) 附: String str= ...

  5. mysqldump详解

    Ⅰ.mysqldump的简单使用与注意点 1.1 基本参数 只备份innodb,用不了几个参数,记住下面几个即可,其他的没什么卵用 -A 备份所有的database -B 备份哪几个数据库 -R 备份 ...

  6. is not eligible for getting processed by all BeanPostProcessors

    BeanPostProcessor是控制Bean初始化开始和初始化结束的接口.换句话说实现BeanPostProcessor的bean会在其他bean初始化之前完成,BeanPostProcessor ...

  7. 值得注意的CSS属性

    文本TEXT letter-spacing 字符间距 word-spacing 字间距 line-height 行高 text-decoration 修饰(下划线) text-indent 首行缩进 ...

  8. Spring+SpringMVC+MyBatis+easyUI整合进阶篇(十五)阶段总结

    作者:13 GitHub:https://github.com/ZHENFENG13 版权声明:本文为原创文章,未经允许不得转载. 一 每个阶段在结尾时都会有一个阶段总结,在<SSM整合基础篇& ...

  9. Go缓存DNS

    Go里面的DNSclient没有带任何的缓存,语言层面没有任何的缓存,但是我们可以通过下面这样的代码来 这样当我们调用http.Client的时候就会采用这个缓存的ip了,里面用到的dnscache是 ...

  10. UOJ#37. 【清华集训2014】主旋律

    题目大意: 传送门 题解: 神题……Orz. 首先正难则反. 设$f_S$表示选取点集状态为s时,这部分图可以构成非强联通图的方案数. 设$p_{S,i}$表示点集s缩点后有i个入度为0点的方案数,保 ...