一、前言
完整代码在第三段落,不看废话可以直达,第四段是对Next数组的改良:NextVal数组的代码实现,以及我对采用NextVal数组进行连续匹配的一些疑问,如果有大佬知道怎么解决这个问题欢迎评论区帮我解答。最近在复习数据结构(跟着老韩),前天学kmp的时候,代码实现着实有点拉跨,照着文本敲了代码让人摸不着头脑,于是乎找来了放在手边的大话数据结构,求next数组的时候是默认字符串第一位(也就是下标为0的位置)存储字符串长度,这让我有点懵逼,我想着平时字符串第一位我也不会放字符长度吧,就来到了b站看up主的思路,KMP字符串匹配算法1_哔哩哔哩_bilibili,KMP字符串匹配算法2_哔哩哔哩_bilibili。这个up主讲的很好很全了,与其他的讲解不同的是,他求的next数组是当前位置的相同公共子串的长度,最终全部后移一位,第一位置为-1,原本的最后一位舍弃,想知道kmp具体怎么实现的看完上方的两个视频肯定就懂了,这篇文章具体针对其中的一些边界情况的代码问题修改一下。
二、代码的一些问题
1、先上up主的代码:
-
首先肯定是先求前缀表了(注意这里前缀表全部后移一位,然后将第一位置为-1),具体的思路都在上方视频链接中,这边我就不赘述了。
- 前缀表后移
//前缀表后移一位,第一个值设为-1 public static void movePrefixTable(int prefix[]) { int i; int n = prefix.length; for (i = n - 1; i > 0; i--) { prefix[i] = prefix[i - 1]; } prefix[0] = -1; }
- 前缀表具体求解代码
//获得模式串的前缀表 public static void getPrefixTable(String pattern, int prefix[]) { prefix[0] = 0;//前缀表第一位永远是0 int k = 0;//k为上一个字符的对称程度,如果k=0说明不再有子对称 int i = 1;//因为第一位的prefix永远是0,所以从第二位开始计算 int n = pattern.length();//模式串的长度 while (i < n) { if (pattern.charAt(k) == pattern.charAt(i)) {//找到了这个子对称,或者是直接继承了前面的对称性,这两种都在前面的基础上++ k++; prefix[i] = k; i++; } else { if (k > 0) { k = prefix[k - 1];//寻找更小的对称程度 } else {//如果遍历了所有子对称都无效,说明这个新字符不具有对称性,清0 prefix[i] = k; i++; } } } movePrefixTable(prefix);//得到最终的前缀表 } }
-
求完前缀表,当然是具体的kmp算法咯
//kmp模式匹配算法 public static void kmp(String S, String T, int pos) { int i = pos;//i用于存储主串S中当前位置的下标值,从pos位置开始匹配 int j = 0;//j用于存储子串T中当前位置的下标值 int[] prefix = new int[T.length()]; getPrefixTable(T, prefix); while (i <= S.length() - 1) {//当i小于(S的长度 - 1)并且j小于(T的长度 - 1),循环继续 if (j == T.length() - 1 && S.charAt(i) == T.charAt(j)) { System.out.println("找到匹配字符index:" + (i - j)); j = prefix[j]; } if (S.charAt(i) == T.charAt(j)) {//检测到有相等的字符,就继续 ++i; ++j; } else { j = prefix[j]; if (j == -1) { i++; j++; } } } }
2、运行时的问题
-
这是我的main方法测试函数
public static void main(String[] args) { String str1 = "真da真真真真帅 韩帅比真帅"; String str2 = "真"; kmp(str1, str2, 0); String str3 = "真真a真真真帅 韩帅比真帅"; String str4 = "真真"; kmp(str3, str4, 0); String str5 = "ABABABAACABAACABAAA"; String str6 = "ABAACABAA"; kmp(str5, str6, 0); }
当我们运行的时候就会发现第一种情况,程序会报出越界异常
String index out of range: -1
,这是因为字符串prefix[0] = -1,当找到模式串对应的子串后就会返回 j = prefix[j] = -1,这时候下一组再比较就会出现越界情况了。所以我在找到匹配子串输出语句并重置j值之后增加了一个if语句
if (j == -1) {j++;}
,但是这样,第一种情况就会越界String index out of range: 1
,越界方式不同了而已。然后我想到既然匹配到了子串,模式串长度大于等于2的完全可以继续这个循环,他最多重置j值为0,所以不会出错。但长度为1的模式串,找到了匹配子串,j值重置为-1,我们首先需要将其置为0,然后还要将主串的比较的位置后移,才能继续比较,并且还需要使用
continue
语句进入下一个循环,不然如果在匹配主串为"AA",模式串为"A"的情况时,A匹配到第一个A,j变为0,继续循环,再次匹配到,++j,此时会越界String index out of range: -1
。所以增加代码:if (T.length() == 1) { i++; j++; continue; }
这里要注意必须是模式串长度为1才可执行此代码,否则会丢失某些匹配的子串,如用"ABAACABAA"寻找"ABABABAACABAACABAAA"中匹配的子串,会丢失第二个匹配的子串。
三、最终代码
package DataStructures.String; import java.util.Arrays; /** * KMP算法 * * @author 韩帅比 * @create 2021-08-23 19:40 */ public class KMP { public static void main(String[] args) { String str1 = "真da真真真真帅 韩帅比真帅"; String str2 = "真"; kmp(str1, str2, 0); String str3 = "韩帅比 大帅逼真帅逼真帅 韩帅比真帅"; String str4 = "帅"; int[] prefix = new int[str4.length()]; getPrefixTable(str4, prefix); System.out.println(Arrays.toString(prefix)); kmp(str3, str4, 0); String str5 = "ABABABAACABAACABAAA"; String str6 = "ABAACABAA"; int[] prefix2 = new int[str6.length()]; getPrefixTable(str6, prefix2); System.out.println(Arrays.toString(prefix2)); kmp(str5, str6, 0); } //kmp模式匹配算法 public static void kmp(String S, String T, int pos) { int i = pos;//i用于存储主串S中当前位置的下标值,从pos位置开始匹配 int j = 0;//j用于存储子串T中当前位置的下标值 int[] prefix = new int[T.length()]; getPrefixTable(T, prefix); while (i <= S.length() - 1) {//当i小于(S的长度 - 1)并且j小于(T的长度 - 1),循环继续 if (j == T.length() - 1 && S.charAt(i) == T.charAt(j)) { System.out.println("找到匹配字符index:" + (i - j)); j = prefix[j]; if (T.length() == 1) { i++; j++; continue; } } if (S.charAt(i) == T.charAt(j)) {//检测到有相等的字符,就继续 ++i; ++j; } else { j = prefix[j]; if (j == -1) { i++; j++; } } } } //前缀表后移一位,第一个值设为-1 public static void movePrefixTable(int prefix[]) { int i; int n = prefix.length; for (i = n - 1; i > 0; i--) { prefix[i] = prefix[i - 1]; } prefix[0] = -1; } //获得模式串的前缀表 public static void getPrefixTable(String pattern, int prefix[]) { prefix[0] = 0;//前缀表第一位永远是0 int k = 0;//k为上一个字符的对称程度,如果k=0说明不再有子对称 int i = 1;//因为第一位的prefix永远是0,所以从第二位开始计算 int n = pattern.length();//模式串的长度 while (i < n) { if (pattern.charAt(k) == pattern.charAt(i)) {//找到了这个子对称,或者是直接继承了前面的对称性,这两种都在前面的基础上++ k++; prefix[i] = k; i++; } else { if (k > 0) { k = prefix[k - 1];//寻找更小的对称程度 } else {//如果遍历了所有子对称都无效,说明这个新字符不具有对称性,清0 prefix[i] = k; i++; } } } movePrefixTable(prefix);//得到最终的前缀表 } }
四、NEXT数组改良,NEXTVAL数组
-
下面是对Next数组的改良,NextVal数组的代码
private static int[] getNextVal(String T, int nextVal[]) { int len = T.length(); nextVal[0] = -1; int i = 0; int k = -1; while (i < len - 1) { if (k == -1 || T.charAt(i) == T.charAt(k)) { ++k; ++i; if (T.charAt(i) != T.charAt(k)) { nextVal[i] = k; } else { nextVal[i] = nextVal[k]; } } else { k = nextVal[k]; } } return nextVal; }
NextVal数组在匹配第一个相等子串的时候增加了效率,但在连续匹配的时候却会丢失匹配,比如"ABABABAACABAACABAAA"匹配"ABAACABAA"时,原Next数组为{-1,0,0,1,1,0,1,2,3},所以在匹配到第一个对应的子串之后,回退到模式串第四位即第三个A处,即可继续匹配,但NextVal数组为{-1,0,-1,1,1,-1,0,-1,1},匹配到第一个对应的子串后会回退到模式串的第二位,即第一个B,这样就匹配不到紧随其后的第二个对应的子串了。