1. 暴力匹配算法
i=0、j=0,S.charAt(i)==P.charAt(j)、i++、j++,直到i=6、j=6、不再相等
字符串S | a | b | b | a | a | b | b | a | a | b | a | 匹配结果 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
模式串P | a | b | b | a | a | b | a | 不匹配,右移 |
回退i,i=1、j=0,字符不相等
字符串S | a | b | b | a | a | b | b | a | a | b | a | 匹配结果 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
模式串P | a | b | b | a | a | b | a | 不匹配,右移 |
i=2、j=0,字符不相等
字符串S | a | b | b | a | a | b | b | a | a | b | a | 匹配结果 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
模式串P | a | b | b | a | a | b | a | 不匹配,右移 |
i=3、j=0,i=4、j=1、不相等
字符串S | a | b | b | a | a | b | b | a | a | b | a | 匹配结果 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
模式串P | a | b | b | a | a | b | a | 不匹配,右移 |
回退i,i=4、j=0,i=5、j=1,,,直到i=10、j=7,匹配完成
字符串S | a | b | b | a | a | b | b | a | a | b | a | 匹配结果 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
模式串P | a | b | b | a | a | b | a | 匹配,不移动 |
- 存在的问题:如果模式串与字符串的前几个字符都匹配,但下一个字符不匹配,则需要回退i,浪费时间
- 解决思路:固定字符串S,只移动模式串P
以第一次匹配为例,可以发现字符串S的5、6位与模式串P的前2位相同。在i=6、j=6时,匹配失败,可将模式串P向右移动4位,从i=6、j=2继续开始匹配,保证了i不回退
字符串S | a | b | b | a | a | b | b | a | a | b | a |
---|---|---|---|---|---|---|---|---|---|---|---|
模式串P | a | b | b | a | a | b | a |
2. KMP算法
-
引入2个概念
- 前缀:从首字符开始的子串,不包含尾字符,abcde前缀:a、ab、abc、abcd
- 后缀:从尾字符开始的子串,不包含首字符,abcde后缀:e、de、cde、bcde
-
KMP需要找到字符串中最长的相同前后缀
- 比如:ababa
- 前缀有:a、ab、aba、abab
- 后缀有:a、ba、aba、baba
- 相同的前后缀:a、aba
- 最长的相同前后缀:aba
-
部分匹配值:最长的相同前后缀的长度,ababa的部分匹配值 = 3
-
部分匹配表:依据部分匹配值
a | b | b | a | a | b | b | a | a | b | a | 说明 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | a的前后缀均为空 | ||||||||||
0 | 0 | ab的前缀为a、后缀为b,相同前后缀为空 | |||||||||
0 | 0 | 0 | abb相同前后缀为空 | ||||||||
0 | 0 | 0 | 0 | abba相同前后缀为a,部分匹配值=1 | |||||||
0 | 0 | 0 | 0 | 1 | abbaa相同前后缀为a,部分匹配值=1 | ||||||
0 | 0 | 0 | 0 | 1 | 2 | abbaab相同前后缀为ab,部分匹配值=2 | |||||
0 | 0 | 0 | 0 | 1 | 2 | 3 | abbaabb相同前后缀为abb,部分匹配值=3 |
- 代码实现
步骤1:计算模式串的部分匹配表
//获取到一个字符串(子串) 的部分匹配值表
public static int[] kmpNext(String dest) {
//创建一个next 数组保存部分匹配值
int[] next = new int[dest.length()];
next[0] = 0; //如果字符串是长度为1 部分匹配值就是0
for(int i = 1, j = 0; i < dest.length(); i++) {
//当dest.charAt(i) != dest.charAt(j) ,我们需要从next[j-1]获取新的j
//直到我们发现 有 dest.charAt(i) == dest.charAt(j)成立才退出
//这是kmp算法的核心点
while(j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j-1];
}
//当dest.charAt(i) == dest.charAt(j) 满足时,部分匹配值就是j+1
if(dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
//测试
public static void main(String[] args) {
int[] next=kmpNext("AAA");
System.out.println("next="+Arrays.toString(next));
}
//输出结果:next=[0, 1, 2]
步骤二:在遍历匹配中依据部分匹配表来右移模式串,失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的部分匹配值
/**
* @param str1 源字符串
* @param str2 子串
* @param next 部分匹配表, 是子串对应的部分匹配表
* @return 如果是-1就是没有匹配到,否则返回第一个匹配的位置
*/
public static int kmpSearch(String str1, String str2, int[] next) {
//遍历
for(int i = 0, j = 0; i < str1.length(); i++) {
//需要处理 str1.charAt(i) != str2.charAt(j), 去调整j的大小
//KMP算法核心点, 可以验证...
while( j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j-1];
}
if(str1.charAt(i) == str2.charAt(j)) {
j++;
}
if(j == str2.length()) {//找到了 // j = 3 i
return i - j + 1;
}
}
return -1;
}
- 详细阅读,参考:从头到尾彻底理解KMP