本文已参与「新人创作礼」活动,一起开启掘金创作之路。
对KMP的思考
KMP算法是字符串匹配的经典算法之一,能将暴力匹配的复杂度从降低到。在了解KMP之前,我们先来看看暴力匹配字符串的实现。
给定一个带匹配字符串str = “abababc”和一个模板串t = “ababc”,返回模板串在带匹配串中的下标。
设一个指针i在str上移动,指针j在t上移动,当i指向索引4即“b”的时候,j指针指向“c”,此时发生不匹配的情况,于是i指针重新指向索引1,j指针索引0,进行下一轮匹配,直到模板串匹配完成。
很明显,暴力求解将整个待匹配的字符串重复循环,其时间复杂度是较高的,那么有什么办法可以优化呢?
如果我们只让i向前移动,不进行回退,那么就可以将算法优化为线性时间。例如在上图中,在索引4的位置发生了不匹配,但是发生不匹配的前两位和模板串的开头两位是匹配的,因此我们不回退i指针,让j指针跳过模板串的前两位,从第三位继续匹配。
对于已经遍历过的子串,我们是否可以从中再发掘一些信息呢?我们将发送不匹配的点称为错误点,从中我们可以看出,错误点的前两位和模板串的前两位是匹配的,我们将错误点之前的串提取为一个子串。在这个子串中,最长的相等的前后缀,其长度为2即“ab”,因此如果我们在这个子串的下一位发生了不匹配,那么就说明,错误点的前两位必定是“ab”,此时再进行匹配的时候,我们就可以跳过模板串的前两位
对于前后缀,上述可能表示的不是很清楚。此时的前缀是指不包含最后一个字符的前缀,后缀指的是不包含第一个字符的后缀。例如“abab”,其前缀有“a”、“ab”、“aba”,其后缀有“b”、“ab”、“bab”。
由此引申除了KMP算法中的核心——next数组,我们为模板串的每一位都计算一个值,这个值的意思就是,当下一位发生发生不匹配的时候,让j指针跳过模板串的next[ j - 1 ]位。
此时我们再来看KMP算法的字符串匹配情况
还是在索引4发生了不匹配,但是此时我们有了next数组,通过查询错误点前一位的next数组值,等于2,因此我们将跳过模板串的前两位,此时再向后移动i、j指针继续检查,不必再回退i指针,我们的算法就被优化为了线性时间。
下面是KMP算法的代码
public int kmp(String str, String sub) {
int[] next = findNext(sub);
int i = 0, j = 0;
while (i < str.length() && j < sub.length()) {
if (str.charAt(i) == sub.charAt(j)) {
i++;
j++;
} else if (j > 0) {
j = next[j - 1];
} else {
i++;
}
}
if (j == sub.length()) {
return i - j;
} else {
//不存在则返回-1
return -1;
}
}
next数组求解
特别说明,此时KMP算法中的next数组并不是原KMP算法中表达的意思,我们日常讨论的KMP都是经过简化处理的。
从前面的讨论我们得出,next数组每一位的值是以该位结尾的模板串的子串,其所对应的最大前后缀长度
顺着这个思路我们先暴力求解一下next数组,设模板串为“abacabab”。显然next数组第一位均为0即next[0] = 0,索引1的子串“ab”其最长相等前后缀的长度也为0,next[1] = 0,子串“aba”其最长相等前后缀的长度为1,以此类推,“abac”其next[3] = 0,“abaca”其next[4] = 1。
此时我们发现,当我们找到第一个相等前后缀的时候,我们可以判断这个字符的下一位是否相等,如果相等则可以直接让前后缀的长度加一,可如果不相等呢?对于上述模板串的子串“abacaba”其最大前后缀长度为3,然而其下一个子串“abacabab”,我们难道又要暴力求解吗?
对于“abacaba”其存在长度为3的相等的前后缀,我们以“aba”为基础判断加上下一个字符是否也满足相等的前后缀是行不通的,又因为左边的子串等于右边的子串(都是“aba”),所以左边子串的最长的前缀一定等于右边子串的最长后缀即“a”,我们就以“aba”的最长前后缀即“a”为基础再进行后面的查找,如果加上下一个字符任然没有最长前后缀,我们就查“a”的最长前后缀,又因为“a”的最长前后缀为0,所以其next值为0。
有了思路算法就很好实现了
public int[] findNext(String str) {
if (str.length() == 1) {
return new int[] {0};
}
int[] next = new int[str.length()];
next[0] = 0;
int prefixNum = 0;
for (int i = 1; i < str.length(); i++) {
if (str.charAt(i) == next[prefixNum]) {
prefixNum++;
next[i] = prefixNum;
} else if (prefixNum == 0) {
next[i] = prefixNum;
} else {
next[i] = prefixNum - 1;
}
}
return next;
}