美文网首页数据结构与算法
算法系列:KMP算法

算法系列:KMP算法

作者: littlefogcat | 来源:发表于2020-08-12 08:36 被阅读0次

算法解决问题:指定原字符串s与模式串p,需要找到ps中第一次出现的位置。也就是Java中的s.indexOf(p)

常规做法,逐一匹配。最优时间复杂度为O(n),最坏时间复杂度为O(n*(n-m))

    public int indexOf(String s, String p) {
        if (p.length() == 0) return 0;
        char[] ss = s.toCharArray();
        char[] pp = p.toCharArray();
        int size = ss.length - pp.length;
        for (int i = 0; i <= size; i++) {
            boolean match = true;
            for (int j = 0; j < pp.length; j++) {
                if (ss[i + j] != pp[j]) {
                    match = false;
                    break;
                }
            }
            if (match) return i;
        }
        return -1;
    }

那么,有没有一种时间复杂度稳定O(n)的算法呢?

KMP算法

KMP算法是在字符串s中找出模式串p,并且时间复杂度为O(n)的算法。

一、前缀和后缀

什么是前缀、后缀?字符串以啥开头,前缀就是啥;以啥结尾,后缀就是啥,但二者都不包括其自身。
abacaba为例:
前缀有aababaabacabacaabacab
后缀有abaabacabaacababacaba

二、pmt表(Partial Match Table)

pmt表是kmp算法的核心。
什么是pmt表?
pmt表用一个数组表示,数组的长度等于模式串p的长度。其中pmt[i]代表p[0 : i]中,前缀与后缀重叠的最大长度。

举例,p = "abcabxabcab"
i = 4时:p[0 : i]"abcab",其前缀与后缀最大重叠为"ab",故pmt[4] = 2
i = 5时:p[0 : i]"abcabx",其前缀与后缀最大重叠为"",故pmt[5] = 0

以此类推,对于p = "abcabxabcab",pmt表如下:

p a b c a b x a b c a b
pmt 0 0 0 1 2 0 1 2 3 4 5

pmt表的意义

pmt表有什么意义呢?

在常规做法中,即使s[u : u + i - 1]p[0 : i - 1]匹配成功,但是s[i] != p[i],那么就会回溯到s[u + 1]p[0]作比较。
相当于p是一个窗口,每次移动一格,然后从头开始比较,直到某项匹配失败,然后再移动一格,接着从头匹配。

逐一匹配

这么做的问题在于完全舍弃了之前的匹配结果。

在kmp算法中,同样以p为窗口在s中进行匹配。当在第i项失配时,只需要将窗口右移使得前缀与之前的后缀重叠,就可以继续匹配了。而这个移动的距离正是通过pmt表来得到。
这样一来,就不用每次都从头匹配了,因为前面的项是已经进行过匹配的了。

通过pmt移动窗口
通过pmt移动窗口

这个算法的思想理解起来其实挺容易的,但是它看起来很难理解的原因是很难用文字去表述出来。如果有动图或者游标卡尺之类的小道具的话就很好理解了。
当然了,最难的还是发明轮子,而不是造轮子,也不是理解轮子怎么造。

三、使用代码实现

理解了思路之后,代码实现就是顺水推舟了。

首先创建pmt表:

    // 双指针计算pmt数组
    int[] pmt(char[] p) {
        int[] pmt = new int[p.length];
        pmt[0] = 0;
        // 双指针,i在j右边
        for (int i = 1, j = 0; i < p.length; ) {
            if (j == 0 && p[i] != p[j]) {
                pmt[i] = 0;
                i++;
            } else if (p[i] == p[j]) {
                pmt[i] = j + 1;
                i++;
                j++;
            } else { // p[i]!= p[j] && j!= 0
                // 在i、j处失配,但是之前的都是匹配的
                j = pmt[j - 1];
            }
        }
        return pmt;
    }

通过pmt表,我们可以得知失配时应该怎么移动窗口。不用证明可以得到,当p[i]失配时,接下来的匹配项是p[pmt[i - 1]]
然后使用双指针进行匹配:

    int kmp(String s, String p) {
        int[] pmt = pmt(p.toCharArray());

        // s指针
        int i = 0 /* s指针 */, j = 0 /* p指针 */; // 双指针
        while (i < s.length() && j < p.length()) {
            if (s.charAt(i) == p.charAt(j)) {
                i++;
                j++;
            } else if (j == 0) { // p[0] 匹配不上,那么s指针右移一位
                i++;
            } else { // p 右移
                j = pmt[j - 1];
            }
        }
        return j == p.length() ? i - j : -1;
    }

