KMP 算法介绍
- Knuth-Morris-Pratt 字符串查找算法,简称为“KMP算法”,常用于在一个文本串 S 内查找一个模式串 Р 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H.Morris 三人于1977年联合发表,故取这3人的姓氏命名此算法。
- KMP 方法算法就利用之前判断过信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过 next 数组找到前面匹配过的位置,省去大量的计算时间
- 详细参考:https://www.cnblogs.com/zzuuoo666/p/9028287.html
KMP 算法最佳应用-字符串匹配问题
字符串匹配问题:
- 有一个字符串 str1=“BBC ABCDAB ABCDABCDABDE”,和一个子串 str2=“ABCDABD”
- 判断 str1 是否含有 str2,如果存在,就返回第一次出现的位置,如果没有,则返回 -1
- 要求:使用 KMP 算法完成判断,不能使用简单的暴力匹配算法.
思路分析图解
- 首先,用 str1 的第一个字符和 str2 的第一个字符去比较,不符合,关键词向后移动一位
- 重复第一步,还是不符合,再后移
- 一直重复,直到 str1 有一个字符与 str2 的第一个字符符合为止
- 接着比较字符串和搜索词的下一个字符,还是符合。
- 遇到 str1 有一个字符与 str2 对应的字符不符合
- 这时候,想到的是继续遍历 str1 的下一个字符,重复第 1 步。(其实是很不明智的,因为此时 BCD 已经比较过了,没有必要再做重复的工作,一个基本事实是,当空格与 D 不匹配时,你其实知道前面六个字符是 ”ABCDAB”。KMP 算法的想法是,设法利用这个已知信息,不要把 ”搜索位置” 移回已经比较过的位置,继续把它向后移,这样就提高了效率。)
- 可以对 str2 计算出一张《部分匹配表》,这张表的产生在后面介绍
- 已知空格与 D 不匹配时,前面六个字符 ”ABCDAB” 是匹配的。查表可知,最后一个匹配字符 B 对应的 ”部分匹配值” 为 2,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 – 对应的部分匹配值
因为 6 - 2 等于 4,所以将搜索词向后移动 4 位。 - 因为空格与 C 不匹配,搜索词还要继续往后移。这时,已匹配的字符数为 2 (”AB”),对应的 ”部分匹配值” 为 0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移 2 位。
- 因为空格与 A 不匹配,继续后移一位
- 逐位比较,直到发现 C 与 D 不匹配,于是,移动位数 = 6 - 2,继续将搜索词向后移动 4 位
- 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动 7 位,这里就不再重复了。
- 介绍《部分匹配表》怎么产生的
先介绍前缀,后缀是什么
“部分匹配值” 就是 ”前缀” 和 ”后缀” 的最长的共有元素的长度。
以 ”ABCDABD” 为例,
-”A” 的前缀和后缀都为空集,共有元素的长度为 0;
-”AB” 的前缀为 [A];后缀为 [B],共有元素的长度为 0;
-”ABC” 的前缀为 [A,AB],后缀为 [BC,C],共有元素的长度 0;
-”ABCD” 的前缀为 [A,AB,ABC],后缀为 [BCD, CD, D],共有元素的长度为 0;
-”ABCDA” 的前缀为 [A,AB,ABC,ABCD],后缀为 [BCDA,CDA,DA,A],共有元素为 ”A”,长度为 1;
-”ABCDAB” 的前缀为 [A,AB,ABC, ABCD,ABCDA],后缀为 [BCDAB,CDAB,DAB,AB,B],共有元素为 ”AB",长度为 2;
-”ABCDABD”的前缀为 [A,AB,ABC,ABCD,ABCDA,ABCDAB],后缀为 [BCDABD,CDABD, DABD,ABD, BD],共有元素的长度为 0; - ”部分匹配” 的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB” 之中有两个 ”AB”,那么它的 ”部分匹配值” 就是 2 (”AB” 的长度)。搜索词移动的时候,第一个 ”AB” 向后移动 4 位(字符串长度 - 部分匹配值),就可以来到第二个 ”AB” 的位置。
代码实现
public class KMPAlgorithm {
public static void main(String[] args) {
// TODO Auto-generated method stub
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int[] next = kmpNext("ABCDABD");
System.out.println("next=" + Arrays.toString(next));
int index = kmpSearch(str1, str2, next);
System.out.println("index=" + index);
}
//写出kmp搜索算法
/**
*
* @param str1 源字符串
* @param str2 子串
* @param next 部分匹配表, 是子串对应的部分匹配表
* @return 如果是-1就是没有匹配到,否则返回第一个匹配的位置
*/
public static int kmpSearch(String str1, String str2, int[] next) {
//遍历
for(int i = 0, j = 0; i < str1.length(); i++) {
//需要处理 str1.charAt(i) != str2.charAt(j), 去调整j的大小
//KMP算法核心点, 可以验证...
while( j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j-1];
}
if(str1.charAt(i) == str2.charAt(j)) {
j++;
}
if(j == str2.length()) {//找到了 // j = 3 i
return i - j + 1;
}
}
return -1;
}
//获取到一个字符串(子串) 的部分匹配值表
public static int[] kmpNext(String dest) {
//创建一个next 数组保存部分匹配值
int[] next = new int[dest.length()];
next[0] = 0; //如果字符串是长度为1 部分匹配值就是0
for(int i = 1, j = 0; i < dest.length(); i++) {
//当dest.charAt(i) != dest.charAt(j) ,需要从next[j-1]获取新的j
//直到发现有 dest.charAt(i) == dest.charAt(j)成立才退出
//这时kmp算法的核心点
while(j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j-1];
}
//当dest.charAt(i) == dest.charAt(j) 满足时,部分匹配值就是+1
if(dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}