一、应用场景-字符串匹配问题
二、解决方式-暴力匹配
如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:
public static int violenceMatch(String source, String target) {
int i = 0, j = 0;
char[] s1 = source.toCharArray();
char[] s2 = target.toCharArray();
//任意一个索引越界了就结束 一般i越界了有可能没找到,j越界了是找到了
while (i < source.length() && j < target.length()) {
count++;
//第一个字符匹配成功,接着往下面匹配
if (s1[i] == s2[j]) {
i++;
j++;
} else {
//匹配不成功,从下一个位置重新开始匹配
i = i - j + 1;
j = 0;
}
//j指向了目标字符串的最后,匹配成功了
if (j == target.length()) return i - j;
}
return -1;
}
三、解决方式-KMP算法
3.1 前缀与后缀
3.2 next数组与部分匹配值:
3.3 举例
假如我们已经匹配到了这里,可以看到下一个字符不匹配。
如果按照暴力方法,下一次匹配的位置应该是最开始匹配位置的后一位:
利用KMP算法,计算出下一次后移的位数是 4 位:
(已匹配字符数(6)- 最后一个已匹配字符的部分匹配值(2))
四、KMP的代码演示
1、首先是获取next数组,这个是网上的版本,我不理解。我在主程序中用别的思路实现了同样的结果
//获取next数组的网上版本,我看不懂
public static int[] Next1(String s) {
int[] next = new int[s.length()];
next[0] = 0;//第一个元素无论如何都是0,不跟自己匹配
//索引后移
for (int i = 1, k = 0; i < s.length(); i++) {
while (k > 0 && s.charAt(k) != s.charAt(i)) k = next[k - 1];//
if (s.charAt(k) == s.charAt(i)) k++;//如果某次匹配到了,就让匹配长度+1
next[i] = k;//不论每次是否匹配到都把k赋给next的对应元素,也就是匹配长度
}
return next;
}
2.主程序
注:next数组的思路:
1. i指向字符串首位,j指向字符串首位的后一位。j 不断后移,直到遇到和首位相同的字符时 i、j同时后移,这时同时记录匹配长度到next数组中。
2. 一旦又遇到了不同的字符,这里要判断了: i 回到字符串首位,引入右指针 k=j 。i、k此时是一对对撞指针。移动 i、k(移动次数小于匹配长度),判断所指元素是否相等,以验证之前的匹配长度是否有效。
加入 2.1 这一步是因为可能有下面情况:
a a b a a a c
0 1 0 1 2 ?
此前匹配长度为2,这里 b 与 a不匹配,如果直接清除匹配长度,结果是错的!
a a b a a a 的最大匹配长度应该是 2(aa),而不是0
我们应该利用对撞指针,判断之前的匹配长度是否还有效,类似于回文串的判断,对撞指针移动次数就是匹配长度-1
2.1 若匹配都成功,记录此次匹配长度,i 不需要再移到首位,等待下一次匹配。
2.1 若匹配失败,i 就回到字符串首位,清空 匹配长度变量,等待下一次匹配。
3. j始终后移。如果又匹配成功了,就重复类似于 1 的操作(i、j同时后移)。直到 j 指到了字符串末尾。
public static Integer count=0;//匹配次数计数,对比两种方法
public static int KMPMatch(String source,String target){
//1、获得next数组,这是kmp算法的关键,这样我们才能决定下次从哪里开始匹配
int[] next = new int[target.length()];
int matchedLength = 0, i = 0, j = 1;
next[0] = 0;//第一个元素无论如何都是0,不跟自己匹配
//i指向字符首位,j后移。
while (j < target.length()) {
//如果 i 与 j 所指字符匹配成功,就同时后移,同时记录匹配的字符长度
if (target.charAt(i) == target.charAt(j)) {
next[j] = ++matchedLength;
i++;
//否则就把 i 移到首位,进行判断
} else{
i=0;//重置指针
int k=j;//记录当前的后指针
//这里要额外的判断:利用对撞指针,验证之前的匹配长度是否还适用
while(i<k && i<matchedLength ){
//某次匹配失败,即验证之前的匹配长度失效
if(needle.charAt(i)!=needle.charAt(k)){
next[j]=0;
matchedLength =0;//重置匹配长度
i=0;//重置指针
break;
}else{
i++;//移动对撞指针(左右)
k--;
}
}
//如果验证成功,就记录先前的匹配长度
next[j]=matchedLength;//记录后指针的最长匹配长度
}
//j始终向后遍历
j++;
}
//2、真正的匹配过程
int k = 0, l = 0;//k指向源字符串,l指向目标字符串
int matched=0;
char[] s1 = source.toCharArray();
char[] s2 = target.toCharArray();
//任意一个索引越界了就结束 一般i越界了有可能没找到,j越界了是找到了
while (k < source.length() && l < target.length()) {
count++;
//第一个字符匹配成功,接着往下面匹配
if (s1[k] == s2[l]) {
k++;
l++;
matched++;
} else {
//匹配不成功,从后面位置重新开始匹配,这个位置经过了kmp算法的优化
if(matched>=1) k=k-l+matched-next[matched-1];//字符长度不为0,那就根据算法选择下次开始比较的位置
else k=k-l+1;//字符长度为0,那就跟暴力方法一样回到原来位置的下一位
l = 0;
matched=0;
}
//l指向了目标字符串的最后,匹配成功了
if (l == target.length()) return k - l;
}
return -1;
}
五、最终对比
@Test
public void testViolenceMatch() {
String source = "BBC ABCDAB ABCDABCDABDE";
String target = "ABCDABD";
System.out.println(solution.violenceMatch(source, target));
System.out.println("匹配次数:"+solution.count);
}
15
匹配次数:36
@Test
public void testKMPMatch() {
String source = "BBC ABCDAB ABCDABCDABDE";
String target = "ABCDABD";
System.out.println(solution.KMPMatch(source,target));
System.out.println("匹配次数:"+solution.count);
}
15
匹配次数:29