字符串前缀字符串后缀

  • 字符串前缀(Proper prefix) :包含第一个字符,不包含最后一个字符的所有子串 例如:abababca 的前缀:a、ab、aba、abab、ababa、ababab、abababc

  • 字符串后缀(Proper suffix):不包含第一个字符,包含最后一个字符的所有子串 例如:abababca 的后缀:a、ca、bca、abca、babca、ababca、bababca

字符串部分匹配表

字符串部分匹配表 (Partial Match Table) 也称为 next 数组,例如:abababca 的部分匹配表为:

charabababca
index01234567
value00123401

每列 value 值表示前 index + 1 个字符子串的最大字符串前缀与字符串后缀相匹配的长度,例如:

  • index 为 0 的子串为 a,没有字符串前缀和字符串后缀,所以 value 为 0
  • index 为 1 的子串为 ab,字符串前缀为 a,字符串后缀为 b,没有相匹配的字符串前缀与字符串后缀,所以 value 为 0
  • index 为 2 的子串为 aba,字符串前缀为 a、ab,字符串后缀为 a、ba,字符串前缀与字符串后缀相匹配的子串为 a,长度为 1,所以 value 为 1
  • index 为 3 的子串为 abab,字符串前缀为 a、ab、aba,字符串后缀为 b、ab、bab,字符串前缀与字符串后缀相匹配的子串为 ab,长度 2,所以 value 为 2
  • index 为 4 的子串为 ababa,字符串前缀为 a、ab、aba、abab,字符串后缀为 a、ba、aba、baba,字符串前缀与字符串后缀相匹配的子串为 a、aba,长度最大的为 aba ,所以 value 为 3
  • index 为 7 的子串为 abababca,字符串前缀为 a、ab、aba、abab、ababa、ababab、abababc,字符串后缀为 a、ca、bca、abca、babca、ababca、bababca,字符串前缀与字符串后缀相匹配的子串为 a,所以 value 为 1

KMP 算法

KMP 算法就是利用字符串部分匹配表可以计算出当模式串与主串不匹配时,模式串可以多后移几位 (默认后移 1 位)

当模式串与主串不匹配时,如果不匹配字符对应模式串下标大于 0 (非首个模式串字符),取此字符前一个字符对应字符串部分匹配表中的 value ,如果 value 大于 0,模式串中不匹配字符之前 (不含不匹配字符) 子串长度减去 value 即模式串为后移的位数。

暴力匹配算法当模式串和主串不匹配时,主串匹配下标 +1,模式串匹配下标置为 0KMP 算法优化点在于模式串匹配下标置为 value

例如在主串 bacbababaabcbab 中查找模式串 abababca

  • 第一次符合以上规则的情况如下,模式串与主串不匹配字符 (b) 前一个字符为 a,对应字符串部分匹配表 index0value0,所以不存在多后移情况
bacbababaabcbab
  |
 abababca
  • 第二次符合以上规则的情况如下
bacbababaabcbab
         |
    abababca
  • 模式串与主串不匹配字符 (b) 前一个字符是 a,对应字符串部分匹配表 index4value3,不匹配字符之前模式串为 ababa 长度为 5, 所以后移 5-3=2
bacbababaabcbab
         |
      abababca
  • 模式串与主串不匹配字符 (b) 前一个字符是 a,对应字符串部分匹配表 index2value1,不匹配字符之前模式串为 aba 长度为 3, 所以后移 3-1=2
bacbababaabcbab
           |
         abababca
  • 此时,模式串长度已经比剩余主串长,匹配结束。

代码实现

next 数组的代码实现思路参考 KMP 算法的 Next 数组详解

/*
 * The MIT License (MIT)
 *
 * Copyright © 2022 xrv <xrv@live.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package io.github.ehlxr.algorithm.match;

import java.util.Arrays;

/**
 * 字符串匹配算法 KPM
 *
 * @author ehlxr
 * @since 2022-03-13 10:06.
 */
public class Kmp {
    public static void main(String[] args) {
        String s = "bacbababaabcbab";
        String p = "abab";

        System.out.println(Arrays.toString(getNexts(p)));
        System.out.println(kmp(s, p));
    }

    public static int kmp(String s, String p) {
        char[] scs = s.toCharArray();
        char[] pcs = p.toCharArray();

        int m = s.length();
        int n = p.length();

        int[] next = getNexts(p);

        for (int i = 0; i <= m - n; i++) {
            int j = 0;
            while (j < n) {
                if (scs[i] == pcs[j]) {
                    i++;
                    j++;
                } else {
                    // 当模式串与主串不匹配时,如果**不匹配字符**对应模式串下标大于 j > 0 (非首个模式串字符),
                    // 并且此字符前一个字符对应字符串部分匹配表中的值 next[j - 1] 也大于 0,
                    // j - next[j - 1] 即模式串为后移的位数,等价于 j 置为 next[j - 1]
                    if (j > 0 && next[j - 1] > 0) {
                        j = next[j - 1];
                    } else {
                        break;
                    }
                }
            }
            if (j == n) {
                return i - n;
            }
        }

        return -1;
    }

    private static int[] getNexts(String p) {
        int m = p.length();
        char[] b = p.toCharArray();
        int[] next = new int[m];

        next[0] = 0;
        int k = 0; // 表示前后缀相匹配的最大长度

        for (int i = 1; i < m; ++i) {
            // k 为 b[0, i-1] 子串最大匹配前、后缀长度
            // b[0, k] 为 b[0, i-1] 子串最大匹配前缀子串
            while (k != 0 && b[k] != b[i]) {
                // 若:b[k] != b[i],则求 b[0, i] 子串最大匹配前、后缀长度问题转换成了求 b[0, k] 子串最大匹配前、后缀长度问题
                k = next[k];
            }
            if (b[k] == b[i]) {
                // 若:b[k] == b[i],则 b[0, i] 子串最大匹配前、后缀长度为 k + 1
                ++k;
            }
            next[i] = k;
        }
        return next;
    }
}

参考 The Knuth-Morris-Pratt Algorithm in my own words