KMP算法,是由Knuth,Morris,Pratt共同提出的模式匹配算法,其对于任何模式和目标序列,都可以在线性时间内完成匹配查找,而不会发生退化,是一个非常优秀的模式匹配算法。本文就对该算法进行基本的介绍,由于水平有限,解释不恰当的地方,欢迎指出谢谢。
在KMP算法中,对于每一个模式串我们会事先计算出模式串的内部匹配信息,在匹配失败时最大的移动模式串,以减少匹配次数。
比如,在简单的一次匹配失败后,我们会想将模式串尽量的右移和主串进行匹配。右移的距离在KMP算法中是如此计算的:在已经匹配的模式串子串中,找出最长的相同的前缀和后缀,然后移动使它们重叠。
KMP算法对于朴素匹配算法的改进是引入了一个跳转表next[]数组。以模式字符串ABABABB为例,其跳转表为:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
substr | A | B | A | B | A | B | B |
next | -1 | 0 | 0 | 1 | 2 | 3 | 4 |
求解过程如下:候选串即为最长的相同的前缀和后缀串
(1)当 j 等于 0 的时候发生不匹配,即模式串第一个字符与主串i位置不匹配,应将i跳过当前位置,从下一个位置和模式串第一个字符继续比较,此时将 next[0] 设置为-1来表示特殊情况;
(2)当 j 等于 1 时发生不匹配,此时匹配的子串 S 为“A”,候选串只能是空串,下次匹配还是从模式串的下标0开始比较,即 next[1] 设为0;
(3)当 j 等于 2 时发生不匹配,此时匹配的子串 S 为“AB”,候选串只能是空串,下次匹配还是从模式串的下标0开始比较,即 next[2] 设为0;
(4)当 j 等于 3 时发生不匹配,此时匹配的子串 S 为“ABA”,候选串为是“A”,因此 next[3] 设为1;
(5)当 j 等于 4 时发生不匹配,此时匹配的子串 S 为“ABAB”,候选串为是“AB”,因此 next[4] 设为2;
(6)当 j 等于 5 时发生不匹配,此时匹配的子串 S 为“ABABA”,候选串为是“ABA”和“A”,选择长度大的子串“ABA”,因此 next[5] 设为3;
(7)当j等于6时发生不匹配,此时匹配的子串 S 为“ABABAB”,候选串为是“ABAB”和“AB”,选择长度大的子串“ABAB”,因此 next[6] 设为4;
为了更进一步的说明next的求解:
再以模式串ababaaababaa为例:
以主串为ABABCABCABABABBC, 模式串为ABABABB为例的匹配过程
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
substr | A | B | A | B | C | A | B | C | A | B | A | B | A | B | B | C |
one | A | B | A | B | A(2) | B | B | |||||||||
two | A | B | A(0) | B | A | B | B | |||||||||
three | A | B | A(0) | B | A | B | B | |||||||||
four | A | B | A | B | A | B | B |
通过模式串的4次移动,完成了对目标串的模式匹配。这里以匹配的第一步为例,当模式串(从0计数)在第4个位置处不匹配,next[4] = 2, 所以从模式串的第2个位置从新与不匹配处比较,得到第二步。当第二步中的不匹配位置的next值0时,表示第一个字符匹配不成功,则i向后移动一个,模式串从头开始匹配,得到第三步。
在整个匹配过程中,无论模式串如何向后滑动,目标串的输入字符都在不会回溯,直到找到模式串,或者遍历整个目标串都没有发现匹配模式为止。
关于求解 next 数组的方法:
如何利用 next[n] 求解 next[n+1],减少重复计算呢?
求解 next[n+1] 的时候,由上边的分析可知,此时存在两个子串 a 和 b 是匹配的,即模式串中 0 到 k-1 的子串(对应 n 时的前缀)和子串 n-k 到 n-1 (对应 n 时的后缀)是相互匹配的,下面分两种情况求解 next[n+1]:
第一种情况:下标 k 处的字符与 n 处的字符匹配,则 0 到 k 位置的子串与 n-k 到 n 位置的子串匹配(例如 n+1 的情况(k=2)为:ABABA),显然有 next[n+1] = k + 1 = next[n] + 1
第二种情况:下标 k 处的字符与 n 处的字符不匹配,这样我们为了消除将0 到 k的子串与n处的不匹配,需要向前移动到 next[n] 处。假设 next[k] = k’,相当于找到了S串中的这么一对子串0~k’-1 和 n- (k’ - 1) 到n是匹配的,如果 k’ 处字符和 n 处字符匹配,这利用情况 1)中的办法求得 next[n+1] , 否则用同样的办法借助之前求得的 next 数组值来继续处理。(这一部分我也不是很懂?)
Java代码如下:
import java.util.Scanner;
public class KMPOfJava {
public static void main(String[] args) {
KMPOfJava kmp = new KMPOfJava();
Scanner in = new Scanner(System.in);
String str = "";
String substr = "";
int next[];
while (in.hasNext()){
str = in.nextLine();
substr = in.nextLine();
next = kmp.getNext(substr);
System.out.println(kmp.KMP(str, substr, next));
kmp.printNext(next);
}
}
/**
* 测试用例:
* 主串: ABABCABCACBAB
* 模式串:ABCAC
* 模式串:ABABAB
* 模式串:ABABABB
* 主串: abbababaaababaa
* 模式串:ababaaababaa
* */
/** KMP算法 */
public int KMP(String str, String substr, int next[]){
int i = 0, j = 0;
while (i < str.length() && j < substr.length()){
if (str.charAt(i) == substr.charAt(j)){//匹配
++i;
++j;
}else{//不匹配
j = next[j];//取出next数组对应j处的值
if (j == -1){//如果为第一个位置
j = 0;//从模式串的头部开始比较
++i;//主串后移一个位置
}
}
}
if (j == substr.length())
return i - substr.length();
else return -1;
}
/** 得到模式串的next数组 */
public int[] getNext(String substr){
int i = 0, j = -1;
//定义next数组,长度为模式串的长度
int next[] = new int[substr.length()];
//设置next初始位置为-1
next[0] = -1;
//求解next其他值(当前字符串的最长的相同的前缀和后缀)
while (i < substr.length() - 1){
//第一种情况:k处字符与n-k处字符匹配
if (j == -1 || substr.charAt(i) == substr.charAt(j)){
++i;
++j;
next[i] = j;
}else{//第二种情况:k处字符与n-k处字符不匹配
j = next[j];
}
}
return next;
}
/**打印next函数 */
public void printNext(int next[]){
for (int n : next){
System.out.print(n + " ");
}
System.out.println();
}
}