KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。KMP算法的时间复杂度O(m+n)
案例
假设主串(下文中我们称作T)为:a b a a c a b a b c a c
模式串(下文中我们称作W)为:a b a b c
目的是在T中找寻是否存在W。
传统的字符串匹配方法
一个字母一个字母进行暴力破解。例如第一次进行匹配,当匹配到第四个字幕时,发生了不匹配,则将W向右移动一位,继续这个操作,直到完全匹配。
这个效率是极低的,每一次都是简单的往后移动一位。
最坏的情况下是当T为aaaaaab,W为aaab,这时每次操作都需要将aaa进行匹配一遍。
KMP算法
前期准备工作 构建prefix table
当我们使用KMP算法时,需要计算一个前缀表prefix table。
前缀是指除了最后一个字符以外,一个字符串的全部头部组合;
后缀 是指除了第一个字符以外,一个字符串的全部尾部组合。
所以ababc的前缀表如下:
然后我们把每一行都当成一个独立的字符串,寻找每个字符串的公共前后缀。
举例:拿abab这个字符串。这个字符串的长度为4。
这样abab这个字符串的公共前后缀长度为2,然后在前缀表中标明:
同理对于aba这个字符串,他的公共前后缀长度为1.
对于字符串ab来说,他的公共前后缀长度为0.
对于字符串a来说,他的公共前后缀长度也是0.
一般情况下,我们会在最前面补一个-1。这样我们就可以构建出一个前缀表:
我们这么写:
我们将W的每一个字符的索引也添加上:
利用前缀表匹配
当匹配到索引是3时:
这时出现了不匹配的现象,那么在匹配失败的情况下,查看对应的前缀表值是1,表示将索引是1的元素与当前匹配失败的元素对齐:
对齐调整后,仍然匹配失败,继续调整
然后我们继续移动:
然后继续移动,因为a对应的前缀表是-1,遇到-1时,直接右移一位,不需要匹配。
直到完全匹配上。
当T中不只有P一个匹配时
也就是说说把T中所有P找出来,就需要继续往后移动。也就是说,将最后一个前缀表对应的索引值进行移动:
然后继续往后匹配,发现已经到头了。
这样就找到了一段完全匹配的字符串。
极端情况
如果使用暴力匹配则会做很多无用功。
如果使用KMP时,构建索引表:
然后进行匹配:
当发生匹配失败时,找到对应的前缀表的索引3,将索引3移动到失败位置:
当移动到指定位置时,就不需要比较之前的元素了,直接从匹配失败的元素开始往后比较。
这样就成功了。
这里最重要的步骤就是构建前缀表。
KMP算法代码实现
上面我们介绍了KMP算法的原理,主要分为两部分,第一部分是构建生成Prefix Table,第二部分是根据Prefix Table完成搜索。
案例
匹配串:A B A B C A B A A
Prefix Table如下:
我们可以发现一个规律:
当第七行的最长公共前后缀长度是1时,如果要将第八行的公共前后缀变为2,那么就一定要在第八行A的最后面添加B,判断依据是第七行A后面的B;
而第八行的最长公共前后缀长度是2时,如果要将第九行的公共前后缀变为3,那么就一定要在第九行最后面添加A,判断依据是第八行AB后面的A。
假设:
已经知道第5位的公共前后缀长度是1,那么如何判断第6位是多少?
答案:因为第5位的公共前后缀长度是1,那么判断第6位的字母是不是B,如果是B,那么第6位的公共前后缀长度是2
但是, 如果不相等时,如上图,也就是说如何根据第3位的公共前后缀来判断第4位的公共前后缀呢?错误做法: 是将第3位的公共前后缀长度2,A与C不相等,则C的公共前后缀长度为0,虽然结果是对的,但是如何根据第7位的公共前后缀判断第8位的公共前后缀呢?
如果按照上面的做法,B与A不相等,所以第8位是0,答案是不对的,应该是1。
整个过程如下所示:
构建前缀表的代码如下:
public class KMP {
public static void prefix_table(char pattern[], int prefix[], int n){
prefix[0] = 0;
int i = 1;
int length = 0; //最长公共前后缀长度
while(i < n) {
// System.out.println("pattern[i]:" + pattern[i] + ",pattern[length]:" + pattern[length]+"," + (pattern[i] == pattern[length]));
if(pattern[i] == pattern[length]){
length ++;
prefix[i] = length;
i++;
}else {
if(length>0){
length = prefix[length-1];
}else {
prefix[i] = 0;
i++;
}
}
}
}
public static void main(String[] args) {
String str = "ABABCABAA";
char[] pattern = str.toCharArray();
int[] prefix_table = new int[9];
int n = 9;
prefix_table(pattern, prefix_table, n);
for(int i:prefix_table){
System.out.print(i);
}
}
}
程序打印输出结果为:001201231
构建完成前缀表之后,为了方便后面的匹配过程,把结果进行移位:
//为了方便后面的匹配,对前缀表进行移位
static void move_prefix_table(int[] prefix, int n) {
for (int i = n - 1; i > 0; i--) {
prefix[i] = prefix[i - 1];
}
prefix[0] = -1;
}
规定:n表示pattern的长度,m表示text原始字符串的长度。i表示text当前匹配位置,j表示pattern当前匹配位置。
当匹配成功之后,如下图,需要继续对P[j]进行prefix操作,
完整代码如下:
public class KMP {
public static void prefix_table(char pattern[], int prefix[], int n) {
prefix[0] = 0;
int i = 1;
int length = 0; //最长公共前后缀长度
while (i < n) {
// System.out.println("pattern[i]:" + pattern[i] + ",pattern[length]:" + pattern[length]+"," + (pattern[i] == pattern[length]));
if (pattern[i] == pattern[length]) {
length++;
prefix[i] = length;
i++;
} else {
if (length > 0) {
length = prefix[length - 1];
} else {
prefix[i] = 0;
i++;
}
}
}
}
//为了方便后面的匹配,对前缀表进行移位
static void move_prefix_table(int[] prefix, int n) {
for (int i = n - 1; i > 0; i--) {
prefix[i] = prefix[i - 1];
}
prefix[0] = -1;
}
static void kmp_search(char[] text, char[] pattern) {
int n = pattern.length;
int m = text.length;
int[] prefix = new int[n];
prefix_table(pattern, prefix, n);
move_prefix_table(prefix, n);
int i = 0;
int j = 0;
// text[i], len(text) = m
// pattern[j], len(pattern) = n
while (i < m) {
if(j == n-1 && text[i] == pattern[j]){
System.out.println("Found pattern at " + (i-j));
j = prefix[j];
}
if (text[i] == pattern[j]) {
i++;
j++;
} else {
j = prefix[j];
if (j == -1) {
j++;
i++;
}
}
}
}
public static void main(String[] args) {
String pattern_str = "ABABCABAA";
String text_str = "ABABABCABAABABABAB";
char[] pattern = pattern_str.toCharArray();
char[] text = text_str.toCharArray();
/*int[] prefix_table = new int[9];
int n = 9;
prefix_table(pattern, prefix_table, n);
move_prefix_table(prefix_table, n);
for (int i : prefix_table) {
System.out.print(i);
}*/
kmp_search(text, pattern);
}
}