KMP算法简介
是一种线性时间复杂度的字符串匹配、查找算法。
暴力实现字符串匹配
对于字符串的匹配,可以使用暴力进行匹配:
如图进行演示:(以a串 ABABABCAA 被b串 ABABC 匹配为例):
第一轮匹配:(从a串下标为0处开始比对,在下标4处出现了不同: A -> C)
a串 | A | B | A | B | A | B | C | A | A |
b串 | A | B | A | B | C | ||||
是否相同 | √ | √ | √ | √ | × |
第二轮匹配:(从a串下标为1处开始比对,在下标1处出现了不同: B -> A)
a串 | A | B | A | B | A | B | C | A | A |
b串 | A | B | A | B | C | ||||
是否相同 | × |
第二轮匹配:(从a串下标为2处开始比对)
a串 | A | B | A | B | A | B | C | A | A |
b串 | A | B | A | B | C | ||||
是否相同 | √ | √ | √ | √ | √ |
结束!
代码如下:
// 这个是暴力
public static int find(String a,String b){
for(int i = 0;i<a.length()-b.length();i++){
for(int j = 0; j<b.length();j++){
if(a.charAt(i+j) != b.charAt(j)){
break;
}
if(j == b.length()-1){
return i;
}
}
}
return -1;
}
但是 复杂度O(m*n)太高啦,显然 ⑧ 行。
KMP算法实现字符串匹配
首先我们对比一下之前的两个字符串:
a串 | A | B | A | B | A | B | C | A | A |
b串 | A | B | A | B | C | ||||
是否相同 | √ | √ | √ | √ | × |
在出现不同的时候,我们其实已经知道了,ABAB已经和前面匹配了,所以我们只需要知道最长公共前后缀就好了,such as : ABAB中 AB = AB 就是 2个相同的长度:
A | B | A | B | C |
0 | 0 | 1 | 2 | 0 |
这就是它的公共前后缀。也可以叫做next数组,(有的版本的kmp喜欢用整体向右移动,第一位设置为-1的表示法。其实都行)
我们先不考虑怎么求出这个next数组,我们先来看看已知next数组的话,怎么写我们的kmp代码:
逻辑如下:
假设i为a串的下标,j为b串的下标。
第一轮正常匹配(下标4出现错误,这时我们令j =next[3] = 2,也就是说我们不需要重置i的位置,保证O(n),接下来比对a串i位置的字符和b串j位置的字符开始比较,如第二轮):
a串 | A | B | A | B | A | B | C | A | A |
b串 | A | B | A | B | C | ||||
是否相同 | √ | √ | √ | √ | × |
第二轮:
a串 | A | B | A | B | A | B | C | A | A |
b串 | A | B | A | B | C | ||||
是否相同 | √ | √ | √ | √ | √ |
这样子的话,复杂度就降低了。代码如下:
public static int kmp(String a,String b){
int next[] = getNext(b); //先求出next数组
int i = 0; //主串a的指针
int j = 0; //子串b的指针
while(i < a.length()){
if(a.charAt(i) == b.charAt(j)){
i++;
j++;
}
else if(j > 0){ //匹配失败,根据next跳过
j = next[j-1];
}else{ //子串第一个匹配就失败了
i++;
}
if(j == b.length()){ //匹配成功了
return i-j;
}
}
return -1;
}
接下来就是求出这个next数组
对于ABABC,找出最长公共前后缀,可以用暴力来找,但是复杂度并不好,解决的复杂度的方法就是使用递推,我们在遍历字符串的时候,同样定义最长的相同前后缀作为下一个next的值,具体看代码理解:
private static int[] getNext(String b) {
int next[] = new int[b.length()];
next[0] = 0;
int i = 1;
int prefix_len = 0; //当前公共前后缀长度
while (i < b.length()) {
if (b.charAt(i) == b.charAt(prefix_len)) {
prefix_len++;
next[i] = prefix_len;
i++;
} else if (prefix_len == 0) {
next[i] = 0;
i++;
} else {
prefix_len = next[prefix_len - 1]; //回退
}
}
return next;
}
所有代码:
package 算法;
import java.util.Scanner;
/**
* @Author: stukk
* @Description:
* @DateTime: 2023-11-11 14:36
**/
public class kmpDemo {
// 匹配两个字符串,如果a中存在b,那么返回下标,否则返回-1
public static void main(String[] args) {
//匹配字符串
Scanner cin = new Scanner(System.in);
String a = cin.nextLine();
String b = cin.nextLine();
System.out.println(kmp(a, b));
}
// 这个是kmp
public static int kmp(String a, String b) {
int next[] = getNext(b); //先求出next数组
int i = 0; //主串a的指针
int j = 0; //子串b的指针
while (i < a.length()) {
if (a.charAt(i) == b.charAt(j)) {
i++;
j++;
} else if (j > 0) { //匹配失败,根据next跳过
j = next[j - 1];
} else { //子串第一个匹配就失败了
i++;
}
if (j == b.length()) {
return i - j;
}
}
return -1;
}
private static int[] getNext(String b) {
int next[] = new int[b.length()];
next[0] = 0;
int i = 1;
int prefix_len = 0; //当前公共前后缀长度
while (i < b.length()) {
if (b.charAt(i) == b.charAt(prefix_len)) {
prefix_len++;
next[i] = prefix_len;
i++;
} else if (prefix_len == 0) {
next[i] = 0;
i++;
} else {
prefix_len = next[prefix_len - 1]; //回退
}
}
return next;
}
// 这个是暴力
public static int find(String a, String b) {
for (int i = 0; i < a.length() - b.length(); i++) {
for (int j = 0; j < b.length(); j++) {
if (a.charAt(i + j) != b.charAt(j)) {
break;
}
if (j == b.length() - 1) {
return i;
}
}
}
return -1;
}
}
例题
现在我们可以用一道稍微变形的题来训练一下:
P3375 【模板】KMP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述
给出两个字符串 s1 和 s2,若 s1 的区间 [l,r] 子串与 s2 完全相同,则称 s2 在 s1 中出现了,其出现位置为 l。
现在请你求出 s2 在 s1 中所有出现的位置。
定义一个字符串 s 的 border 为 s 的一个非 s 本身的子串 t,满足 t 既是 s 的前缀,又是 s 的后缀。
对于 s2,你还需要求出对于其每个前缀 ′s′ 的最长 border ′t′ 的长度。
输入格式
第一行为一个字符串,即为 s1。
第二行为一个字符串,即为 s2。
输出格式
首先输出若干行,每行一个整数,按从小到大的顺序输出 s2 在 s1 中出现的位置。
最后一行输出∣s2∣ 个整数,第 i 个整数表示 s2 的长度为 i 的前缀的最长 border 长度。
输入输出样例
输入 #1复制
ABABABC ABA
输出 #1复制
1 3 0 0 1
AC代码:
import java.io.*;
import java.util.Scanner;
/**
* @Author: stukk
* @Description:
* @DateTime: 2023-11-11 17:06
**/
public class Main {
private static BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
private static PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
private static int aLen,bLen;
public static void main(String[] args) throws IOException {
String a = bf.readLine();
String b = bf.readLine();
aLen = a.length();
bLen = b.length();
kmp(a,b);
pw.flush();
}
private static void kmp(String a, String b) {
int next[] = getNext(b);
int l = 0;
int r = 0;
while(l < aLen){
if(a.charAt(l) == b.charAt(r)){
l++;
r++;
}else if(r > 0){
r = next[r-1];
}else{
l++;
}
if(r == bLen){
int ans = l - r + 1;
pw.println(ans);
r = next[r-1];
}
}
for(int i = 0;i<next.length;i++){
pw.print(next[i]+" ");
}
}
private static int[] getNext(String b) {
int next[] = new int[bLen];
next[0] = 0;
int prefix = 0;
int i = 1;
while(i < bLen){
if(b.charAt(i) == b.charAt(prefix)){
prefix ++;
next[i] = prefix;
i++;
}else{
if(prefix == 0){
next[i] = 0;
i++;
}else{
prefix = next[prefix - 1];
}
}
}
return next;
}
}
稍微变一变代码就可以AC
总结
其实KMP算法也在告诉我们一个重要的道理:我们能够走的多远不取决于顺境时能走多块,而是取决于逆境时能多久找回曾经的自己。