判断字符串中是否包含有某子字符(串),是各种字符串相关算法题的基础,这里列举了博主学习时写的一些方法。如果包含某SubString,我们返回它在字符串的位置,如果不包含,返回-1。
1. 朴素算法
public static int matchSubString(String str, String sub) {
char[] strList = str.toCharArray();
char[] subList = sub.toCharArray();
int subLen = sub.length();
int strLen = str.length();
for(int i = 0; i <= strLen - subLen; i++) {
int temp = i;
int j = 0;
while(j < subLen && strList[temp] == subList[j]) {
j++;
temp++;
}
if (j == subLen) {
return i;
}
}
return -1;
}
2. String.indexOf() 方法
public static int matchSubString(String str, String sub) {
return str.indexOf(sub);
}
3. KMP算法
Donald Knuth 和 Vaughan Pratt 于1970年提出了线性复杂度的字符串匹配方法,James Morris在同年也独立发现了相同的方法,他们三人于1977年一起发布了相关论文,以他们三人姓氏命名的Knuth–Morris–Pratt(KMP)算法由此诞生。简单来说,与朴素算法最大的不同是,KMP算法不会去重复查找主字符串中已匹配过的字符,因此,KMP算法将朴素算法的O(m*n)优化成了O(m+n)。
朴素算法的思路是,如果本次迭代未匹配到,下次迭代把pattern后移一位,然后再次尝试匹配。而KMP算法运用了部分匹配表(Partial Match Table)来优化这个过程,这也是KMP算法的核心。我们用一个例子来说明,我们有如下两个字符串,主字符串和待匹配字符串pattern:
主String:
Pattern:
想要求出Pattern的PMT,首先我们要明白前缀和后缀的概念,比如我们有一个字符串 ababa
, 那么它的前缀有{a,ab,aba,abab},它的后缀有{a,ba,aba,baba},那么,字符串的最长公共前后缀就是aba
,长度为3,这就是我们要在PMT里填入的值。Pattern可以被看做不同长度的字符串,比如我们说到的ababa
,就是Pattern的前5位,那么在PMT的第五位就赋值为3,也就是PMT[4] = 3
。同理,完整的PMT表就是:
| a | b | a | b | a | c |
---|
0 | 1 | 2 | 3 | 4 | 5 |
a | 0 | | | | | |
ab | | 0 | | | | |
aba | | | 1 | | | |
abab | | | | 2 | | |
ababa | | | | | 3 | |
ababac | | | | | | 0 |
换种写法:1
char | a | b | a | b | a | c |
---|
index | 0 | 1 | 2 | 3 | 4 | 5 |
PMT | 0 | 0 | 1 | 2 | 3 | 0 |
Next | -1 | 0 | 0 | 1 | 2 | 3 |
为了方便使用,我们将PMT的值,整体后移一位,然后将空出来的第一位补上-1,记作Next行。个人认为这么做纯粹是为了编程简洁,在后面代码中会进一步说明。
### Iteration 1:
a | b | a | d | a | b | a | b | a | b | a | c |
---|
a | a | a | a | a | a | a | a | a | a | a | a |
a | b | a | b | a | c | | | | | | |
第一次迭代,Pattern的第四位匹配失败,朴素算法在此时会将Pattern后移一位。而KMP算法,就要查询Next数组中对应的值了,此次迭代,Pattern的第四位,也就是Pattern[3]匹配失败,那么我们便查询Next[3]对应的值,对应的值是1,那么,就将Pattern[1]的位置对齐到当前匹配失败的位置。
### Iteration 2:
a | b | a | d | a | b | a | b | a | b | a | c |
---|
a | a | a | a | a | a | a | a | a | a | a | a |
a | b | a | b | a | b | a | c | | | | |
第二次迭代,我们不用管Pattern[1]之前的字符了,因为根据部分匹配表,之前的字符已经是相同的了。然后Pattern[1]匹配失败,这里Next[1]对应的值是0,于是将Pattern[0]对齐当前匹配失败的位置。
### Iteration 3:
a | b | a | d | a | b | a | b | a | b | a | c |
---|
a | a | a | a | a | a | a | a | a | a | a | a |
a | b | a | a | b | a | b | a | c | | | |
第三次迭代,Patttern[0]匹配失败,Next[0]对应的-1, 所以将Pattern后移一位。这时就可以看出,实际上在Next表中添加的-1仅仅是为了后移一位,你完全可以设它为-100,只要在代码中你能判断出,当前是要整体后移一位就可以,只不过,使用-1可以使你的代码更简洁。
### Iteration 4:
a | b | a | d | a | b | a | b | a | b | a | c |
---|
a | a | a | a | a | a | a | a | a | a | a | a |
a | b | a | a | a | b | a | b | a | c | | |
第四次迭代,匹配到弟6位失败,Next[5]对应的是3,于是将Pattern[3]与当前匹配失败的位置对齐。
### Iteration 5:
a | b | a | d | a | b | a | b | a | b | a | c |
---|
a | a | a | a | a | a | a | a | a | a | a | a |
a | b | a | a | a | b | a | b | a | b | a | c |
第五次迭代,只需要看Pattern[3]、Pattern[4]和Pattern[5]是否匹配,结果是匹配,于是匹配完成。
### 代码:
我们可以先得出PMT,然后后移一位得出Next表。
`
Java
public static int[] PMT(String str) {
int[] pmt = new int[str.length()];
pmt[0] = 0;
int i = 0;
int j = 1;
int count = 0;
while(j < str.length()) {
if(str.charAt(i) == str.charAt(j)) {
count++;
pmt[j] = count;
i++;
j++;
}
else {
count = 0;
pmt[j] = 0;
i = 0;
j++;
}
}
return pmt;
}
public static int[] getNext(String str) {
int[] pmt = PMT(str);
int[] next = new int[str.length()];
next[0] = -1;
for (int i=0;i < str.length()-1; i++) {
next[i+1] = pmt[i];
}
return next;
}
`
当然我们也可以不写PMT表,我们直接得出Next表,这样会更加简洁:
`
java
public static int[] getNext(String str) {
int[] next = new int[str.length()];
next[0] = -1;
int i = 0;
int j = 1;
int count = 0;
while(j<str.length()-1) {
if(str.charAt(i) == str.charAt(j)) {
count++;
next[j+1] = count;
i++;
j++;
}
else {
count = 0;
next[j+1] = count;
i = 0;
j++;
}
}
return next;
}
`
我们的主循环就是:
`
java
public static int matchSubString(String str, String sub) {
char[] strList = str.toCharArray();
char[] subList = sub.toCharArray();
int[] next = getNext(sub);
int subLen = sub.length();
int strLen = str.length();
int i = 0,j = 0;
while(i<strLen && j<subLen ) {
// 这里就是设-1的好处,因为我们不用单独为了它写一种情况,比如:
// if (j == -100) {
// i++;
// j = 0;
// }
if (j == -1 || strList[i] == subList[j]) {
i++;
j++;
}
else{
j = next[j];
}
//匹配到字符串
if(j == subLen) {
return i - subLen;
}
}
//未匹配到
return -1;
}
`
## 3. 结果
input:
> str: Some Computing Science students are studying in the Computing Science Center.
> sub: Computing
output:
> 5
很显然,这几种方法都是返回最先找到的SubString位置,后面若有相同的SubString会被忽略掉。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。