由于编程语言不同以及对于next数组的定义不同,会导致学习kmp算法的过程中有很多疑惑,这篇文将讲述java语言实现kmp算法。
学习KMP算法首先要了解的两个内容,一是kmp算法本身,二是next数组。
kmp算法
KMP(Knuth-Morris-Pratt)算法是一种用于在文本中查找模式的高效字符串匹配算法。该算法的关键思想是通过预处理模式字符串,构建一个部分匹配表,然后利用这个表在匹配过程中避免不必要的比较,从而提高匹配的效率。
学习kmp算法前,可以先去了解普通的模式匹配法,这样更可以体会到kmp算法的强大。
第一步:
在主串中匹配子串,当匹配到第五个字符时,会发现A与C并不匹配。在传统的算法中,下一步会将子串的A与主串的B进行匹配,这样显然很繁琐,那么kmp算法是怎么样的呢?那么看下图。
kmp算法会直接将箭头所指向的两个值进行匹配,跳过了前面的步骤。那么为什么可以这样。
这时就需要先引入一个叫作next数组的东西,先不管它怎么来的,后面会讲。
子串下面的数字就是next数组,当A与C不匹配时,上一个匹配字符也就是B,所对应的数字是2,那么就移动子串将子串中索引为2的字符(A)与主串中索引为4的字符(A)进行匹配。
那么接下来讲解next数组。next数组代表的是前缀与后缀的最长匹配数
以ABABC为例,当只有一个A时,没有字符与它匹配,所以是0,接下来是AB,A与B并不相同,所以B下也是0,ABA时,A与A相同所以是1,ABAB时AB与AB相同所以是2,当ABABC时,前缀与后缀没有相同的串时,值为0。这就是next数组的求法。
那么算法应该怎么写呢
看这个情况,当B与C不匹配时该怎么办,看上一个匹配的字符A的next值为3,当前的前缀长度为3,那么看前缀的第三个字符,值为1,代表这个前缀只有一个匹配字符长度为1,说明第一个字符与第三个字符是相等的,而第三个字符又于第七个字符相等,因为前缀与后缀相等,所以可以直接开始匹配第二个字符B与第八个字符B。
同样的道理,再回到主串与子串的匹配中来
此时C与B不相等,它的上一个匹配的字符是A,它的next值为1,也就是说此时子串的第一个字符与第四个字符相等,子串的第四个字符与主串的第四个字符相等,所以子1与主4相等,可以直接跳过匹配,接下来应该匹配的是子串第二个字符B与主串第五个字符C。
理解kmp算法的一个要点是,主串是永远递增的,不会回退,子串匹配不相等时会回退。
如果看到这里你还是不太明白,可以结合下面的代码进行理解。
public int KMP(cusString S, cusString T, int pos) {
int i = pos;
int j = 0;
int[] next = new int[T.getLength()];
getNext(T, next);
while (i < S.getLength() && j < T.getLength()) {
if (S.chars[i] == T.chars[j]) {
i++;
j++;
}else {
if (j == 0) {
i++;
}else {
j = next[j - 1];
}
}
}
if(j == T.getLength()) {
return i - T.getLength();//起始位置索引
}else {
return -1;
}
}
public void getNext (cusString T,int[] next){
next[0] = 0;
int g = 0;
int i = 1;
while (i < T.getLength()) {
if (T.chars[i] == T.chars[g]) {
g++;
next[i] = g;
i++;
} else {
if (g == 0) {
next[i] = 0;
i++;
} else {
g = next[g - 1];
}
}
}
}
下面是串的数据结构的定义
public class cusString {
private int maxSize = 10;
private char[] chars;
private int length;
public cusString() {
chars = new char[maxSize];
length = 0;
}
public cusString(int n) {
if (n <= 0) {
System.out.println("n must be > 0");
System.exit(1);
}
chars = new char[n];
length = 0;
}
}
下面是一个测试的完整代码
public class KMPTest {
public static void main(String[] args) {
// 测试模式串
cusString pattern = new cusString("ababaaaba");
// 初始化一个数组用于存储计算得到的 next 数组
int[] next = new int[pattern.getLength()];
// 调用 getNext 方法计算 next 数组
getNext(pattern, next);
// 输出计算得到的 next 数组
System.out.println("Next 数组: " + Arrays.toString(next));
}
// 这里简化了 cusString 类的定义,你需要确保 cusString 类中有 getLength 和 chars 属性的实现
static class cusString {
private final char[] chars;
public cusString(String str) {
this.chars = str.toCharArray();
}
public int getLength() {
return chars.length;
}
}
// getNext 方法的实现
public static void getNext(cusString T, int[] next) {
next[0] = 0;
int g = 0;
int i = 1;
while (i < T.getLength()) {
if (T.chars[i] == T.chars[g]) {
g++;
next[i] = g;
i++;
} else {
if (g == 0) {
next[i] = 0;
i++;
} else {
g = next[g - 1];
}
}
}
}
}