KMP算法介绍-java实现

字符串匹配算法——KMP算法

本部分内容转自博客1博客2

字符串匹配是什么?

  • 判断一个主字符串S中是否包含模式字符串P,并返回P在S中出现的位置。举例来说,有一个字符串”BBC ABCDAB ABCDABCDABDE”,想知道里面是否包含另一个字符串”ABCDABD”,以及开始位置。
  • Knuth-Morris-Pratt算法1(简称KMP)是最常用模式匹配算法之一。

字符串匹配算法

最简单的模式匹配方法,就是暴力匹配,遍历S中的每个字符,以该字符为起点,开始与P做比较,如果相同,就继续比较S和P的下一个字符,全部匹配就输出;如果不完全匹配,就返回S的下一个字符开始,继续和P起始字符作比较,时间复杂度为O(N*M)。N是S的长度,M是P的长度。
过程如下所示:
1. 字符串S=”BBC ABCDAB ABCDABCDABDE”的第一个字符(下标i==0)与搜索词P=”ABCDABD”的第一个字符(下标j==0),进行比较。
图片1
2. 因为B与A不匹配,所以S的下标i向后移一位,P的下标j保持不变:
图片2
3. 直到S有一个字符,与P的第一个字符相同(i==4,j==0):
图片3
4. 接着S的下标i和P的下标j同时加1,如果还是相同,继续加1:
图片4
5. 直到字符串S有一个字符(i==10),与搜索词P的字符(j==6)不相同为止:
图片4
6. 这时,把i归位到第3步(i==4)的后一位i=5,P的下标再归零j=0,重新开始比较。
图片4
* 这样做虽然可行,但是效率很差。

部分匹配值

  • KMP算法的想法是,利用字符串P自身的特点,保持 i 的值不变,改变 j 的值。
  • 如何利用字符串P自身的特点? 从前面的例子可以看出,当(i==10, j==6)时,P前面的6个字符”ABCDAB”和S是匹配的,其中前两个字符“AB”和最后两个字符“AB”是相同的,如果赋值(i=5, j=0),重新开始比较,S的子串(i=5~7,BCD)是肯定和P的子串(j=0~1,AB)不匹配的,做了无用功;如果保持(i==10)不变,改变 j ,那么j的值应该赋值为多少?
  • 那么应该把 j 赋值为多少? 先介绍两个基本概念:前缀和后缀

    • 前缀:一个字符串中,除了最后一个字符外,从前面第一个字符开始的连续组合
    • 后缀:一个字符串中,除了第一个字符外,后面最后一个字符开始的连续组合
    • 部分匹配值”前缀和后缀的最长共有元素的长度
    • 以P=”ABCDABD”为例:
    下标 字符串 前缀 后缀 部分匹配值
    0 A [] [] 0
    1 AB [A] [B] 0
    2 ABC [A, AB] [BC, C] 0
    3 ABCD [A, AB, ABC] [BCD, CD, D] 0
    4 ABCDA [A, AB, ABC, ABCD] [BCDA, CDA, DA, A] 1
    5 ABCDAB [A, AB, ABC, ABCD, ABCDA] [BCDAB, CDAB, DAB, AB, B] 2
    6 ABCDABD [A, AB, ABC, ABCD, ABCDA, ABCDAB] [BCDABD, CDABD, DABD, ABD, BD, D] 0

图片5
* 此时把字符串P向后移动的位数通过公式:

=
来计算。
7. 前面六个字符”ABCDAB”是匹配的,最后一个匹配字符B对应的”部分匹配值”为2,P向后移动4(6-2)位,下标 j 值变为2;
图片6
8. 空格与C不匹配,已匹配的字符数为2(”AB”),对应的”部分匹配值”为0,移动位数 = 2 - 0,j 值变为0:
图片6
9. 因为空格与A不匹配,i 加1为 11,j保持不变,并开始逐位比较,直到C不匹配D:
图片7
10. P向后移动4(6-2)位,j 赋值为2,逐位比较,最终完全匹配:
图片7
11. 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。

