[算法系列]-KMP 算法笔记

280 阅读3分钟

这是我参与更文挑战的第10天,活动详情查看:更文挑战

KMP 算法用于串匹配。它通过确保只扫描 目标串 一遍,保证算法的时间复杂度只需要O(N)。

KMP 算法借助存储存储的信息(一般放在dpnext数组),将匹配串一道正确的位置继续匹配。(也属于一种动态规划算法)

So, 关键就在于匹配串来确定next[](确定有限自动机) 而确定next[]的关键则在于匹配串的公共前缀。

在labuladong写的KMP 算法详解中,参考《算法4》,将跳转的过程看作状态机的状态转换,这个思路十分符合计算机的思维,不过如果没学过编译原理,恐怕还有点难理解它使用状态机的表述。

public class KMP {
    private int[][] dp;
    private String pat;

    public KMP(String pat) {
        this.pat = pat;
        int M = pat.length();
        // dp[状态][字符] = 下个状态
        dp = new int[M][256];
        // base case
        dp[0][pat.charAt(0)] = 1;
        // 影子状态 X 初始为 0
        int X = 0;
        // 当前状态 j 从 1 开始
        for (int j = 1; j < M; j++) {
            for (int c = 0; c < 256; c++) {
                if (pat.charAt(j) == c) 
                    dp[j][c] = j + 1;
                else 
                    dp[j][c] = dp[X][c];
            }
            // 更新影子状态
            X = dp[X][pat.charAt(j)];
        }
    }

    public int search(String txt) {...}
}

在平时的使用中,一般就是使用一维数组来表达模式串。 每当发生不匹配的时候,找出不匹配的字符pi,去之前的字串F=p1……pi-1,将模式串后移,将F最先发生前部(FL)与后部(Fr)想重合的位置,及为模式串应该后移的位置。也就是说,每当发生不匹配的时候,j重新指向的位子恰好就是F串中前后想重合自处啊的长度加1,(因为前面的字串一定无法匹配,但是可以确保前面已经匹配过的字符串中,有一定相同的字串时后面匹配可以 复用的)。 所以,我们通常定义一个next[j]数组。j取1~m,m为模式串长度,表示当第j个字符发生不匹配的时候,应当从next[j]处的字符重新开始匹配。

总结一下关键

KMP的关键就在于如何找到 next[]数组,而我上面没有很好的讲清楚求解的过程。 首先确定next[0],然后由于后面求解next[i+1]的时候,我们其实是知道前面的next[i]的值的(代表着我们知道前面的前后缀相等情况),也就是说,我们只需要比对P next[i]的值Pi。如果两者相等就说明next[i+1] = next[i]+1,但如果两者不相等,则知道,P next[i],不存在与前后想的过的匹配串中。我们可以求前后匹配串的过程,也看做模式匹配问题,我们的目的就在于求前后共同的匹配串,这个时候我们找P next[next[j]],他在后面的匹配中也拥有相同的匹配。所以这样循环反复下去,不断缩小。 下面谈一下,KMP的改进,其实就是利用上了匹配失败的那一次。若是匹配失败的那次与接下来要匹配的P【next[i]】相同,那就说明你其实可以多走一步?你对目标串中将要匹配的那个字符是不确定的,但是你对匹配失败的模式串的那一步,你是清清楚楚的,也就是说,如果你确定P next[i] 等于Pi,那就说你直接匹配也是失败的,但是如果你匹配失败了,那你就需要匹配P next[next[i]] 所以你其实可以提前给他走好,知道到达了串顶或者遇到了匹配成功的那一个字符位子。