什么是KMP?
KMP全称为Knuth Morris Pratt算法,这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。KMP是一种高效的字符串匹配算法,用来在主字符串中查找模式字符串的位置(比如在**"hello,world"主串中查找"world"模式串**的位置)。
KMP算法原理
对比BF算法
我们先拿BF(Brute Force)算法做一下对比。BF算法是一种最朴素的暴力搜索算法。它的思想是在主串的[0, n-m]区间内依次截取长度为m的子串,看子串是否和模式串一样(n是主串的长度,m是子串的长度)。
BF的时间复杂度是O(N*N),存在很大优化空间。当模式串和主串匹配时,遇到模式串中某个字符不能匹配的情况,对于模式串中已经匹配过的那些字符,如果我们能找到一些规律,将模式串多往后移动几位,而不是像BF算法一样,每次把模式串移动一位,就可以提高算法的效率。
下面通过一个具体的例子来看看可以跳过的情况。比如主模式串是"ababaeaba",模式串是"ababacd",在BF算法中,遇到不匹配的情况是这样处理的:我们希望找到一些规律,遇到两个字符不匹配的情况时,希望可以多跳几个字符,减少比较次数。
KMP算法的思想
在模式串和主串匹配过程中,当遇到不匹配的字符时,对于主串和模式串中已对比过相同的前缀字符串,找到长度最长的相等前缀串,从而将模式串一次性滑动多位,并省略一些比较过程。在KMP算法中通过next数组来存储当两个字符不相等时模式串应该移动的位数。
KMP算法的next数组
next数组用来存模式串中每个前缀最长的能匹配前缀子串的结尾字符的下标。 next[i] = j 表示下标以i-j为起点,i为终点的后缀和下标以0为起点,j为终点的前缀相等,且此字符串的长度最长。用符号表示为p[0~j] == p[i-j~i]。下面以"ababacd"模式串为例,给出这个串的next数组。
KMP的Java代码实现
下面给出KMP算法的完整代码,里面有详细的注释。
public class KMP {
/**
* KMP算法.
* 在目标字符串中对搜索词进行搜索。
*
* @param t 目标字符串
* @param p 搜搜词
* @return 搜索词第一次匹配到的起始位置或-1
*/
public int kmp(String t, String p) {
char[] target = t.toCharArray();
char[] pattern = p.toCharArray();
// 目标字符串下标
int i = 0;
// 搜索词下标
int j = 0;
// 整体右移一位的部分匹配表
int[] next = getNext(pattern);
while (i < target.length && j < pattern.length) {
// j == -1 表示从搜索词最开始进行匹配
if (j == -1 || target[i] == pattern[j]) {
i++;
j++;
// 匹配失败时,查询“部分匹配表”,得到搜索词位置j以前的最大共同前后缀长度
// 将j移动到最大共同前后缀长度的后一位,然后再继续进行匹配
} else {
j = next[j];
}
}
// 搜索词每一位都能匹配成功,返回匹配的的起始位置
if (j == pattern.length)
return i - j;
else
return -1;
}
/**
* 生成部分匹配表.
* 生成搜索词的部分匹配表
*
* @param p 搜搜词
* @return 部分匹配表
*/
private int[] getNext(char[] p) {
int[] next = new int[p.length];
// 第一位设为-1,方便判断当前位置是否为搜索词的最开始
next[0] = -1;
int i = 0;
int j = -1;
while (i < p.length - 1) {
if (j == -1 || p[i] == p[j]) {
i++;
j++;
next[i] = j;
} else {
j = next[j];
}
}
return next;
}
}
KMP的时间复杂度
对于KMP,目标串任意length长度的子串,匹配时间最多为2*length,原因是KMP的目标串指针不会回溯。看朴素算法与KMP对待这样的一个匹配的行为:txt=“aaaab”, pat=“aaaaa”,失配时,朴素算法会将目标串指针移动到第2位,模式串重头开始匹配;而KMP只前移一位模式串,虽然还是会失配(最后模式串也要从头开始),但时间是线性的。
所以该算法时间复杂度为O(m+n)。