KMP 算法

Knuth-Morris-Pratt(KMP) 算法用于搜索给定字符串中的模式。

  • 首先,它在模式中找到称为LPS的重复子串,并将LPS信息存储在数组中。
  • 其次,进行字符串匹配。当发生不匹配时,它利用 LPS 数组来决定从哪里开始下一个匹配,以避免多余的旧比较。

如下图

  • 第一次匹配失败后,右移到已匹配部分数组的最大 前缀=后缀=a 的后缀起始,重新进行匹配
  • 第二次匹配失败后,右移到已匹配部分数组的最大 前缀=后缀=aba 的后缀起始,重新进行匹配

lps 数组的定义

lps[i] 是模式 pattern,0 ~ i 的子串,前缀 = 后缀 的最大长度

lps[i] = the longest proper prefix of pat[0..i] which is also a suffix of pat[0..i]. 

  • 如 abaabac,lps[0] = 0 (a),lps[1] = 0 (ab),lps[2] = 1(aba), lps[3] = 1 (abaa), lps[4] = 2 (abaab), lps[5] = 3 (abaaba), lps[6] = 0 (abaabac)

前缀后缀不能是整个 pattern ,最多到倒数第一个

  • 如 AAAA, lps[0] = 0 (A),lps[1] = 1 (AA),lps[2] = 2 (AAA),lps[2] = 3 (AAAA)

计算 lps 数组

java 代码

public static int[] computeLPS(String pat) {
        int len = 0;
        // i 总是在 len 的前面
        int i = 1;
        int[] lps = new int[pat.length()];
        // lps[0]总是为0
        lps[0] = 0;
        while (i < pat.length()) {
            if (pat.charAt(len) == pat.charAt(i)) {
                len++;
                // 匹配上的长度
                lps[i] = len;
                i++;
            }
            else {
                if (len != 0) {
                    // 不相等,下面的往右拉
                    // 从比 0~len(kmpkmd) 少一个字符的子串(kmpkm) 的最大前后缀 开始比较 (len=lps[kmpkm]=2(km))
                    len = lps[len-1];
                }
                else {
                    // 下面的已经到头了,不能往右拉了。只能上面的 i++ 带来新的可能性
                    i++;
                    // i 加了,lps[i] 必有值
                    lps[i] = 0;
                }
            }
        }
        return lps;
    }

模式匹配

public void kmp(String pat, String txt) {
        int i = 0;
        int j = 0;
        int[] lps = computeLPS(pat);
        List<Integer> res = new ArrayList<>();
        /* // 退出循环条件
         * 后面这样肯定是匹配不上了(长度都不够),所以退出循环
         * kmppkmpkmpkmdkmpkmpk
         *                i
         *              kmpkmdkmpkmpk
         *                j
         */
        while (txt.length()-i >= pat.length()-j) {
            if (txt.charAt(i) == pat.charAt(j)) {
                i++;
                j++;
            }
            
            if (j == pat.length()) {
                // 为啥是 i-j
                /*
                 * kmppkmpkmpkmdkmpkmpk
                 *       i-j          i
                 *        kmpkmdkmpkmpk
                 *                    j 
                 */
                res.add(i-j);
                j = lps[j-1];
            }
            else if (i<txt.length() && txt.charAt(i) != pat.charAt(j)) {
                if (j == 0) {
                    i++;
                }
                else {
                    j = lps[j-1];
                }
            }
        }
        System.out.print(res);
    }

示例

复杂度

暴力匹配: O(n*m)

KMP 算法:O(n+m)

 

28. 找出字符串中第一个匹配项的下标