题目:给定两个字符串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算法
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数组
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++;
Java代码
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));
}
}
文章讲述了如何判断一个字符串s2是否是另一个字符串s1的子串,首先介绍了暴力算法的基本思路,即逐个字符比较,然后详细解析了KMP算法,包括最长前缀等于最长后缀的长度概念,以及如何利用next数组优化匹配过程,从而减少不必要的比较次数。
911

被折叠的 条评论
为什么被折叠?



