文章目录
- 题目
- KMP 算法
- 1)例子演示
- 2)KMP算法思路
- 3)疑惑模型验证
- 4)求 next 数组
- 5)代码演示
- 6)复杂度分析
题目
有字符串 str1 和 str2 ,str1 中是否包含 str2,如果没有包含返回 -1,如果包含,则返回 str2 在 str1 中开始的位置
注:保证 str1 和 str2 字符串的长度大于 0,str1 长度为N,str2 长度为 M
之前有介绍过如果使用 BF 算法和 RK 算法来解决字符串匹配问题,但是两者都会有缺点,或是效率低下,或是性能不稳定,KMP 算法可以解决这样的问题,以一种非常巧妙的思路解决问题
链接直达——》字符串匹配算法 (BF & RK)
KMP 算法
1)例子演示
str1 和 str2 如图所示,准备变量 x、y,依次进行匹配,如果相同,x 和 y 共同向后移动一格
此时,str1 的 x 位置和 str2 的 y 位置的字符出现了不同,此时我将前面的 “ababa” 字符视作已匹配前缀
,在下一轮中我们将移动 y,继续进行匹配
我们需要在已匹配前缀(“ababa”)中找到最长前缀(后缀)可匹配子串
,即 “aba”,长度为 3
(注:最长前缀(后缀)可匹配子串不可以是已匹配前缀字符串本身)
在这一轮中,将 y 移动到 str2 的 3 下标上,x 位置和 y 位置的字符继续进行比较
发现 x 位置和 y 位置上的字符仍然不一样,这个时候的已匹配前缀为 “aba”,该字符串的最长前缀(后缀)可匹配子串是 “a”,长度为 1
在这一轮中,将 y 移动到 str2 的 1 下标上,x 位置和 y 位置的字符继续进行比较
发现 x 位置和 y 位置上的字符仍然不一样,这个时候的已匹配前缀为 “a”,该字符串的最长前缀(后缀)可匹配子串不存在了,长度为 0,那么将 y 移动到 str2 的 0 下标上,x 位置和 y 位置的字符继续进行比较
发现 x 位置和 y 位置上的字符仍然不一样,此时 y 已经没有办法再后退了,x 位置及其之前的字符都不可能成为成功匹配 str2 的首字符,x 向后移动一格
发现 x 位置和 y 位置上的字符终于一样了,x 和 y 一起向后移动一格,再比较字符,依次类推,直到 x 和 y 有一个等于对应的字符串的长度,就不再结束匹配操作。
如果此时 y 等于 str2 的长度,说明匹配成功了,返回 x - y,反之失败,返回 -1
2)KMP算法思路
- x、y 分别从 str1、str2 的 0 坐标出发,每次比较 x 位置的字符和 y 位置的字符
- x、y 位置的字符相同,就共同向后移动一格;
- 不同就根据已匹配前缀的最长前缀(后缀)可匹配子串的长度(len),将 y 移动到 str2 的 len 下标处;
- y 已经在下标 0 处,就把 x 向后移动一格
- 直到 x 或 y 越界。y 越界,匹配成功,反之,匹配失败
3)疑惑模型验证
提问1:
为什么在 y 位置和 x 位置的字符不同时,可以根据最长前缀(后缀)可匹配子串的长度将 y 直接移动?(假设 str1 从位置 i 到 位置 x-1这段字符串和 str2 从位置 0 到位置 y-1这段字符串是能够匹配成功的)
答1:
假设此时 ① 区域是最长前缀可匹配子串,② 区域是最长后缀可匹配子串,两区域字符串一定相等。
根据题意,亦可得 ③ 区域和 ② 区域是相等的,根据传递性, ① 区域的字符串等于 ③ 区域的字符串,y 可以直接移动到 ① 区域后面一个字符的位置,下一轮依旧比较 x 位置和 y 位置的字符
提问2:
为什么能够这么自信的跳过 i + 1 位置到 j - 1 位置的子符,坚信他们中不会有字符成为匹配成功 str2 的首字符?
答2:
首先明确好① 区域是最长前缀可匹配子串,② 区域是最长后缀可匹配子串
假设 str1 字符串中 i + 1 位置到 j - 1 位置中有 R 位置,R 位置就是能够成功匹配 str2 的首字符位置
那么因为直到 x 位置和 y 位置开始才有字符出现不同,那么 ④ 区域和 ③ 区域一定相同
R 是首字符位置,R 位置的字符一定等于 str2 中 0 下标的字符,不管怎么样 ⑤ 区域一定等于 ③区域,根据传递性, ④ 区域和 ⑤ 区域一定相同,和原本指定好的最长前缀(后缀)可匹配子串长度不同了,自相矛盾,假设不成立
4)求 next 数组
next 数组实际上就是一个一维数组,长度为 str2 字符串的长度,下标和 str2 字符串的下标是一一对应的,值
代表着对应子符的已匹配前缀的最长前缀(后缀)可匹配子串的长度
注:无前缀的情况,规定 next 对应的下标的值为 -1
那么如何求得 next 数组呢?如果每次求最长前缀(后缀)可匹配子串的长度时都要依次把已匹配前缀的的每一种情况进行比较尝试的话,效率显而易见的低
可以使用动态规划
,动态规划就是大事化小的分治思想,我们有明确的信息 next[0] = -1,next[1] = 0,没有办法下一子根据这两个信息获得 next[i] 的值,但是我们可以 next[1] 的值求得 next[2] 的值,只要把处理好的结果进行保存,那么在处理更大规模问题(求 next[i])时就可以使用这些结果
举例:
我们现在要求得 next[i] ,next[x](0 <= x <= i-1)的值是我们已经求得知晓的
已知 next[i-1] = 4,使用一个变量 j 指向 str2 的 j 下标上。
此时这个 j 有两层含义:
① next[i-1]的值
② 哪个位置的字符和 i-1 位置上的字符进行对比
没错,我们就要拿 j 位置的字符和 i-1 位置上的字符进行对比,从而尝试求得 next[i] 的值
下图所示,j 位置的字符和 i-1 位置的字符是一样的,那么 next[i] = ++j = 5
注:++j 实际上是两个步骤:① j 自增一变成 5 ② 求得 next[i] 的值是 5
如果 j 位置的字符和 i-1 位置的字符不同,"abab"这个最长前缀可匹配子串没法完整用上了,只能退而求其次,让 j 回退到 next[j] 处
再次比对,发现此时 j 位置的字符和 i-1 位置的字符是一样的,那么 next[i] = ++j = 3
假设发现 j 位置的字符和 i-1 位置的字符不同,那就一直遵循上面的思想,一直回退,直到 j = 0,退不了了,next[i] 的值就是 0
5)代码演示
public static int KMP(String str1,String str2) {
char[] chs1 = str1.toCharArray(); //变成字符数组
char[] chs2 = str2.toCharArray();
int[] next = getNextArray(chs2); //获取到 next 数组数据
int x = 0,y = 0;
//循环条件就是 x 和 y 都不可以越界
while (x < str1.length() && y < str2.length()) {
if (chs1[x] == chs2[y]) {
x ++; //相同一起走
y ++;
} else if (y > 0) {
y = next[y]; //不同,但是 y > 0,y 回退
} else {
x ++; //不同,并且 y == 0,无路可退,x 后移一格
}
}
return y == str2.length() ? x-y : -1; //y 越界匹配成功,反之失败
}
public static int[] getNextArray (char[] chs) {
int[] next = new int[chs.length];
next[0] = -1;
next[1] = 0; //初始值
int j = 0;//表示 next[i-1],回头需要和 i-1 位置上的字符进行比对
int i = 2;
while (i < next.length) {
if (chs[i-1] == chs[j]) {
next[i++] = ++j; //相同,next[i]=j+1,i++,j++
} else if (j > 0) {
j = next[j]; //不同,退而求其次,回退
} else {
next[i++] = 0; //不同,无路可退,next[i]=0,i++
}
}
return next;
}
6)复杂度分析
空间复杂度:
开辟的额外空间只有 next 数组,由于 str2 的长度为 M,所以空间复杂度
就是 O(M)
时间复杂度:
首先分析匹配过程(KMP 方法),在整个匹配过程中,str1 中的 x 变量是不会回退的(str1 不动),而 str2 中的 y 变量在匹配失败时是会向左回退的(相当于将 str2 向右推),直到在滑动的过程中匹配成功,循环结束,或者滑到最右,仍然匹配失败,循环结束
滑动长度为最大为 N,时间复杂度为 O(N)
在求 next 数组时,时间复杂度为 O(M)
所以总的时间复杂度
为 O(N+M)