KMP

72 阅读4分钟
  • 在BF算法中,正常的回退操作应该是 j 指针回退到1并且 i 指针也要回退一部分,但是这里不用,这就KMP算法的妙处,通过预处理算出模式串中不与s匹配的那个字符之前的字符串(不包括这个没有匹配上的字符)的最大公共前后缀长度,这个后缀就是s中不匹配的字符前面的一小部分字符串,前缀就是模式串的前面一小部分字符串,这样子再进行回退的时候,j 指针就只用回退到上面所说的那个前缀的最后一个字符,而 i 指针不用进行回退,接下来就继续 i 与 j + 1进行匹配,重复这样的操作,直到结束

  • 在存储s和p时,从数组下标为1的地方开始存

  • Next[ i ]指的是以模式串下标为 i 的元素为末尾(算上这个元素),最长的公共前后缀的长度

  • 匹配详解

    • 首先初始化两个指针,指针 i 指向存储着s的数组的1索引,也就是s的第一个字符;另一个指针 j 指向p数组的0索引,也就是p的第零个字符
    • 接下来就是 i 的循环
      • 如果 j 指针没有指向0索引 并且 i 与 j + 1 不匹配,则将 j 指针回退到ne[ j ]的位置, 重新开始寻找
      • 如果如果 i 与 j + 1 匹配,那么 j 向后移动一位,如果此时 j 移动到了模式串的最末尾,也就是在s中找到了相匹配的模式串,可以进行匹配后的逻辑操作,然后 j 指针回退到ne[ j ]的位置
      • 此时到了一次循环的结尾,这个时候出现了两种情况:
        • 第一种情况:i 与原来的 j + 1匹配成功了,并且 j 也向后移动了一位,为了继续向后匹配,需要 i++,进入下一个循环进行进一步的匹配
        • 第二种情况:这个时候的 j 指向0索引了,并且 i 与 j + 1不匹配,也就是一开始第一个字符就不匹配了,这个时候就只能进入下一个循环,即 i++,重新开始匹配了
  • 预处理next数组

    • 首先初始化两个指针,指针 i 指向模式串的第二个元素,j 指向模式串的第零个元素
      • 为什么不算第一个元素?因为在进行匹配时,如果模式串的第一个元素都匹配不上,此时 j 指针其实就指向模式串的第零个元素,退无可退了,因此ne[ 1 ] = 0,这个时候只能 i++ 重新开始匹配
    • 接下来就是 i 的循环,每一次循环中都求出了以 i 指针指向的元素作为末尾(算上这个元素),最长的公共前后缀的长度
      • 如果 j 指针没有指向0索引 并且 i 与 j + 1 不匹配,则将 j 指针回退到ne[ j ]的位置, 重新开始寻找
      • 如果如果 i 与 j + 1 匹配,那么 j 向后移动一位
      • 将 j 赋给next[ i ]
        • 以 i 指针指向的元素作为末尾(算上这个元素),最长的公共前后缀的长度最长最长也就是i - 1因此考虑最极端的情况,只要匹配成功了一次,那么 j 就要向后移动一位的同时将自己赋给next[ i ]

模板

  • C++
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
//求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}
  • Java
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
//求模式串的Next数组:
for (int i = 2, j = 0; i <= m, i++) {
    while (j != 0 && p[i] != p[j + 1]) {
        j = ne[j];
    }
    if (p[i] == p[j + 1]) {
        j++;
    }
    ne[i] = j;
}

//匹配
for (int i = 1; j = 0; i <= n; i++) {
    while (j != 0 && s[i] != p[j + 1]) {
        j = ne[j];
    }
    if (s[i] == p[j + 1]) {
        j++;
    }
    if (j == m) {
        j = ne[j];
        //匹配成功后的逻辑
    }
}

练习

01 KMP字符串

  • 题目

Snipaste_2023-03-02_19-47-29.png

  • 题解
import java.io.*;

public class Main {
    public static final int N = 100010;
    public static final int M = 1000010;
    public static char[] s = new char[M];
    public static char[] p = new char[N];
    public static int[] ne = new int[N];
    public static int n, m;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new BufferedReader(new InputStreamReader(System.in)));
        PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
        n = Integer.parseInt(br.readLine());
        p = (" " + br.readLine()).toCharArray();
        m = Integer.parseInt(br.readLine());
        s = (" " + br.readLine()).toCharArray();

        for (int i = 2, j = 0; i <= n; i++) {
            while (j != 0 && p[i] != p[j + 1]) {
                j = ne[j];
            }
            if (p[i] == p[j + 1]) {
                j++;
            }
            ne[i] = j;
        }

        for (int i = 1, j = 0; i <= m; i++) {
            while (j != 0 && s[i] != p[j + 1]) {
                j = ne[j];
            }
            if (s[i] == p[j + 1]) {
                j++;
            }
            if (j == n) {
                j = ne[j];
                pw.print(i - n + " ");
            }
        }
        pw.close();
        br.close();
    }
}