KMP算法
KMP算法是干什么的?
KMP算法可以认为是一种字符串的查找算法,或者说是匹配算法。给大家举个例子,比如说有一个字符串 str1 = abc123def ,str2 = 123,str1是否有某一个子串(连续的)和str2一样, 有,返回str1中第一次出现str2中起始字符的位置。
面对这样一个问题,我们先不说KMP算法,我们先说一个麻烦的算法。我们再str1从0开始依次匹配str2,能匹配上就返回,匹配不上就下一个位置继续。对于这整个一个暴力的过程,str1 = aaaaaaaaab,str2 = aaab,我们从0开始遍历,0-6位置都遍历了s2长度次数才宣告失败,所以时间复杂度在最差情况下为O(N*M) N为str1的长度 M为str2的长度。而使用KMP算法可以做到时间复杂度为O(N)。
KMP算法是一种比较难的算法,所以在了解KMP算法之前呢,我们需要奠定一些基本的概念。
1.什么是前缀与后缀串的最长匹配长度?
我们还是用例子说话,比如说 abcabck 首先,这个信息是根据位置来分的,也就是说每一个位置都有一个该信息,我们拿6位置的k举例,该信息代表不包含该位置字符前缀与后缀最长的匹配长度。该位置的信息和该位置字符没有任何关系,它跟该位置之前的字符有关。6位置之前字符为abcabc,在不包含自身字符的情况下,最长前缀和最长后缀相等均为 abc 长度为3 ,所以6位置的信息为 3,再来一个例子 aaaaak,那么k位置的信息为 4,前缀后缀均为aaaa,我们不能取5,因为刚刚说了,不能包含字符本身。再举一个例子 aabaabs 每一个字符所对应的信息为[-1,0,1,0,1,2,3] 对于0位置和1位置,我们人为规定,该位置就是-1和0,怎么解释,0位置,之前没有字符串,就没有不包含该位置的前缀与后缀,我们设置为 -1 ,而1位置呢,1位置前面有一个0位置,但我们说了,不能包含该字符串本身,所以就是0,其他的就按上述规则填写即可。上述的一个位置信息数组,就是KMP算法的核心,我们称之为next数组。
2.KMP算法核心,next数组。
上述例子不是有一个str1 和 str2 嘛,没错,这个next数组就是对str2求的。也就是说,我们如果要找str1中是否包含str2,我们需要先求出str2的一个next数组。
3.我们为什么需要str2的next数组?
因为它可以使我们整个匹配的过程加速。
4.怎么加速我们的匹配过程?
我们先假设str2已经拥有了next数组。
然后我们假设一个str1 前 ..... 都没和str2的字符匹配上,然后突然再i位置和str2的0位置字符匹配上了,然后一直都和str2匹配上了,知道s1到了x为止,s2到了y为止才没有匹配上,这个时候注意,我们的str2是不是都有一个信息,我们的y为止是不是存储了一个最长的前缀和最长的后缀的长度。我们再str1中找到和str2后缀串相同的长度,假如说我们再str1中叫j位置,然后我们把str2中0位置往后推,推到和j位置齐平。如下图
太长时间没写过字了,大家尽量看,是不是稍微的好理解一些。
实质一:
类似于 a = b ,b = c 所以 a = c 的实质。因为前缀串和后缀串相等,而后缀串和 j....x 相等,所以前缀串和 j.....x 相等,所以这部分就不需要重新匹配了,直接跳过,有一个小的加速。
实质二:
我们直接从 j 位置开始往后匹配了, 那 i+1位置呢, i+2位置呢, i+3位置呢,通过next数组,我们可以知道,其实这些位置都不可能匹配出str2,所以,这些位置也直接舍弃,又有一段小加速。
接下来来一个实际的例子。
s1 ..............aabaat.....................
s2 aabaab
我们b底下的信息是2,代表最长前缀aa 和最长后缀 aa 然后我们找到s1中对应的j位置,
s1 ..............aabaat.....................
s2 aabaab
接下来我们再这么比,看是不是一下子加速很多。比暴力算法少了很多比较,这就完了嘛,没有,你看b和t是不是也没对上啊,这个时候,我们的b是不是记的也有信息啊,我们拿出这个时候第一个b的信息。这个b的信息是啥呢,是1对不对 ,前缀为 a 后缀为 a 不能包含字符串本身,我们继续移。
s1 ..............aabaat.....................
s2 aabaab
然后t和第二个a又没对上,这时我们a的信息记录的是 0 这个时候,我们再往右推一个,第一个a和t 还是没对上,这个时候,a的对应信息是啥啊,是 -1,结束。
这些有部分重的都没对上,s1 t 位置继续往下走,如果遇到有重合的,继续重复该步骤。可以发现KMP算法少了很多重复的步骤。
知道KMP快了,接下来我们来解答一下实质二 为啥跳过了 i + 1 , i + 2, i + 3。
我们这么来假设,我们假设 i + 1, i + 2, i + 3 ...... j 之前存在一个位置能 匹配上 s2 ,我们假设该位置 为 k, 那么 k ....... x 是不是能搞定 s2 等量的前缀啊,为啥,我们都假设 能匹配出 s2 了,再没有等量的前缀,那么是怎么匹配出s2的。所以我们能推出拥有等量前缀,还有一点,我们是从i ........ x 可是一路相等的啊,我靠,这不就是 y 之前一个等量的后缀嘛,我们推出了y之前一个更长的前缀和后缀。跟我们的next数组中的信息矛盾了,如果我们next数组中信息求解正确,那么这种情况根本就不会发生。所以 i + 1, i + 2, i + 3 .....到 j之前不存在一个能匹配出 s2 的位置。
说了这么多,大家是不是觉得往右推这件事很复杂啊,其实一点都不复杂,还用原来的例子 aabaab 对应的信息 -1 0 1 0 1 2
a a b a a t
a a b a a b
t 和 b 比对失败了 ,那么往右推这件事啊其实就代表,验证的 t 还在原来位置,而底下验证的位置往 左跳,它跳到哪, b对应的信息就是他跳的位置。其实就是比对的位置,跳到next的值上。
如上图,是不是 b 往其对应的位置跳,就相当于 a 往后右推。很简单吧。
KMP代码:
public class KMP {
public int getIndexOf(String str1,String str2){
if (str1 == null || str2 == null || str2.length() < 1 ||str2.length() > str1.length()){
return -1;
}
char[] charsArr1 = str1.toCharArray();
char[] charsArr2 = str2.toCharArray();
int[] next = getNextArray(charsArr2);
int index1 = 0;
int index2 = 0;
while (index1 < charsArr1.length && index2 < str2.length()){
if (charsArr1[index1] == charsArr2[index2]){
index1++;
index2++;
}else if (next[index2] == -1){ // index2 == 0
index1++;
}else {
index2 = next[index2];
}
}
//如果index2 是越界的,那么一定是index2所有的都匹配上了。所以返回 x位置 - index2
//如果index2 不是越界的,那么一定是index1越界 index1越界,说明所有都没匹配上。
return index2 == str2.length() ? index1 - index2 : 0;
}
private int[] getNextArray(char[] str2) {
if (str2.length == 1){
return new int[]{-1};
}
int[] next = new int[str2.length];
next[0] = -1;
next[1] = 0;
int i = 2; //目前在哪个位置求next数组的值
int cn = 0; // cn表示哪个位置的字符 跟 i - 1 位置比
// 为啥cn 一开始是 0 因为 当i为 2 时谁和 1比呀,0位置呀,所以是 0
while ( i < next.length) {
if (str2[i - 1] == str2[cn]){ //匹配成功的时候
next[i++] = ++ cn; //设置了 i 位置该有的值,同时 我也让我下一个位置使用我的信息去完成它的位置
}else if (cn > 0){
cn = next[cn];
}else {
next[i++] = 0;
}
}
return next;
}
}
习题:我想知道两个字符串是否互为旋转字符串,啥意思,啥叫旋转字符串,例如 "123456" 我可以不转 "123456" ,我还可以把 1 转过去 变成 "234561",还可以把 12 转过去变成 "345612" 等等现在给你两个字符串,请你判断是否互为旋转串。
怎么做:
str1 和 str2 先判断长度一样不一样,如果长度不一样,直接false ,因为如果互为旋转串,长度一定一样,我们选择总不能旋转没一个吧。长度一样之后,我们用str1 + str1 生成一个大字符串,就是两个str1拼在一起的大字符串,我们称之为 str1s ,然后我们直接KMP,判断 str2是否是str1s的子串,如果是,原始的str1 和 str2 就互为旋转串。为啥,我们看 123456 拼接上 123456 是啥,是123456123456 在这个大字符串中,任何长度为 6 的子串都为原始串的旋转串。
是不是很简单,代码就是KMP的代码,就不重复写了。
接下来再看另一题。
有两颗树,如下图,我们假设左边的树叫T1右边是树叫T2 问 T1是否有某一个子树的结构和T2一样。如果T1和T2为上面的样子返回false如果为下面的样子,返回true。
// 3 4
// / \ /
// 1 4 5
// \ / \
// 2 5 1 F
// 3 4
// / \ /
// 1 4 5
// \ /
// 2 5 T
子树从一个头出发,所有东西都得要,左边以4为头的子树为 4 5 1 跟 4 5 不一样所以上面的返回false。
这道题我们把T1先序或者后续序列化成一个字符串,把T2先序或者后续序列化成一个字符串,然后我们通过KMP发现 T2是T1的子串,则T2是T1的子树。O(n)拿下。
举个栗子:
// 32 45
// / \ /
// 11 45 22
// / \ /
// 19 13 22
左边序列化完应该是这样的 ["32","11","19",null,null,13,null,null,"45","22",null,null,null] 这是我们序列化后的结果,这是一个字符类型的数组是吧。我们把整个数组理解为一个字符串,我们把第一个字符认为是 "32"这个字符串,"32"这个字符串,我们就理解为单个字符,"11"这个字符串我们就理解为单个字符。右边 45 22 也一样,序列化后为 ["45","22",null,null,null] 。 然后直接KMP ,怎么样,骚不骚。