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;
    }
}

java 字符串匹配 日期格式 java字符串匹配数字_数据结构

稍微变一变代码就可以AC

总结

其实KMP算法也在告诉我们一个重要的道理:我们能够走的多远不取决于顺境时能走多块,而是取决于逆境时能多久找回曾经的自己。