KMP算法 - 基于《算法》第四版

基本思想

  1. 规定:匹配字符串 - 模式串(pat), 匹配文本 - 文本(txt)
  2. 基本思想:当出现不匹配时,就能知晓一部分文本的内容(因为在匹配失败之前它们已经和模式相匹配),根据这些已经知晓的内容决定 在出现不匹配时,模式应在处于哪个位置和文本的下一个字符比较 - 即找到已知晓内容和模式的最长公共前缀(利用模式去匹配已知晓的内容)

要点

看《算法》的时候,有点部分真的太简洁了,看的晦涩难懂( 是我太菜了~ /(ㄒoㄒ)/~~) ,关注一下几个要点,要结合书中内容看可能会有新的理解

  1. 指向文本的指针 i 永远不会回退,不会重复扫描文本,只有模式的指针 j 会进行回退。

  2. 有限状态机(DFA)只和 模式 有关,所以只要计算出了一个 模式的有限状态机(DFA),就可以匹配不同的文本。

  3. DFA[txt.charAt(i)][j] = next, 表示 当前状态 j 时,遇到 文本字符 txt.charAt(i) (即 txt.charAt(i) 和 pat.charAt(j)进行比较)后的下一个状态是 next ( 即模式的指针 j 需要回退/前进到 next 处 和 文本字符 txt.charAt(i + 1)进行比较) 。

  4. 在一个状态中,要确定状态会进行何种转移,需要知道 当前状态遇到的字符

  5. 构造DFA:匹配成功时,即txt.charAt(i) == pat.charAt(j),DFA[txt.charAt(i)][j] = j +1; 匹配失败时,模式指针 j 的回退也不是盲目回退, 它会根据 部分已经匹配成功的字符串与模式进行匹配后所处的状态(即书上所说的 X 重启状态) 以及 当前匹配失败的输入 来决定回退到哪个位置在箭头处出现了不匹配,那此时pat 指针 j 应该回退到哪个地方在和 txt 的下一个字符 A 比较呢?
    知道 状态 j 遇到了 字符D 发生了不匹配,意味着pat的前 j 个字符串 (0… j-1)和文本的 (i - j, i - 1)是相匹配的, 就像上述所示,但是我们不用理会 txt.charAt(i-j),因为 i - j处已经不可能出现匹配,所以 部分已经匹配成功的字符串 就为 B A B A (pat[1… j - 1])。
    现我们考虑 B A B A 和 模式 进行匹配会到达什么状态(所到达的状态也是书中所提到的 重启状态 X),这个过程我们也可以看成是找 部分已经匹配成功的字符串模式最长公共前缀 的过程。可以看到 B A B A 和 模式 进行匹配之后到达了 状态 3,即 X =3。则可以知道 DFA[D][5] = DFA[D][3],即在状态 5 遇到 字符 D发生不匹配时应该回退的位置 就是在状态 3 遇到 字符 D 时 应该到达的位置。 这可以对应到书中代码 DFA[C][j] = DFA[C][X]。

  6. 困扰我很久的一个 X 如何进行求得,可以将其看成一个 X[] 数组,记录了模式与 部分匹配成功的字符串(pat[1…j -1])所达到的所有状态(书本P765,图5.3.8很好的表达了此点)。它们之间关系是一个递推关系,X[i+1]为X[j]状态 遇到 pat.charAt(j)时所到达的状态,即 X[j + 1] = DFA[pat.charAt(j)][x[j]],X[0]初始化状态为0。 这也便是书中代码中的 X = DFA[pat.charAt(j)][X]。理解了上述6,7之后,就可以写出构造DFA的过程(当然,我对于上述6,7的说明都是基于你已经看过了《算法》中字符串查找部分哦~)

    int[][] DFA;
    public void generateDFA(String pat){
            int M = pat.length();
            int R = 256; // ASCII字符不会操过256种
            DFA = new int[R][M];
            // 初始化状态0, 在状态0只有遇到了pat.charAt(0)才会向前推进, 遇到其他为0(java默认初始化数组为0)
            DFA[pat.charAt(0)][0] = 1;
    int X = 0;  // 初始化重启状态为0
            for (int j = 1; j < M; j++){ // 构造DFA数组过程
               for (int c = 0; c < R; c++)
              // 状态j遇到字符c不匹配时,把重启状态X遇到字符c到达哪个状态赋值DFA[c][j]
    DFA[c][j] = DFA[c][X];
    DFA[pat.charAt(j)][j] = j + 1; // 匹配成功, 状态向前推进
               X = DFA[pat.charAt(j)][X];  // 部分已经成功匹配字符串中增加了pat.charAt(i), 需要更新重启状态X,即它们的最长重叠字符会发生变化
            }
    }

  7. 我们已经计算出了DFA,下面是利用DFA来搜索文本的算法 - 结合书本P498 图5.3.7理解:

    public int search(String txt) {
    int M = pat.length();
    int N = txt.length();
    int i,j;
    // pat 的初始态为 0 - 模拟有限状态机运行
    for (i = 0,j = 0; i < N && j < M; i++) {
    // 当前是状态 j,遇到字符 txt[i],
    // pat 应该转移到哪个状态?
    j = dp[txt.charAt(i)][j];
    // 如果达到终止态,返回匹配开头的索引
    if (j == M) return i - M;
    }
    // 没到达终止态,匹配失败
    return N;
    }

  8. 结合上面两部分就可以得到具体的KMP算法啦! ( 具体参考书籍上的算法哦

总结

  1. 要使用KMP算法进行匹配,重要的是求出 DFA 数组,而要求出正确的得到DFA数组,格外需要关注(难理解)的是重启状态X和重启状态X的转移,即为每次发生匹配时,模式 和 pat[1…j]的最长公共前缀。只要得到了DFA数组之后,模拟有限状态机运行就可以进行匹配操作了。
  2. 对于长度为M的模式字符串和长度为N的文本,KMP查找算法访问字符串不会超过 N + M个。
  3. 就算书上所提的一样,KMP算法为最坏情况提供的线性级别运行时间保证的一个理论成果,在实际运用中,它比暴力算法的速度优势并不明显

参考资料

[1] https://judes.me/tech/2016/04/10/kmp.html
[2] https://book.douban.com/subject/19952400/discussion/59623403/
[3] https://zhuanlan.zhihu.com/p/83334559

Author: HB
Link: http://www.huangbin.fun/KMP算法-基于《算法》第四版.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.