难点
这里有一个难点,在求pmt数组的过程中,有这么一句:

            } else { // p[i]!= p[j] && j!= 0
                j = pmt[j - 1];
            }

即,当p[i]p[j]不匹配,并且j != 0(即:i - 1 与 j - 1 及之前的项是匹配成功的!)时,j = pmt[j - 1]。为什么要这么赋值?
如图,当i、j失配时,很明显,需要把j指针移动到j'处继续匹配。

失配

对于j指针来说,这里的'c'匹配失败,所以需要把j往左移动。但是应该移动到什么位置呢?如之前第二节中所言,那不就应该移动到前缀与后缀重叠的位置吗!

移动 j 指针,使前后缀重叠

注意到pmt数组的意义正是前后缀重叠的最大长度,也就是说j' = pmt[j - 1]

网上说kmp算法很难理解,的确如此。而它的核心难点就是求pmt表的这段代码。只要理解了这点,整个kmp算法也就迎刃而解了。其实整个kmp算法围绕着两件事进行:前后缀与移动窗口。如果真的理解不了,背下来就行,就像背公式一样,开始的时候不一定理解,到后来学的深入了就会茅塞顿开。

完整代码(这里的next表完全可以直接用pmt表代替)

    int kmp(String s, String p) {
        int[] next = next(p.toCharArray());

        // s指针
        int i = 0 /* s指针 */, j = 0 /* p指针 */; // 双指针
        while (i < s.length() && j < p.length()) {
            if (s.charAt(i) == p.charAt(j)) {
                i++;
                j++;
            } else if (j == 0) { // p[0] 匹配不上,那么s指针右移一位
                i++;
            } else { // p 右移
                j = next[j];
            }
        }
        return j == p.length() ? i - j : -1;
    }

    int[] next(char[] p) {
        int[] next = pmt(p);
        System.out.println("p: " + Arrays.toString(p));
        System.out.println("pmt: " + Arrays.toString(next));
        if (next.length > 0) System.arraycopy(next, 0, next, 1, next.length - 1);
        next[0] = 0;
        return next;
    }

    // 双指针计算pmt数组
    int[] pmt(char[] p) {
        int[] pmt = new int[p.length];
        pmt[0] = 0;
        // 双指针,i在j右边
        for (int i = 1, j = 0; i < p.length; ) {
            if (j == 0 && p[i] != p[j]) {
                pmt[i] = 0;
                i++;
            } else if (p[i] == p[j]) {
                pmt[i] = j + 1;
                i++;
                j++;
            } else { // p[i]!= p[j] && j!= 0
                // 在i、j处失配,但是之前的都是匹配的
                j = pmt[j - 1];
            }
        }
        return pmt;
    }

相关文章

  • Implement strStr()

    标签: C++ 算法 LeetCode 字符串 KMP 每日算法——leetcode系列 问题 Implemen...

  • 对KMP算法的一些理解

    最近学到KMP算法,下面讲讲对KMP算法的一些个人理解,希望对大家有帮助! 对于KMP算法的理解: 整个KMP算法...

  • KMP 专题整理

    KMP 学习记录 kuangbin专题十六——KMP KMP 学习总结 朴素 KMP 算法 拓展 KMP 算法(E...

  • 串的模式匹配算法

    KMP算法 算法匹配

  • KMP算法文章合集

    字符串的查找:朴素查找算法和KMP算法 暴力匹配算法与KMP算法(串的匹配) 字符串查找算法BF和KMP 字符串匹...

  • 字符串匹配

    BF 算法和 RK 算法BM 算法和 KMP 算法

  • KMP算法——寻找子串位置

    KMP算法——寻找子串位置 1、KMP算法简介: KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J....

  • 字符串匹配 - KMP算法

    前面我们介绍非常高效的 BM 算法,今天我们介绍另一个非常出名且高效的 KMP 算法。 KMP 算法思想 KMP ...

  • KMP 算法

    KMP 算法 1. 暴力匹配算法 在分析KMP算法前, 先看看暴力匹配算法是如何工作的.暴力匹配算法的基本思想是:...

  • 字符串匹配

    朴素算法=BF算法=暴力算法 KMP算法 去除朴素算法中 配对不上的 和 已配对过的 比较过程 什么是KMP中的n...

网友评论

    本文标题:算法系列:KMP算法

    本文链接:https://www.haomeiwen.com/subject/ingydktx.html