Version: Next
KMP
KMP
是Knuth-Morris-Pratt
的简称,取自三位发明人的名字,于 1977 年提出
直接跳到下一个以 1
开头的位置
- 与蛮力法相比,KMP 的精妙之处在于:充分利用先前比较过的内容,可以聪明地跳过一些不必要比较的位置
KMP —— next 表的使用
- KMP 会预先根据
模式串
的内容生成一张next
表(一般是一个数组)- 对于模式串
ABCDABCE
,其next
表为:
模式串字符 A B C D A B C E 索引 0 1 2 3 4 5 6 7 元素 -1 0 0 0 0 1 2 3
如果遇到了在 模式串 的靠后位置匹配失败的情况
- 对于蛮力法,需要将
pi
清零,ti
回退,中间比较操作全部相当于白做- KMP 则会查询
next
表,根据模式串出现匹配失败的字符索引—— pi
,在next
进行查询。对于此处的pi = 7
和上文给出的next
表,明显可知此时从表中查出的值是3
- 大幅移动
模式串
,直接跳过文本串
的部分字符,通过pi - next[pi]
实现,此处等于7 - 3 = 4
- 接下来将
next[pi]
的值赋值给pi
,即pi = next[pi]
,此时pi = 3
。达到的效果为 从模式串先前实配的位置开始比较,先前匹配成功的字符直接不比了
KMP 核心原理
A
、B
为子串;c
、d
、e
为单个字符- 当
d
、e
失配时,如果希望模式串
能够一次性向右移动一大段距离,然后直接比较d
、c
字符- 前提条件是
A串
必须等于B串
- 前提条件是
- 因此,KMP 必须在失配字符
e
左边的子串中找出符合条件的A
、B
,从而得知向右移动的距离 - 向右移动的记录:
e左边字符串的长度 - A串的长度 = e的索引 - c的索引
- c的索引:等于
next[e的索引]
,所以向右移动的距离:e的索引 - next[e的索引]
总结
- 如果在
pi
位置失配,向右移动的距离是pi - next[pi]
,所以next[pi]
越小,移动幅度越大 next[pi]
是pi
左边子串的真前缀后缀的最大公共子串长度
KMP——真前缀后缀的最大公共子串长度
模式串 | 真前缀 | 真后缀 | 最大公共子串长度 |
---|---|---|---|
ABCDABCE | A、AB、ABC、ABCD、ABCDA、ABCDAB、ABCDABC | BCDABCE、CDABCE、DABCE、ABCE、BCE、CE、E | 0 |
ABCDABC | A、AB、ABC 、ABCD、ABCDA、ABCDAB | BCDABC、CDABC、DABC、ABC 、BC、C | 3 |
ABCDAB | A、AB 、ABC、ABCD、ABCDA | BCDAB、CDAB、DAB、AB 、B | 2 |
ABCDA | A 、AB、ABC、ABCD | BCDA、CDA、DA、A | 1 |
ABCD | A、AB、ABC | BCD、CD、D | 0 |
ABC | A、AB | BC、C | 0 |
AB | A | B | 0 |
A | - | - | 0 |
将上表压缩为一个数组
模式串字符 A B C D A B C E 最大公共子串长度 0 0 0 0 1 2 3 0
- 第一行,到哪个字符,就表示以该字符结尾的
子串
- 元素值表示该
子串
对应的子串长度例如:下图就表示
子串 ABCDABC
对应的真前缀后缀最大公共子串长度为3
得到 next 表
将最大公共子串长度都向右移动
1
位,首字符设置为-1
, 就得到了 next 表
模式串字符 A B C D A B C E 索引 0 1 2 3 4 5 6 7 元素 -1 0 0 0 0 1 2 3
构建 next 表
对于一个模式串,
n
、i
为图中两个字符的下标,则next[i] == n
,因为 next 表存储着i
索引对应字符左侧子串的真前缀后缀最大公共子串长度,该长度在数值上恰好等于索引n
- 如果
pattern[i] == pattern[n]
,索引i
位置的字符 与 索引n
位置的字符 相同,则:
next[i + 1] == n + 1
- 如果
pattern[i] != pattern[n]
,索引i
位置的字符 与 索引n
位置的字符 不相同,则:
- 在
A串
右侧虚拟放置一个字符,其下标索引为k
,则可认为A串
的长度为k
- 则
next[n] == k
,因为索引n
左侧的真前后缀最大公共子串此时为A串
,而A串
的长度为k
- 如果
pattern[i] == pattern[k]
,则如图所示,next[i + 1] = k + 1
- 如果
pattern[i] != pattern[k]
:将k
带入n
,重复执行2.
实现
KMP
public static int indexOf(String text, String pattern) {
if (text == null || pattern == null) return -1;
char[] textChars = text.toCharArray(); // 转成字符数组
char[] patternChars = pattern.toCharArray();
int tlen = textChars.length;
int plen = patternChars.length;
// 如果模式串长度大于 文本串长度 返回 -1
if (tlen == 0 || plen == 0 || plen > tlen) return -1;
// next 表
int[] next = next(pattern);
int pi = 0, ti = 0, lenDelta = tlen - plen;
while (pi < plen && ti - pi <= lenDelta) {
if (pi < 0 || textChars[ti] == patternChars[pi]) {
pi++;
ti++;
} else { // 失配
pi = next[pi];
}
}
return (pi == plen) ? (ti - pi) : -1;
}
/**
* 生成并返回 next 表
*
* @param pattern 模式串
* @return next 表
*/
private static int[] next(String pattern) {
char[] chars = pattern.toCharArray();
int len = chars.length;
int[] next = new int[chars.length];
next[0] = -1;
int i = 0;
int n = -1;
int iMax = len - 1; // 下表最大值为 长度 - 1
while (i < iMax) { // 没有=号,因为循环体里有 ++i
if (n < 0 || chars[i] == chars[n]) {
next[++i] = ++n;
} else {
n = next[n];
}
}
return next;
}