背景
字符串匹配问题:
(1)有一个字符串str1=“BBC ABCDAB ABCDABCDABDE”,和一个子串str2=“ABCDABD”
(2)现在要判断str1中是否含有str2,如果存在,就返回第一次出现的位置,如果没有,则返回-1
暴力匹配算法:
利用暴力匹配算法,假设现在str1匹配到i位置,子串str2匹配到j位置,则有:
1)如果当前字符串匹配成功(即str1[i]=str[j]),则i++,j++,继续匹配下一个字符
2)如果匹配失败(即str1[i]!=str[j]),则i=i-(j-1),j=0。相当于每次匹配失败时,i回溯,j被设为0。
存在问题:
利用暴力算法解决此类问题会有大量的回溯,每次只移动一位,若不匹配,移动到下一位接着判断,计算时间复杂度较大(O(nm): n为字符串str1的长度,m为字符串str2的长度)。
解决办法:
KMP算法
KMP算法理论介绍
克努特—莫里斯—普拉特操作(Knuth-Morris-Pratt, KMP),是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。
KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到前面匹配过的位置,省去了大量的计算时间。
举例说明
字符串匹配问题:
1)有一个字符串str1=“BBC ABCDAB ABCDABCDABDE”,和一个子串str2=“ABCDABD”
2)判断str1是否含有str2,如果存在,则返回第一次出现的位置,如果没有,则返回-1
KMP算法思路分析图解:
(1)首先,用str1的第一个字符和str2的第一个字符去比较,不符合,关键词向后移动一位
(2)重复第一步,若还是不符合,再次后移
(3)一直重复,直到str1有一个字符与str2的第一个字符匹配为止
(4)接着比较str1与str2的下一个字符,还是匹配
(5)遇到str1有一个字符与str2的字符不匹配
(6)此时,如果利用暴力匹配算法时,将会重复第一步(这是很不明智地,因为此时BCD已经比较过了,没有必要在做重复地工作,一个基本事实为,当空格与D不匹配时,你已经知道前六字符为"ABCDAB"。KMP的思想是:利用这个已知信息,不把搜索位置移回已经比较过的位置,继续把它向后移,这样就可以提供效率)
(7)如何把刚刚重复的步骤省略掉?可以对str2计算出一张”部分匹配表“,此表的产生后面介绍
(8)已知空格与D不匹配时,前面6个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的”部分匹配值“为2,因此按照下面的公式算出向后移动的位数:
移动位数=已匹配的字符数-对应的部分匹配值(6-2=4,因此将str2向后移动4位)
(9)因为空格与C不匹配,str2还有向后移动。此时,已匹配的字符数位2(“AB”),对应的”部分匹配值“为0,移动位数=2-0,于是将str2后移2位
(10)因为空格与A不匹配,继续后移一位
(11)逐位比较,知道发现C与D不匹配。因此,移动位数=6-2,将str2后移4位
(12)逐位比较,直到str2的最后一位,发现完全匹配,搜索结束
(13)”部分匹配表的产生“,首先介绍前缀和后缀
”部分匹配值“就是”前缀“和”后缀“的最长的共有元素的长度。以"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,D]共用元素长度为0。
(14)”部分匹配“的实质为,有时字符串头部和尾部会有重复。如”ABCDAB“之中有两个"AB",那么它的”部分匹配值“为2(”AB“的长度)。str2移动时,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。
代码实现(Java)
package com.zq.kmp;
import java.util.Arrays;
public class KMP {
public static void main(String[] args) {
String str1="BBC ABCDAB ABCDABCDABDE";
String str2="ABCDABD";
int[] next=kmpnext(str2);
System.out.println("next="+Arrays.toString(next));
int index=kmpSearch(str1, str2, next);
System.out.println("index="+index);
}
/*
* KMP匹配算法
* */
public static int kmpSearch(String str1,String str2,int[] next) {
for(int i=0,j=0;i<str1.length();i++) {
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()) {
return i-j+1;
}
}
return -1;
}
/*
* 获取一个字符串的部分匹配表
* */
public static int[] kmpnext(String dest) {
//创建数组next保存部分匹配值
int[] next=new int[dest.length()];
next[0]=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)成立时退出
while(j>0 && dest.charAt(i)!=dest.charAt(j)) {
j=next[j-1];
}
//当dest.charAt(i)=dest.charAt(j)时,部分匹配值加一
if (dest.charAt(i)==dest.charAt(j)) {
j++;
}
next[i]=j;
}
return next;
}
}
仿真结果
参考文献:
1.韩顺平-图解Java数据结构和算法
2.https://www.cnblogs.com/zzuuoo666/p/9028287.html