给出如下两字符串:
String str1=“lovilovilovloveiloveyou”;
String str2=“ilove”;
要求从 str1 中找出是否存在目标字符串 str2,如果有,返回第一次出现的首字符下标,没有返回-1.
暴力匹配算法:
思路分析:
假设现在str1匹配到了i位置,子串str2匹配到了j位置,则:
1)如果当前字符匹配成功(即str1[i]==str2[j]),则i++,j++,继续匹配下一个字符
2)如果失败(即str1[i]!=str2[j]),令i=i-(j-1),j=0.相当于每次匹配失败是,i回溯,j被置为0.
代码实现:
public class BruteForceDemo {
public static void main(String[] args) {
String arr="lovilovilovloveiloveyou";
String target="ilove";
int index = violenceMatch(arr,target);
System.out.println(index);
}
/**
* @param str1 被匹配字符串
* @param str2 待匹配字符串
* @return 返回出现的首字母索引
*/
public static int violenceMatch(String str1,String str2) {
char[] s1=str1.toCharArray();
char[] s2=str2.toCharArray();
int i=0;//i索引指向s1
int j=0;//j索引指向s2
while(i<s1.length&&j<s2.length) {//保证匹配时下标不越界
if(s1[i]==s2[j]) {//匹配正确时,i,j指针均往后移动
i++;
j++;
}else {//没有匹配成功时
//匹配失败,i回溯,j置0
i=i-j+1;
j=0;
}
}
//判断是否匹配成功
if(j==s2.length) {
return i-j;
}else {
return -1;
}
}
}
弊端:
用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。
KMP算法:
1)KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法。
2)KMP算法利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间。
图解:
1.首先,用str1的第一个字符和str2的第一个字符去比较,不符合,关键字向后移动一位
2.重复第一步,还是不符合,继续后移
3.一直重复,直到str1有一个字符与str2的第一个字符符合为止
4.接着比较字符串和搜索词的下一个字符,还是符合
5.直到遇到str1有一个字符与str2的一个字符不匹配
6.这个时候,想到的是继续遍历str1的下一个字符,重复第一步。(这样是很不明智的,因为此时ilov已经比较过了,没有必要再做重复的工作,一个基本的事实是,当i与e不匹配时,你其实知道前面四个字符是ilov。KMP算法的想法是,设法利用这个已知信息,不要把“搜素位置”移动回已经比较过的位置,继续把它向后移,这样就提高了效率。)
暴力匹配的思路便是:i=i-j+1 ,j=0:
如果用KMP思想便是:(直接把i的索引返回到之前匹配过的位置上)
但是这样是在“lov”中没有与“i”重复的情况下。
如果有重复,也会有新的问题:
如下:如果是这样两个字符串
当e与v不匹配时,按照上面思路,ovoe应该直接开始与oe进行匹配,即:
但是显然,我们此时错过了第二个o而导致的匹配结果出错。即当匹配子串的首字符与后面字符有相同时,不应直接放到已匹配字符的后面。
即应该从第二个o重新匹配:
如此我们得到的结果才算正确。
7.那么如何判断子串在匹配失败时应该回溯的位置?
我们可以给出一个公式:
子串向后移动的位数=已匹配的字符数-对应的部分匹配值
那么
什么是部分匹配值:
字符串:“bread”
前缀:b,br,bre,brea
后缀:read,ead,ad,d
而部分匹配值就是“前缀”和“后缀”的最长的共有元素的长度。
以ovoe为例
“o"的前缀和后缀都为空,所以最长的共有元素的长度为0
"ov"的前缀为“o“,后缀为”v“,最长的共有元素的长度为0
“ovo"的前缀为”o“,”ov“,后缀为”vo”,“o”,最长的共有元素的长度为1
“ovoe"的前缀为”o“,”ov“,”ovo“,后缀为”voe“,”oe“,”e”,最长的共有元素的长度为0
所以ovoe已匹配的字符为”ovo“,其对应的部分匹配值为1,
已匹配的字符数为3,
子串向后移动的位数便是3-1=2;
所以ovoe才从
到
。
代码实现:
public class KMP {
public static void main(String[] args) {
String arr="lovovoe";
String target="ovoe";
int[] next=next(target);
int index = kmpMatch(arr,target,next);
System.out.println(index);
}
//先得到子串的部分匹配表
//获取到一个字符串(子串)的部分匹配值
public static int[] next(String target) {
//创建一个next数组,保存部分匹配值
int[] next=new int[target.length()];
next[0]=0;//如果字符串长度为1,部分匹配值一定为0
for(int i=1,j=0;i<target.length();i++) {
while(j>0&&target.charAt(i)!=target.charAt(j)) {
j=next[j-1];
}
//部分匹配值加1
if(target.charAt(i)==target.charAt(j)) {
j++;
}
next[i]=j;
}
return next;
}
//kmp搜素
/**
* @param str1 源字符串
* @param str2 子串
* @param next 子串对应的部分匹配表
* @return 返回第一个匹配的位置,没有返回-1
*/
public static int kmpMatch(String str1,String str2,int[] next) {
for(int i=0,j=0;i<str1.length();i++) {
//考虑str1.charAt(i)!=str2.charAt(j)的情况
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;
}
}