KMP算法讲解、Java实现及面试题目
KMP算法讲解
- KMP用来干什么?
在说KMP算法之前,我们还是先来说一下KMP算法是用来解决什么问题的。
- 问题:假如现在有两个字符串s1(aabcabced)和字符串s2(abcabce)
现在要求:s2在s1中第一次出现的下标(上面的例子结果下标就为1)。
暴力的解法是:遍历一遍s1的所有字符,并且以遍历到的字符为起始与s2进行比对,如果有完全匹配的字符串,则对应的 起始字符下标便是所求的下标;这样的解法时间复杂度是O(N*M)。
而KMP算法就是对暴力解法的优化。
- KMP算法
KMP算法优化主要是提出了一个next数组(跟s2有关系),那么这个是用来干什么的呢?
next数组存的是当前字符前的字符串(不包括自己)的最长的 前缀和 后缀 相同的长度(第一个字符和第二个字符在next中规定为-1和0)
假设s2是(abcabce)那么next数组存储对应如下所示:
s2 | a | b | c | a | b | c | e |
---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
next | -1 | 0 | 0 | 0 | 1 | 2 | 3 |
(1)现在规定next下标为0时值为-1,下标为1时值为0;因为当字符为第一个字符时,没有前缀。当字符为第二个字符时,前缀和后缀一样所以为0;
(2)当下标为2时,前面的字符串为ab,那么他的前缀为a,后缀为b时不相同,所以赋值为0;
(3)同理当下标为3时也为0;
(4)当下标为4时,b前面的字符串,前缀为a和后缀为a时,相同且是最长的,所以赋值为1
(5)当下标为5时,c前面的字符串,前缀为ab和后缀为ab时,相同且是最长的,所以赋值为2
(6)当下标为6时,e前面的字符串,前缀为abc和后缀为abc时,相同且是最长的,所以赋值为3
求出next数组后,接下来就要进行求解了:
(1)首先初始化两个指针x,y,分别记录两个字符串s1,s2,当前的下标;
(2)当两个下标所对应的字符相同则同时自增
(3)不同,则在next数组中寻找y对应的下标的值(即y对应当前字符的前面字符串的前缀的下标),判断其是否为-1,为-1则y=0,且x与y对应的两个字符都不相同,所以x自增1
(4)若不为-1,则表示y当前字符前面可能还存在前缀,所以在next中查找对应y下标的值,并将其赋给y(因为next中存的是长度,所以换成下标的话则为前缀后面一位),继续从头重复前面的步骤,直到越界。
这样求解的算法复杂度为O(N)
Java代码
求s2字符串的next数组:
/*
* 求的是当前字符的最长的前缀和后缀相同的长度例如abcabcd(d的前面的字符串的 前缀和后缀相同的 最长的字符串是abc)
*/
public static int[] getNext(char[] str) {
if(str.length == 1) return new int[] {-1};
int [] next = new int [str.length];
next[0] = -1;//初始化next数组的前两个值
next[1] = 0;
int i = 2;
int cn = 0;//记录前一个数的前缀下一个字符的的下标
while(i < str.length) {
if(str[i - 1] == str[cn]) {
next[i++] = ++cn;//两个字符相同则++
}else if(cn > 0){
cn = next[cn];//如果不相等,且cn大于0,则将next数组中当前cn下标的值赋给cn,即cn下标的最小前缀
}else {
next[i++] = 0;//当cn==0时,当前i对应next为0
}
}
return next;
}
求解s2字符串在s1字符串中第一次出现的下标:
public static int kmp(String s1, String s2) {
if(s1.length() < 1 || s2.length() < 1) return -1;
char[] chars1 = s1.toCharArray();
char[] chars2 = s2.toCharArray();
int [] next = getNext(chars2);//获取s2的next数组
int x = 0;//初始化两个指针
int y = 0;
while(x < s1.length() && y < s2.length()) {
if(chars1[x] == chars2[y]) {//相同则自增一
x++;
y++;
}else if(next[y] == -1) {//找到头了,就重新开始
x++;
}else {
y = next[y];//不同则寻找前一个
}
}
return y == s2.length() ? x - y : -1;
}
面试题目
京东面试题:给一个原始串例如:abcabc
要求:在原始串后添加字符,使添加字符串最短,并且可以使原字符串出现两次,并且起始位置不同:例如:abcabcabc
解法:此题与上面求next数组同理,可以将原始串类比成,求当前字符的最长的前缀和后缀相同的长度;则最长前缀为abc
所以只需要在原始串的基础上继续添加,abc前缀后面的字符串就可以了。
Java代码
public static int[] getNexts(char[] str) {
if(str.length == 1) return new int[] {-1};
int [] next = new int[str.length];
next[0] = 0;
int cn = 0;
int i = 1;
while(i < str.length) {
if(str[cn] == str[i]) {
next[i++] = ++cn;
}else if(cn > 0) {
cn = next[cn];
}else {
i++;
}
}
return next;
}
public static String remke(String s) {
if(s.length() <= 0)return "";
int [] next = getNexts(s.toCharArray());
return s + s.substring(next[next.length - 1], s.length());
}
进阶题目:判断一棵二叉树是否是另一棵二叉树的子树
解法:将两棵二叉树进行序列化成字符串,将问题变为求一个字符串是否是另一个字符串的子串,然后采用KMP算法。