题目:给定两个字符串s1
和s2
,判断s2
是否是s1
的子串,如果是则返回s2
首次出现在s1
的下标位置。
s1=AAAAAAAB, s2=AAAAB
暴力算法思路如下
index1
表示s1
的字符下标,index2
表示s2
的字符下标s1
的第i
(i
从0开始)个位置和s2
的第0个位置开始匹配,此时index1 = i
,index2 = 0
index1++
,index2++
index1 = i + 1
,index = 0
,相当于从s1
的第(i+1)个位置重新开始匹配匹配过程如下所示
第一轮比较
s1下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
s1 | A | A | A | A | A | A | A | B |
s2 | A | A | A | A | B | |||
s2下标 | 0 | 1 | 2 | 3 | 4 | |||
匹配结果 | √ | √ | √ | √ | × |
第一轮比较开始,从s1
的第1个位置与s2
的第一个位置开始比较,index1 = 0
,index2 = 0
如果s1[index1] = s2[index2]
,则index1++
,index2++
,继续往下比较
当index1 = 4
,index2 = 4
时
s1[index1] != s2[index2]
,退出比较,进入下一轮比较。
第二轮比较
s1下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
s1 | A | A | A | A | A | A | A | B |
s2 | A | A | A | A | B | |||
s2下标 | 0 | 1 | 2 | 3 | 4 | |||
匹配结果 | √ | √ | √ | √ | × |
第二轮比较开始,从s1
的第2个位置与s2
的第一个位置开始比较,index1 = 1
,index2 = 0
如果s1[index1] = s2[index2]
,则index1++
,index2++
,继续往下比较
当index1 = 5
,index2 = 4
时
s1[index1] != s2[index2]
,退出比较,进入下一轮比较。
第三轮比较
s1下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
s1 | A | A | A | A | A | A | A | B |
s2 | A | A | A | A | B | |||
s2下标 | 0 | 1 | 2 | 3 | 4 | |||
匹配结果 | √ | √ | √ | √ | × |
第三轮比较开始,从s1
的第3个位置与s2
的第一个位置开始比较,index1 = 2
,index2 = 0
如果s1[index1] = s2[index2]
,则index1++
,index2++
,继续往下比较
当index1 = 6
,index2 = 4
时
s1[index1] != s2[index2]
,退出比较,进入下一轮比较。
第四轮比较
s1下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
s1 | A | A | A | A | A | A | A | B |
s2 | A | A | A | A | B | |||
s2下标 | 0 | 1 | 2 | 3 | 4 | |||
匹配结果 | √ | √ | √ | √ | √ |
第四轮比较开始,从s1
的第4个位置与s2
的第一个位置开始比较,index1 = 3
,index2 = 0
如果s1[index1] = s2[index2]
,则index1++
,index2++
,继续往下比较
当index1 = 8
,index2 = 5
时,退出匹配过程
因为index2 = s2.lenght
,匹配成功,返回3
假设s1
的长度为N
,s2
的长度为M,最坏时间复杂度是O(NM)
代码如下
/**
* 暴力求解
* 判断s2是否是s1的子串, 返回s2首次出现在s1的下标
* 匹配思路
* 1. 使用index1表示s1的字符下标, index2表示s2的字符下标
* 2. 从s1的第i(i从0开始)个位置和s2的第0个位置开始匹配, 此时index1 = i, index2 = 0
* 如果遇到不相等的字符, 则退出匹配过程
* 则令index1 = i + 1, index = 0, 进入下一轮匹配, 相当于从s1的第(i+1)个位置重新开始匹配
*/
public static int blIndexOf(String s1, String s2) {
if (s1 == null || s2 == null || s1.length() < s2.length()) {
return -1;
}
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
for (int i = 0; i < str1.length; i++) {
// 从s1的第i个位置开始与s2的第0个位置开始匹配
int index1 = i, index2 = 0;
while (index1 < s1.length() && index2 < s2.length()) {
if (str1[index1] == str2[index2]) {
// 如果字符相等, 则两个下标向前推进, 判断下一个字符是否还相等
index1++;
index2++;
} else {
// 遇到不同的字符, 说明从s1的第i个位置开始, 无法匹配到s2, 退出匹配过程
break;
}
}
// 如果index2等于s2长度, 说明匹配成功
if (index2 == s2.length()) {
// return index1 - index2;
return i;
}
}
return -1;
}
KMP算法中需要用到前缀子串和后缀子串的概念。
比如字符串ABBABBC
,计算字符C
的前缀子串,字符C
前面的字符串为ABBABB
。
当长度为1时:前缀=A
,后缀=B
,前缀不等于后缀
当长度为2时:前缀=AB
,后缀=BB
,前缀不等于后缀
当长度为3时:前缀=ABB
,后缀=ABB
,前缀等于后缀
当长度为4时:前缀=ABBA
,后缀=BABB
,前缀不等于后缀
当长度为5时:前缀=ABBAB
,后缀=BBABB
,前缀不等于后缀
当长度为6时:前缀跟后缀不能等于整体
因此,字符C
的 [最长前缀跟最长后缀相等的长度] 等于3
在KMP算法中,会使用到**[最长前缀与最长后缀相等的长度]**,对于字符串ABBABBC
,字符C
最长前缀与最长后缀相等时的长度为3
KMP整体思路跟暴力算法是一样的,只是在匹配失败后不会每次都从头开始比较,会有一个加速过程。
KMP具体流程如下
使用index1
表示s1
的字符下标,index2
表示s2
的字符下标
第一轮比较
s1下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
s1 | A | A | A | A | A | A | A | B |
s2 | A | A | A | A | B | |||
s2下标 | 0 | 1 | 2 | 3 | 4 | |||
开始比较 | ↑ | |||||||
匹配结果 | √ | √ | √ | √ | × |
第一轮比较,从s1
的第1个字符(index1=0)和s2
的第1个字符(index2=0)开始逐个比较。
当index1=4、index2=4
时匹配失败(s1[4]!=s2[4]
)。
s1
和s2
的前四个字符AAAA
相等。字符s2[4]
的 [最长前缀等于最长后缀的长度] 为3。
可以得出:s1[4]
前面的三个字符肯定等于s2
的前三个字符,所以可以直接将s2
的前三个字符跟s1[4]
前面的三个字符对其,然后进入下一轮比较。
进入下一轮时,index1=4
不变,index2=3
。index2
不需要从头开始,这里可以减少3次不必要的比较。
第二轮比较
s1下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
s1 | A | A | A | A | A | A | A | B |
s2 | A | A | A | A | B | |||
s2下标 | 0 | 1 | 2 | 3 | 4 | |||
开始比较 | ↑ | |||||||
匹配结果 | √ | √ | √ | √ | × |
第二轮比较,从s1
的第5个字符(index1=4)和s2
的第4个字符(index2=3)开始逐个比较。
当index1=5、index2=4
时匹配失败(s1[5]!=s2[4]
)。
因为字符s2[4]
的 [最长前缀等于最长后缀的长度] 为3,因此s1[5]
前面的三个字符肯定等于s2
的前三个字符,直接将s2
的前三个字符跟s1[5]
前面的三个字符对其,然后进入下一轮比较。
进入下一轮时,index1=5
不变,index2=3
,index2
不需要从头开始,这里可以减少3次不必要的比较。
第三轮比较
s1下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
s1 | A | A | A | A | A | A | A | B |
s2 | A | A | A | A | B | |||
s2下标 | 0 | 1 | 2 | 3 | 4 | |||
开始比较 | ↑ | |||||||
匹配结果 | √ | √ | √ | √ | × |
第三轮比较,从s1
的第6个字符(index1=5)和s2
的第4个字符(index2=3)开始逐个比较。
当index1=6、index2=4
时匹配失败(s1[6]!=s2[4]
)。
因为字符s2[4]
的 [最长前缀等于最长后缀的长度] 为3,因此s1[5]
前面的三个字符肯定等于s2
的前三个字符,直接将s2
的前三个字符跟s1[5]
前面的三个字符对其,然后进入下一轮比较。
进入下一轮时,index1=6
不变,index2=3
,index2
不需要从头开始,这里可以减少3次不必要的比较。
第四轮比较
s1下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
s1 | A | A | A | A | A | A | A | B |
s2 | A | A | A | A | B | |||
s2下标 | 0 | 1 | 2 | 3 | 4 | |||
开始比较 | ↑ | |||||||
匹配结果 | √ | √ | √ | √ | √ |
第四轮比较,从s1
的第7个字符(index1=6)和s2
的第4个字符(index2=3)开始逐个比较。
最后能走到s1[7]=s2[4]
,匹配完成。
可以发现,KMP算法遇到不同字符进入下一轮比较时,如果s2[index2]
(此时的index2为匹配失败的位置)的 [最长前缀等于最长后缀的长度] 大于0,那么进入下一轮比较时index1
不会变,index2
也不会从头开始,这样就可以减少比较次数,达到了加速的效果。
为了达到加速效果,我们需要知道字符串s2
每个字符的 [最长前缀等于最长后缀的长度] ,KMP算法在匹配前需要计算一个next
数组,这个next
数组保存了s2
每个字符的 [最长前缀等于最长后缀的长度]
next
求解过程,假设字符串s
的长度为n
,i
的范围为[0,n-1]
,s
的next
数组长度为n
。
人为规定next[0]=-1
,因为s[0]
前面没有任何字符,也就相当于没有前缀
人为规定next[1]=0
,因为next[0]
前面只有一个字符,但是前缀跟后缀不能等于整体,所以next[1]=0
下面开始讨论i>1 && i < n
的情况
使用cn
保存 [最长前缀等于最长后缀的长度] ,cn
也是与s[i-1]
对比的下标。相当于s
的前cn
个字符与s[i-1]
的前cn
个字符相等。理解cn含义对于理解整个求解next数组过程很重要。
①next[0]=-1;next[1]=0;
②i = 2;
③cn = next[i - 1];
④开始计算next[i]
如果s[i-1] = s[cn]
,则
next[i] = cn + 1;
i++;
cn++;
继续执行步骤④。
如果s[i-1] != s[cn] && cn > 0
,则cn=next[cn]
,cn
往前推进,然后继续执行步骤④,该步骤不太容易理解。
如果s[i-1] != s[cn] && cn <= 0
,说明此时s[i]
不存在相等的前缀与后缀了,cn
不能往前走了
next[i] = 0;
i++;
求解next数组过程,当s[i-1] != s[cn] && cn > 0
时cn=next[cn]
,这个步骤不太容易理解,需要通过例子模拟这个过程才容易理解。
为了理解步骤④,举个例子,假设s=ABBSTABBECABBSTABB?C
,下标为[0,18]
的next
数据已经求出来了,下面开始求next[19]
假设s[18]=E
s | A | B | B | S | T | A | B | B | E | C | A | B | B | S | T | A | B | B | E | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
next | -1 | 0 | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ? |
当i=19
时,cn=8
,说明s
的前8个字符与s[18]
的前8个字符是相等的
此时s[i-1] = s[cn]
,即s[18] = s[8]
,说明s
的前9个字符与s[19]
的前9个字符相等,因此next[19] = cn + 1 = 9
然后i++
,i=20
,令cn++
,继续往下。
假设s[18]=S
s | A | B | B | S | T | A | B | B | E | C | A | B | B | S | T | A | B | B | S | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
next | -1 | 0 | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ? |
当i=19
时,cn=next[i-1]=next[18]=8
,说明s
的前8个字符与s[18]
的前8个字符是相等的。
①此时s[i-1] != s[cn] && cn > 0
,即s[18] != s[8] && 8 > 0
,说明s
的前9个字符与s[19]
的前9个字符不相等,但是s
的前8个字符与s[18]
的前8个字符是相等的,所以让cn
往前推进
这个时候cn = next[cn] = next[8] = 3
,说明s
的前3个字符与s[18]
的前3个字符是相等的。
②此时s[i-1] = s[cn]
,即s[18] = s[3]
,说明s
的前4个字符与s[19]
的前4个字符相等,因此next[19] = cn + 1 = 9
然后i++
,i=20
,令cn++
,继续往下。
假设s[18]=T
s | A | B | B | S | T | A | B | B | E | C | A | B | B | S | T | A | B | B | T | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
next | -1 | 0 | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ? |
当i=19
时,cn=next[i-1]=next[18]=8
,说明s
的前8个字符与s[18]
的前8个字符是相等的。
①此时s[i-1] != s[cn] && cn > 0
,即s[18] != s[8] && 8 > 0
,说明s
的前9个字符与s[19]
的前9个字符不相等,但是s
的前8个字符与s[18]
的前8个字符是相等的,所以让cn
往前推进
这个时候cn = next[cn] = next[8] = 3
,说明s
的前3个字符与s[18]
的前3个字符是相等的。
②此时s[i-1] != s[cn] && cn > 0
,即s[18] != s[3]&& 3 > 0
,说明s
的前4个字符与s[19]
的前4个字符不相等,但是s
的前3个字符与s[18]
的前3个字符是相等的,所以让cn
往前推进
这个时候cn = next[cn] = next[3] = 0
,说明s
的前0个字符与s[i-1]
的前0个字符相等。
③此时s[i-1] != s[cn] && cn <= 0
,说明找不到s[19]
前面的n
(n>0)个字符等于s
的前n
(n>0)个字符,即 [最长前缀等于最长后缀的长度] 为0,然后执行
next[19] = 0;
i++;
public class KMP {
/**
* 判断 m 是否是 s 的子串并返回 m 首次出现在 s 的下标
* 比如 s = abcdefg
* 若 m = def, 则返回3; 若 m = bbb, 则返回 -1
* 这里需要知道一个概念:前缀子串跟后缀子串
* 比如字符串:abbabb_ , 计算下划线前的前缀子串与后缀子串
* 长度为1时:前缀 = a, 后缀 = b, 前缀 != 后缀
* 长度为2时:前缀 = ab, 后缀 = bb, 前缀 != 后缀
* 长度为3时:前缀 = abb, 后缀 = abb, 前缀 = 后缀
* 长度为4时:前缀 = abba, 后缀 = babb, 前缀 != 后缀
* 长度为5时:前缀 = abbab, 后缀 = bbabb, 前缀 != 后缀
* 长度为6时:前缀跟后缀不能等于整体
*
* KMP算法中会用到一个辅助数组next,
* 这个数组记录的就是子串字符串在下标为i位置时最长前缀与最长后缀相等时的前缀长度(或者后缀长度)
* 比如字符串 m = abbabbc
* 根据上面的例子可以知道, i = 6 时, 最长前缀等于最长后缀的长度为3, 因此next[6]=3
* next[0] = -1, 因为下标为0之前已经没有字符了, 所以规定next[0] = -1
* next[1] = 0, 因为下标为1之前只有一个字符,因为前缀跟后缀不能等于整体,所以next[1]=0
*
* 有了这个辅助数组,在进行字符匹配的时候就可以减少很多次重复匹配
* 比如 s = abbabbabbs, m = abbabbs
* 第一次: i1 = 6, i2 = 6, s[6] = a, m[6] = s, s[6] != m[6]
* 此时出现第一次不相等, 通过m的next数组可以知道
* 对于m, 下标为6时, 它的最长前缀等于最长后缀的长度为3, s[6] != m[6]时, 令 i2 = next[i2] = 3
* 此时 i1 = 6, i2 = 3
*/
public static int getIndexOf(String s, String m) {
if (s == null || m == null || s.length() < m.length()) {
return -1;
}
char[] str1 = s.toCharArray();
char[] str2 = m.toCharArray();
// 获取next数组
int[] next = getNextArray(str2);
// i1 记录 str1 的下标, i2 记录 str2 的下标
int i1 = 0, i2 = 0;
while (i1 < str1.length && i2 < str2.length) {
if (str1[i1] == str2[i2]) {
// 当前字符相等
i1++;
i2++;
} else if (next[i2] == -1) {
// next[i2] == -1, 说明已经没有前缀了(str2[0]匹配失败), 只能让 str1 的下标往前走
i1++;
} else {
// i2 往前走
i2 = next[i2];
}
}
return i2 == str2.length ? i1 - i2 : -1;
}
/**
* next数组的计算逻辑
* 人为规定 next[0] = -1;
*/
private static int[] getNextArray(char[] str) {
if (str.length == 1) {
return new int[]{-1};
}
int[] next = new int[str.length];
next[0] = -1;
next[1] = 0;
int i = 2;
/**
* cn 代表了两个含义
* 1. 最长前缀等于最长后缀的长度
* 2. 与 i-1位置进行比较的下标
* 如果 str[cn] == str[i-1], 则next[i] = next[i-1]+1
* 如果 str[cn] != str[i-1] && cn > 0, 则 cn = next[cn]
* 如果 str[cn] != str[i-1] && cn <= 0, 则next[i] = 0
*/
int cn = next[i - 1];
while (i < str.length) {
if(str[cn] == str[i - 1]) {
next[i] = cn + 1;
i++;
/**
* 因为i已经加1了, 所以cn记录的应该是i加1之前的长度, 因此cn也要加1
* 相当于 cn = next[i-1]
*/
cn++;
} else if (cn > 0) {
cn = next[cn];
} else {
// cn 已经不能往前走了
next[i] = 0;
i++;
}
}
return next;
}
public static void main(String[] args) {
String str = "ABBSTABBECBBSTABBEC111111";
String match = "ABBSTABBECABBSTABBSC";
System.out.println(getIndexOf(str, match));
System.out.println(str.indexOf(match));
}
}