KMP算法是数据结构串部分最重要的算法。
它由Knuth、Morris和Pratt共同提出,取首字母称为KMP算法。
它相较于Brute-Force(暴力)算法有了很大的进步,主要是充分利用已经比较过的部分匹配信息。
针对的问题—串的模式匹配
设有两个串 t 和 p ,想在 t 中找到一个与 p 相等的子串。
通常 t 称为目标串(target string),把 p 称为模式串(pattern string),因此这种查找也叫模式匹配。模式匹配成功则返回找到的 t 中子串首字符的索引值,不成功则 t 中不存在模式串 p,返回-1。
KMP算法的思路
暴力算法
也稍微提一下暴力算法吧,不然体现不出KMP的优势,毕竟KMP算法虽然代码优美简洁,但思想“深邃”(看不懂 )。
Brute-Force算法采用穷举思想,对目标串 t 的每一个字符都比较过去,与 p 有一个不符合就往后走,再一个一个比较,直到找到匹配串或到达 t 的最后也没找到就说明不存在。设两串的长度分别为 n 和 m ,该算法的最好时间复杂度为O(m),最坏时间复杂度为O(n x m),可以证明平均时间复杂度为O(n x m)接近最坏时间复杂度。
KMP算法如何优化
暴力算法的缺点在于没有充分利用已经匹配过的信息,而是每次推倒重来。
KMP算法最最根本的思路也还是“以空间换时间”,引入next数组保存部分匹配信息,因此next数组也可称为部分匹配表。
在讲next数组的作用前,我们先来看一个概念“最长相等前后缀”。
最长相等前后缀:
字符串{abcsudabc}
前缀集合{a,ab,abc,abcs,abcsu,abcsud,abcsuda,abcsudab,abcsudabc}
后缀集合{c,bc,abc,dabc,udabc,sudabc,csudabc,bcsudabc,abcsudabc}
通过观察前后缀集合,在两集合中都有的最长子串(除了字符串本身)是abc,则abc称为最长相等前后缀
看懂最长相等前后缀就基本能理解next数组的作用了。next数组就是保存模式串 p 的各个子串的最长相等前后缀。例如模式串{abdabs},对应的next数组如下:
next数组下标 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
部分匹配值 | 0 | 0 | 0 | 1 | 2 | 0 |
保存该值的意义是将模式串 p 的前缀集合与目标串 t 的后缀集合对齐重合。
这样就充分利用了之前的匹配信息。
Java实现
注释比较详细,大家可以自己慢慢领会。
public class KMPalgorithm {
/**
* @param target 目标串
* @param pattern 模式串
* @param next 用于存储部分匹配信息
* @description 在目标串中找到模式串,若找不到返回-1
* @return 返回-1表示没有找到,找到则返回第一个匹配位置
*/
public static int kmpSearch(String target, String pattern, int[] next) {
// 遍历target目标串
for (int i = 0, j = 0; i < target.length(); i++) {
// j负责模式串
while (j > 0 && target.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
// 匹配时j自增
if (target.charAt(i) == pattern.charAt(j)) {
j++;
}
// j等于pattern的长度时表示匹配完
if (j == pattern.length()) {
// i为目标串中匹配到的模式串的末尾,最后要加1因为数组从0开始
return i - j + 1;
}
}
return -1;
}
/**
* @description 返回模式串的部分匹配信息,数组next[i]
* 的值为模式串中从0-i位置的子串的”最大相等前后缀“
*/
public static int[] kmpNext(String pattern) {
// 创建一个next[]数组
int[] next = new int[pattern.length()];
// 字符长度为1时,部分匹配信息肯定为0
next[0] = 0;
for (int i = 1, j = 0; i < pattern.length(); i++) {
// 一旦没有匹配上(将j移到next数组中最后一个为0的地方)
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
// 前后缀匹配到了,j自增
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
public static void main(String[] args) {
String target = "BBCABCDABABCDABCDABDE";
String pattern = "ABCDABD";
int[] next = kmpNext(pattern);
System.out.println("next = " + Arrays.toString(next));
int index = kmpSearch(target, pattern, next);
System.out.println("index = " + index);
}
}
最后
附上几个对我理解KMP算法起到很大帮助的博客与视频,感谢前人的智慧。
B站-尚硅谷-KMP算法
CSDN-漫画解释KMP算法
CSDN-KMP算法配图详解(对next数组的理解很有帮助)