字符串匹配基础(下)—— KMP 算法
Ⅰ 前言
在前两节中,我详细讲了字符串匹配的三个算法,BF,RK 和 BM 算法,BM 算法可以说是这三个包括这篇要讲的 KMP 算法里最难最复杂的,也非常不好理解。但是 BM 算法却是工程中非常常用的一种高校字符串匹配算法。有统计说,它是最高效、最常用的字符串匹配算法。但是,在所有的字符串匹配算法中,要说最知名的一种,那肯定是非 KMP 算法莫属。很多时候,提到字符串匹配,我们首先想到的就是 KMP 算法。
尽管在实际开发中,我们几乎不太可能自己亲手实现一个 KMP 算法。但是,学习这个算法的思想,作为让你开拓眼界、锻炼下逻辑思维也是很不错的。
实际上,KMP 算法跟 BM 算法的本质是一样的。在 BM 算法中,我讲了好后缀和坏字符规则,现在我们就来借助 BM 算法的思路,来理解 KMP 算法。
如果你对 BM 算法还不了解,请跳转去看我下面的文章。
【数据结构与算法】->算法->字符串匹配基础(中)->BM算法->KMP 三倍性能的强大算法
【数据结构与算法】->算法->字符串匹配基础(上)->BF 算法 & RK 算法
Ⅱ KMP 算法基本原理
KMP 算法是根据三位作者(D.E.Knuth,J.H.Morris,Knuth Morris Pratt)的名字来命名的,算法的全称是 Knuth Morris Pratt 算法,简称 KMP 算法。
KMP 算法的核心思想,和 BM 算法非常相近。我们假设主串是 a,模式串是 b。在模式串与主串匹配的过程中,当遇到不可匹配的字符的时候,我们希望找到一些规律,可以将模式串往后多滑动几位,跳过那些肯定不会匹配的情况。
在 BM 算法中,我讲了好后缀和坏字符。这里我们可以类比一下,在模式串和主串匹配的过程中,把不能匹配的那个字符仍然叫作坏字符,把已经匹配的那段字符串叫作好前缀。
当遇到坏字符的时候,我们就要把模式串往后滑动,在滑动的过程中,只要模式串和好前缀有上下重合,前面几个字符的比较,就相当于拿好前缀的后缀子串,跟模式串的前缀子串在比较。这个比较过程能不能更高效一点?可不可以不要一个字符一个字符地比较?
KMP 算法就是在试图寻找一种规律:在模式串和主串匹配的过程中,当遇到坏字符后,对于已经比对过的好前缀,能否找到一种规律,将模式串一次性滑动很多位。
我们只需要拿好前缀本身,在它的后缀子串中,查找最长的那个可以跟好前缀的前缀子串匹配的。假设最长的可匹配的那部分前缀子串是 {v},长度是 k。我们把模式串一次性往后滑动 j-k 位,相当于,每次遇到坏字符的时候,我们就把 j 更新称 k,i 不变,然后继续比较。
为了表述起来方便,我把好前缀的所有后缀子串中,最长的可匹配前缀子串的那个后缀子串,叫作最长可匹配后缀子串;对应的前缀子串,叫作最长可匹配前缀子串。
如何来求好前缀的最长可匹配前缀和后缀子串呢?这个问题其实不涉及主串,只需要通过模式串本身就能求解。这和 BM 算法的思路是一致的,因为一个好前缀或者 BM 算法中的好后缀,都是主串和模式串共有的,所以我们都要对模式串进行预处理,以便提高代码效率。
类似 BM 算法中的 badChar
、suffix
、prefix
数组,KMP 算法也可以提前构建一个数组,用来存储模式串中每个前缀(这些前缀都有可能是好前缀)的最长可匹配前缀子串的结尾字符下标。我们把这个数组定义为 next
数组,很多书中还给这个数组起了一个名字,叫失效函数(failure function)。
数组的下标是每个前缀结尾字符下标,数组的值是这个前缀的最长可以匹配前缀子串的结尾字符下标。这句话比较拗口,我举一个例子,大家对着看,差不多就能理解了。
我还是详细地说一下这些值是怎么来的。
左边的表头为模式串前缀的这列,意思是把它们当作好前缀。因为我们现在是根据模式串来处理主串中的好前缀问题,所以我们把每个前缀都当作是好前缀,然后根据这个好前缀,来计算它的最长可匹配前缀子串结尾字符下标。
所以当好前缀是 a 的时候,它就没有前缀,更不会有可匹配前缀子串,所以 next[0] = -1;
当好前缀是 ab 的时候,它只有一个前缀子串 a,和它的后缀子串 b 不匹配,所以 next[1] = -1;
当好前缀是 aba 的时候,它的一个前缀是 a,和它的后缀 a 是匹配的;它的前缀 ab 和后缀 ba 不匹配,所以它的最长可匹配前缀子串就是 a,下标为 0,所以 next[2] = 0;
以此类推,next 数组的值就是这样计算的。有了这个数组,有很好实现 KMP 算法了。我们先假设 next 数组已经计算好了,直接可以用,根据这个我们给出 KMP 算法的框架。
/**
* KMP算法实现字符串匹配
* @param mainString 主串
* @param patternString 模式串
* @return 模式串在主串中的位置
*/
public static int kmp(char[] mainString, char[] patternString) {
int[] next = getNexts(patternString);
int j = 0;
for (int i = 0; i < mainString.length; i++) {
while (j > 0 && mainString[i] != patternString