本文描述了单模式的字符串匹配的经典算法 KMP 算法的实现。首先对字符串匹配算法做简单的介绍,然后是 KMP 算法的实现描述,最后推荐两道简单的 ACM 模板题做练手用。
字符串匹配算法
字符串匹配(String Matchiing)也称字符串搜索(String Searching)是字符串算法中重要的一种,是指从一个大字符串或文本中找到模式串出现的位置。一个基本的字符串匹配算法分类如下:
- 单模式匹配:即每次算法执行只需匹配出一个模式串。
- 有限集合的多模式匹配:即算法需要同时找出多个模式串的匹配结果,而这个模式串集合是有限的。
- 无限集合的多模式匹配:如正则表达式的匹配。
单模式匹配最容易理解,构造也非常简单。一个最朴素的思路就是从文本的第一个字符顺次比较模式串,不匹配则重新从下一个字符开始匹配,直到文本末尾。Java 实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
但是这种算法,有明显的效率黑洞。因为每次匹配失败后,都会回到原来的匹配起点的下一个字符开始匹配,这些步骤很多情况下,并不是必要的。
实际上这些字符很有可能已经被读入了一次。理论上,如果我们能对所有被读入过的字符有足够的了解,那就能判定是否能避免再次读入一遍做匹配运算了。经典的 KMP 算法正是基于这点思考,对原有的蛮力算法做出了优化。
KMP 算法
网络上关于 KMP 算法的描述很多,其中个人觉得阮一峰老师的《字符串匹配的 KMP 算法》对 KMP 的描述最为简明和清晰。图例展示的算法流程更容易让人接受和理解。这里仅记录我所认为重点的知识点。
算法的思想
相比蛮力算法,KMP 算法预先计算出了一个哈希表,用来指导在匹配过程中匹配失败后尝试下次匹配的起始位置,以此避免重复的读入和匹配过程。这个哈希表被叫做“部分匹配值表(Particial match table)”,它的设计是算法精妙之处。
部分匹配值表
要理解部分匹配值表,就得先了解字符串的前缀(prefix)和后缀(postfix)。
- 前缀:除字符串最后一个字符以外的所有头部串的组合。
- 后缀:除字符串第一个字符以外的所有尾部串的组合。
- 部分匹配值:一个字符串的前缀和后缀中最长共有元素的长度。
举例说明:字符串ABCAB
- 前缀:{A, AB, ABC, ABCA}
- 后缀:{BCAB, CAB, AB, B}
- 部分匹配值:2 (AB)
而所谓的部分匹配值表,则为模式串的所有前缀以及其本身的部分匹配值。
举例如下:还是针对字符串ABCAB
,它的部分匹配值表为:
1 2 |
|
这代表着:字符串A B C A B
中,子串A B C
的部分匹配值为 0,而子串A B C A
的部分匹配值为 1,诸如此理。
算法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
理解算法实现时,有几点特别需要注意:
- 在生成部分匹配值数组的 kmpNext()方法中,第一层循环内,
i
是字符串的索引,而j
则在每次循环开始时代表了i
所指定字符之前的子串的部分匹配值。 - kmpNext()方法的内层 while()循环,是为了迭代得到让
i
指定字符匹配到的情况。有另外一种实现方案:不有用这一层循环,而是直接使用一层循环,在大循环内部做 j 值变更的判定即可。 - kmpNext()方法的 while()循环中,需要特别注意是
next[j -1]
,部分匹配值 j 对应到的是字符串中的第j-1
个字符。 - kmp()的循环代码和 kmpNext()部分匹配值表生成的循环代码很类似。两者使用了相同方式,在字符匹配失败后迭代获取新的可匹配情况,且都是利用了 next 数组。
其他
KMP 算法虽然能达到 O(M+N)的算法复杂度,但在实际使用中,KMP 算法的性能并不如 BM 算法强。
模板题
基础模板题
HDOJ 的 2203 题是一个能检验算法正确性的模板题。Java 实现的答案代码请戳这里。
延伸模板题
POJ 的 2406 题,对考察点做了巧妙的变形,对更深入的理解 KMP 中的部分匹配表(即 next 数组)很有帮助。Java 实现的答案代码请戳这里。
HDOJ 的 1867 题也属于 kmp 的变形。要求对 kmp 利用 next 数组进行比较的过程有清晰的认识。Java 实现的答案代码请戳这里。