KMP算法的图示理解和代码实现
参考资料:左神算法课程。对其中KMP算法的学习理解。
解决的问题
字符串包含问题,在str1
中是否存在str2
这样的子串,如果存在返回子串开始的位置。注意:子串和子序列的区别。
理解KMP算法的前置知识:部分匹配表,即next数组。
next数组的值:表示在某个位置i
,对于i
前面的字符串,这个字符串的前缀集合和后缀集合中,前缀和后缀相等时候的最大长度。
例如:对于abcabcd
这个字符串,如果当前在d
这个位置上,其前面的子串就是abcabc
,前缀集合是{a,ab,abc,abca,abcab}
,后缀集合是{c,bc,abc,cabc,bcabc}
,最长的公共前缀和后缀为abc
,那么这个位置的next数组的值就是3。特别地,对于第一个字符,它前面没有字符串了,此时将next[0]
的值表示为-1。
KMP算法的图示:
解释:
- 当前的位置情况是,
str1
的i
位置字符和str2
的0
位置字符匹配,然后继续往后匹配,直到str1
的i1
位置和str2
的i2
位置,X
和Y
不相等,无法匹配。 - 此时必须将
str2
向后推,那么推到哪里呢?笨办法是将str2
往后推动一格,看str1
的i+1
位置字符和str2
的0位置字符是否匹配,但是根据next数组(部分匹配表)得知,T2
和T3
是相等的,而且str1
的i
到i1
位置之间的字符串和str2
的0
到i2
位置之间的字符串是匹配的,所以T1
等于T3
,故T1=T2=T3
。 - 根据这个性质,只需要将
str2
的0位置和str1
的j
位置对齐即可,j
位置表示T1
范围中的第一个位置。因为T1
和T2
是相等的,即已经匹配好了,接下来str2
只要从T2
的下一个位置开始匹配即可,所以i2
发生了更新,位于T2
范围的下一个位置。
关于next数组的求解示意图:
解释:
- 当前位于
i
位置,求解next[i]
,需要首先看前一个位置i-1
,求解思想是利用之前的信息来求解当前位置的next值。 T1
和T2
是i-1
位置上的最长公共前缀和后缀,pre
位置是T1
的后一个位置,那么首先比较pre
位置的字符和i-1
位置的字符是否相等,如果刚好相等,那么next[i] = next[i-1]+1
,此时i
位置上的最长前缀就是T1
加上pre
上的字符。- 如果
pre
位置的字符和i-1
位置的字符不相等,那么把pre
位置看做中间位置,求这个位置上的最长公共前缀和后缀,即T3
和T4
,然后重复上面的步骤,知道pre
不能再往前为止。
注意:需要明白pre
位置的含义,将pre
位置的元素和i-1
位置的元素进行比较,如果相等则结束,如果不等,那么pre
继续往前跳,直到不能跳为止,此时next[i] = 0
。
KMP算法代码实现
import java.util.Arrays;
public class KMP {
/**
* 利用kmp算法在str1中匹配str2
* @param str1 待匹配的目标字符串
* @param str2 模式串
* @return 如果能在str1中匹配到str2,则返回str1中匹配上的字符串的开始位置;如果匹配不到,返回-1
*/
public static int getIndexOf(String str1, String str2) {
// 特殊情况
if (str1 == null || str1 == null || str2.length() < 1 || str1.length() < str2.length()) {
return -1;
}
int[] next = generateNextArray(str2);
char[] strArr1 = str1.toCharArray();
char[] strArr2 = str2.toCharArray();
int i1 = 0, i2 = 0;
while (i1 < strArr1.length && i2 < strArr2.length) {
if (strArr1[i1] == strArr2[i2]) {
i1++;
i2++;
} else if (next[i2] == -1) { // 此时i2在str2中的第一个位置,并且未匹配
i1++;
} else {
i2 = next[i2];
}
}
return i2 == strArr2.length ? i1-i2 : -1;
}
/**
* 生成字符串的next数组
* next数组中的值:表示当前位置i前面的字符串,这个字符串的前缀和后缀相等的最大长度
* 举例:"ababac"和"ababcababak"将这两个例子带入下面的代码跑一遍就明白pre的含义和代码的逻辑。
* @param str
* @return
*/
private static int[] generateNextArray(String str) {
if (str.length() == 1) {
return new int[]{-1};
}
char[] strArray = str.toCharArray();
int[] next = new int[strArray.length];
next[0] = -1; // 第一个位置前面没有字符串了,约定该位置的next值为-1
next[1] = 0;
int i = 2; // 从第三个字符开始往后考虑
int pre = 0; // pre的含义:假设当前位置为i,pre表示i-1位置上的next值
while (i < strArray.length) {
// 此时pre表示i-1位置上的next值,那么比较i-1位置的字符和pre位置的字符,看是否相等
if (strArray[i-1] == strArray[pre]) {
next[i++] = ++pre;
} else if (pre > 0) { // 如果匹配不上,pre就往前跳
pre = next[pre];
} else { // 如果没法往前跳了,那么i位置的next值就是0
next[i++] = 0;
}
}
return next;
}
}
KMP算法的应用
1、在一个字符串的最后添加任意个字符,使得新的字符串包含两个原始串,比如abcabc
,在末尾添加上abc
之后得到新的字符串abcabcabc
,这个新的字符串包含两个原始串abcabc
。
2、是否存在子树,即A中是否包含像B这样的子树。注意:子树和子结构的区别。
解决方案:将A和B两棵二叉树序列化为字符串之后,就将树的问题转化为字符串的问题,也就是将子树的问题转化为KMP的问题。序列化的方式可以是前序、中序、后序、层序,若选择前序遍历,则树A序列化之后的字符串为1,1,1,null,null,1,null,null,1,1,null,null,null
,树B序列化之后的字符串为1,1,null,null,null
,其中用,
表示节点间的分隔符,用null
来表示空节点,当然用其它的符号,比如#
或者$
都是可以的。序列化问题参考题目:剑指offer37
3、判断某个字符串str
是否是str' x n
这样的形式,比如abcabcabc
就是这样的形式。