Next数组

  • 从前面可以看出,当S[i] != P[j]时,前面已经匹配了P[0,…,j-1],长度为j;假设第 P[j-1]处的部分匹配值为k,那么P要向后移动 j-k 个位置。此时在 i 保持不变的情况下,和S[i]做比较的P的下标是 j ( j k ) = k ,也就是说当S[i] != P[j]时,j的值改变为k。
  • 另外,当j==0且S[i] != P[j]时,此时不是改变j的值,而是i加1,所以把所有部分匹配值向后移动,并把下标0处的值赋值为-1:
    P
  • 定义一个数组Next,长度和字符串长度相同,Next[j]表示的是当S[i] != P[j]时,j应该重新赋的值。如果在下标 j 处的Next[j]值为 k,那么就是意味着如下公式成立:
    P [ 0 , . . . , k 1 ] == P [ j k , . . . , j 1 ]

    那么,当S在下标 i 处与P在 j 处有了不匹配时,保持 i 不变,j 赋值为k。
    因为,当 S [ i ] ! = P [ j ] 且j处的Next[j]值为k时,有
    S [ i j , . . . , i 1 ] == P [ 0 , . . . , j 1 ]

    P [ 0 , . . . , k 1 ] == P [ j k , . . . , j 1 ] 可得:
    S [ i k , . . . , i 1 ] == P [ 0 , . . . , k 1 ]

    这表明,P的[0,…,k-1]和S[i-k,…,i-1]相同,不需要再比较了,直接开始比较P[k]位置的内容,所以j赋值为k。
  • Next[j]处的值k,也就是P[0,…,k-1]和P[j-k,…, j-1]相同的最长子串的长度,这样只需要求解数组Next,就可以知道j所需要重新赋的值。
  • 另一种Next 的定义是:在当前Next数组的基础上,每个值加1。

代码实现-java

/* 暴力破解法 */
public static int subStringButeForce(String S1, String S2) {
    char[] t = S1.toCharArray();
    char[] p = S2.toCharArray();
    int i = 0, j = 0; // 模式串的位置
    while (i < t.length && j < p.length) {
       if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
           i++;
           j++;
       } else {
           i = i - j + 1; // 一旦不匹配,i后退
           j = 0; // j归0
       }
    }
    if ( j == p.length) {
       return i - j;
    }
    return -1;
}
//KMP算法,必须先计算出P的部分匹配值数组,使用函数getNext求解
public static int KMP(String S,String P){
    int[] next = getNext(P);
    int i =0, j=0;
    while(i<S.length() && j <P.length()){
        if(j==-1 || S.charAt(i) == P.charAt(j)){
            i++;
            j++;
        } else {
            //从部分匹配值处的字符开始判断
            j = next[j];
        }
    }
    //匹配成功
    if(j == LP){
        return i-j;
    }
    return -1;
}
// 可以看出,KMP和暴力破解法的最大也是唯一的区别在于,当S[i]和P[j]不匹配时,j的赋值不同,关键在于求解部分匹配值得函数getNext()
//用来生成字符串P的部分匹配值数组next,next[j]表示当第i个下标处的部分匹配值
public static int[] getNext(String P) {
    char[] p = P.toCharArray();
    int[] next = new int[p.length];
    int j = 0, k=-1;
    next[j] = k;
    while (j < p.length - 1) {
       if (k == -1 || p[j] == p[k]) {
           next[++j] = ++k;
       } else {
           k = next[k];
       }
    }
    return next;
}

函数getNext的解读

  • next[j]的值(也就是k)表示,当P[j] != S[i]时,j指针的下一步移动到的位置,根据j和k来求解next[j+1]。

    1. 当j为0时,如果这时候不匹配,怎么办?
      P1

    像上图这种情况,j已经在最左边了,不可能再移动了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。

    1. 如果是当j为1的时候不匹配怎么办?
      P2

    显然,j指针一定是后移到0位置的,next[1]=0,因为它前面也就只有这一个位置了。

    1. P[k] == P[j] 时,有next[j+1] == next[j] + 1,
      这里写图片描述
      可以证明:
      因为在P[j]之前已经有P[0,…, k-1] == p[j-k,…, j-1],如果有P[k] == P[j],可以得到:P[0,…, k-1] ∪ P[k] == p[j-k,…, j-1] ∪ P[j]。即:P[0,…, k] == p[j-k,…, j],即next[j+1] == k + 1 == next[j] + 1。
    2. 如果P[k] != P[j],如下图所示:
      P4

    此时,要改变k的值,然后使用表达式next[++j] = ++k求解:
    P4
    当P[k] != P[j]时,假设next[j-1]==k1,必然满足P[0,…,k1-1]==P[j-k1-1,…,j-2];假设next[k] == k2,满足P[0,…, k2-1]==P[k-k2,…, k-1]。要求解next[j+1]的值k3,必须要满足的条件是:P[k3-2] == P[j-1] == P[k-1] == P[k2-1],也就是k3 == k2+1,所以把k更新为next[k],然后在在下一轮更新中令next[++j] = ++k。

其他

  • 还有一种数组nextval,通过比较next数组和P的值来确定nextval的值:
    1. 如果P[next[j]] == P[j],则 nextval[i] = P[next[j]]对应的nextval值;
    2. 如果P[next[j]] != P[j],则 nextval[i] = next[i]

猜你喜欢

转载自blog.csdn.net/smj19920225/article/details/80171230