应用场景:字符串匹配问题
暴力匹配
《算法(第4版)》
思路分析
- 假设现在 str1 匹配到 i 位置,子串 str2 匹配到 j 位置,则有:
- 如果当前字符匹配成功 (即
str1[i] == str2[j]
),则 i++,j++,然后继续匹配下一个字符 - 如果匹配失败 (即
str1[i] != str2[j]
),令 i = i - j + 1,j = 0;相当于每次匹配失败时,i 回溯,j 被置为 0
- 如果当前字符匹配成功 (即
- 用暴力方法解决的话就会有大量的回溯
- 每次只移动一位;若是不匹配,则 i 便移动到 头一个相匹配的字符 的下一位 接着判断
- 浪费了大量的时间 // 不可行!
代码实现
public class ViolenceMatch {
public static void main(String[] args) {
String str1 = "硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好";
String str2 = "尚硅谷你尚硅你";
int index = violenceMathch(str1, str2);
System.out.println(index);
}
public static int violenceMathch(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int s1Len = s1.length;
int s2Len = s2.length;
int i = 0, j = 0; // i指向s1, j指向s2
// 保证在匹配过程中, 不会出现越界
while (i < s1Len && j < s2Len) {
if (s1[i] == s2[j]) { // 匹配成功
i++;
j++;
} else { // 匹配不上
i = i - j + 1;
j = 0;
}
}
// 判断是否匹配成功
if (j == s2Len) return i - j;
else return -1;
}
}
KMP算法
算法简述
- Knuth-Morris-Pratt 字符串查找算法,简称为 "KMP算法",常用于在一个文本串 S 内查找一个模式串 P 的出现位置,这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。
- KMP 是一个解决“模式串”在“文本串”是否出现过,如果出现过,最早出现的位置的经典算法
- KMP 算法的核心是通过一个被称为 [部分匹配表(Partial Match Table)] 的数组 // 见 #3.2.2
- 其中保存了“模式串”中前后最长公共子序列的长度
- 如果“模式串”有 n 个字符,那么 PMT 就会有 n 个值
- 每次回溯时,通过 PMT 找到,前面匹配过的位置,省去了大量的计算时间
思路分析
通过案例整理思路
部分匹配表
- 先了解下什么是前缀,后缀
- 部分匹配值:"前缀"和"后缀"的最长的共有元素的长度
- "部分匹配"的实质
如何使用这个表来加速字符串的查找,以及这样用的道理是什么?
- 如果在
j
处字符不匹配,那么由于前边所说的模式字符串 PMT 的性质, {主字符串} 中i
指针之前的PMT[j − 1]
位就一定与 {模式字符串} 的前PMT[j−1]
位是相同的 - 故 可直接省略前后缀那
n
位的比对;也就是说,虽然要重新比对了,但还没开始呢就已经完成字符串的部分匹配了(和暴力匹配比起来那算是赢在起跑线了 ...) - 接下来从
n+1
位的那个字符开始比对,前面已经利用 [最长前后缀] 直接完成了部分(n个字符)的匹配任务
next[] 生成
next[] 用于记录“模式串”每 1 个索引位置的最长公共前后缀,也就是标记实际字符串匹配过程中在某 1 位遇到不匹配的情况时,“模式串的”的 k 应该移动到的位置。
- 首先模式串的第 0 位字符公共前后缀长度为 0
- 进入循环,首先 i 在 1 的位置,k 在 0 的位置 (要注意 k 的位置和 next 数组下标没关系,k 是对于原来模式串而言的位置,现在在第 0 个 a 上)。如图,发现 p[i] = p[k],说明 i 位置最长公共前后缀为 1,记入 next[],ne[1] = 1。
- 现在 k=1,在第二个 a 上。紧接着 i=2,发现 p[i]!=p[k],这时候怎么办呢?
- 问问 k 前面的老兄:“你的公共前后缀是多少啊?”,k-1 说自己公共前后缀是 0
- 那好吧,没得跳转了,直接 ne[2] = 0,下一个字符只能从头开始对了
- 此时 i=3,k=0,发现一路顺风顺水
- i=3 后面的字符 和 从头开始的模式串一直都匹配
- 所以公共前后缀长度不断加 1,直到 i=8
- 此时 k=5,而 p[i] != p[k]
- 这时候又问问 k 前面的老兄的公共前后缀,k-1 说我的公共前后缀是 2
- 说明k 前面的 2 个字符 和 打头的 2 个字符 是一样的;那么我们下一次直接从第 3 个字符匹配
- 此时 k=2,i=8,发现还有 p[i] != p[k]
- 再问问 k-1,k-1 说我的公共前后缀长度为 1
- 说明 k 之前的那 1 个字符和开头的第 1 个字符是一样的,那么我们下一次直接从第 2 个字符匹配
- 此时k=1, i=8。发现终于 p[i] == p[k],那么 ne[i] 等于多少呢?
- k-1 的公共前后缀的长度加上这次匹配的字符个数 1,即 ne[8]=2;匹配完成~
- [sum] 如果说KMP是一个模式串匹配主串的过程;那么,next生成过程就是模式串自己匹配自己的过程
代码实现
public class KMPAlgorithm {
public static void main(String[] args) {
String mainStr = "BBC ABCDAB ABCDABCDABDE";
String pattern = "ABCDABD";
int[] next = kmpNext(pattern);
int index = kmpSearch(mainStr, pattern, next);
System.out.println(index);
}
/**
* 通过KMP算法在主串中查找模式串出现的位置
* @param mainStr 主串
* @param pattern 模式串
* @param next 模式串的部分匹配表
* @return 返回模式串首字符在主串中的索引; 如果没有, 返回-1
*/
public static int kmpSearch(String mainStr, String pattern, int[] next) {
for (int i = 0, j = 0; i < mainStr.length(); i++) {
// i 不用动, 让 j 直接到最长前后缀的后一个字符位置
while (j > 0 && mainStr.charAt(i) != pattern.charAt(j)) j = next[j-1];
// 当前字符匹配,都往后移动一位
if (mainStr.charAt(i) == pattern.charAt(j)) j++;
// 匹配
if (j == pattern.length()) return i - j + 1;
}
return -1;
}
// 获取一个字符串(子串)的部分匹配值表
public static int[] kmpNext(String pattern) {
int[] next = new int[pattern.length()];
next[0] = 0;
// 每一位的最长前后缀,i 索引着后缀,j 索引着前缀
for (int i = 1, j = 0; i < pattern.length(); i++) {
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) j = next[j-1];
if (pattern.charAt(i) == pattern.charAt(j)) j++;
next[i] = j;
}
return next;
}
}
return i - ( j - 1 )
- n 个字符,站在最后 1 个字符的位置上,要往前移动多少次,可以回到第一个字符?显而易见, n-1 次
- kmpSearch~return 这里,匹配成功后,i 要回到(返回)和 模式串 相匹配的第 1 个字符的位置
- 和上面是一样的道理,要往前滑动 n - 1 次,由于此时 j == n,所以是
i - (j - 1)