分享

KMP算法详解

 数据结构和算法 2023-06-10 发布于上海

KMP 算法主要用于字符串匹配的,他的时间复杂度 O(m+n) 。常规的字符串匹配我们一般都这样做,使用两个指针,一个指向主串,一个指向模式串,如果他俩指向的字符一样,他俩同时往右移一步,如果他俩指向的字符不一样,模式串的指针重新指向他的第一个字符,主串的指针指向他上次开始匹配的下一个字符,如下图所示。

我们看到当模式串和主串匹配失败的时候,模式串会从头开始重新匹配,主串也会回退,但我们知道失败字符之前的子串都是匹配成功的,那么这个信息能不能被我们利用呢?当然可以,这个就是我们这里要讲的 KMP 算法,他就是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。当使用 KMP 算法的时候,如果某个字符匹配失败,主串指针不回退,而模式串指针不一定回到他的第一个字符,如下图所示。

当字符 ed 匹配失败之后,主串指针不回退,模式串指针指向字符 c ,然后在继续判断。主串指针不回退好操作,但模式串指针怎么确定是回到字符 c ,而不是其他位置呢?这是因为在字符 e 匹配失败后,字符 e 前面有 2 个字符 ab 和最开始的 2 个字符 ab 相同,所以我们可以跳过前 2 个字符。也就是说模式串匹配某一个字符失败的时候,如果这个失败的字符前面有 m 个字符和最开始的 m 个字符相同,那么下次比较的时候就可以跳过模式串的前 m 个字符,如下图所示。通过上面的图我们可以知道,当某个字符匹配失败的时候,他前面的肯定都是成功的,也就是说字符串 s1 和字符串 s2 完全一样。在模式串中,匹配失败的字符前面 b4b3 是相同的,所以我们可以得到 b1,b2,b3,b4 都是相同的,也就是说b2b3 也是相同的,既然是相同的,跳过即可,不需要在比较了,直接从他们的下一个字符开始匹配。

KMP 算法的核心部分就是找出模式串中每个字符前面到底有多少字符和最开始的字符相同,我们用 next 数组表示。有的描述成真前缀和真后缀的相同的数量。这里要注意当前字符前面的字符不包含第 1 个字符,最开始的字符也不能包含当前字符,比如在模式串 abc 中不能说字符 c 前面有 ab 和最开始的 ab 相同,来看下表。

下标
0
1
2
3
4
5
6
模式串
a
b
c
a
b
e
a
next[i]
-1
0
0
0
1
2
0

我们看到字符 e 前面有 ab 和最开始的字符 ab 相同,长度是 2 。在看第 2a 前面没有(不包含自己)和最开始字符有相同的,所以是 0 。在任何模式串的第 2 个字符前面都没有和最开始字符有相同的,因为前面的是不包含第一个字符,所以 next[1]=0 。如果第 2 个没有,那么第 1 个就更没有了,为了区分,可以让 next[0]=-1 ,当然等于 0 也可以,需要特殊处理下即可。我们先来看下 next 数组怎么求。

使用两个指针 ij ,其中 j 是字符 p[i] 前面与最开始字符相同的数量,也是 next[i] 。如果 p[j]==p[i],也就是说字符 p[i+1] 前面有 j+1 个字符和最开始的 j+1 个字符相同,所以可以得到 next[i+1]=j+1 ,这个很容易理解。如果 p[j]!=p[i],我们让指针 j 等于 next[j] ,然后重新和 p[i] 比较,这就是回退,这个回退也是 KMP 算法的最核心部分,我们来分析下为什么要这样回退。如上图所示,如果 nxet[j] 大于 0 ,那么 j 前面的 b2b1 是相同的,我们知道 S1S2 也是相同的,所以我们可以得出 b1,b2,b3,b4 都是相同的,如果 p[i]!=p[j] ,只需要让 j 回退到 next[j] ,然后 ij 在重新比较,这个 next[j] 就是 b1 的长度,因为 b1b4 是相同的,所以不需要再比较了。如果还不相同, j 继续回退,直到回退到 -1 为止,我们拿个真实的字符串看下,如下图所示。我们看到 b1,b2,b3,b4 是相同的,他们都是字符串 "abc" ,如果 p[i]!=p[j] ,我们就让 j 回退到 next[j] ,因为他们前面 b1b4 是相同的,没必须要在比较了,最后再来看下代码。

public int strStr(String s, String p) {
    int i = 0;// 主串S的下标。
    int j = 0;// 模式串P的下标。
    int[] next = new int[p.length()];
    getNext(p, next);// 计算next数组。
    while (i < s.length() && j < p.length()) {
        // 如果j为-1或者p[i]==p[j],i和j都往后移一步。当j为-1时,
        // 说明p[i]!=p[0],然后i往后移一步,j也往后移一步指向p[0]。
        if (j == -1 || s.charAt(i) == p.charAt(j)) {
            i++;
            j++;
            if (j == p.length())// 匹配成功。
                return i - j;
        } else {
            // 匹配失败,j回退,跳过模式串P前面相同的字符继续比较。
            j = next[j];
        }
    }
    return -1;
}

private void getNext(String p, int next[]) {
    int length = p.length();
    int i = 0;
    int j = -1;
    next[0] = -1;// 默认值。
    // 最后一个字符不需要比较。
    while (i < length - 1) {
        // 如果j为-1或者p[i]==p[j],i和j都往后移一步。
        if (j == -1 || p.charAt(i) == p.charAt(j)) {
            i++;
            j++;
            // j是字符p[i]前面和最开始字符相同的数量。
            next[i] = j;
        } else {
            j = next[j];// 回退,KMP的核心代码。
        }
    }
}

KMP优化

我们来看下表 ,当模式串在下标为 4 的位置匹配失败的时候,下一步 j 会回退到下标为 1 的位置,但这两个位置的字符是一样的,既然一样,拿过来比较也是不会成功,所以如果字符一样,我们可以优化一下,往前查找相同的子串。

下标
0
1
2
3
4
5
6
模式串
a
b
c
a
b
e
a
next[i]
-1
0
0
0
1
2
0

看下代码:

private void getNext(String p, int next[]) {
    int length = p.length();
    int i = 0;
    int j = -1;
    next[0] = -1;// 默认值。
    // 最后一个字符不需要比较。
    while (i < length - 1) {
        // 如果j为-1或者p[i]==p[j],i和j都往后移一步。
        if (j == -1 || p.charAt(i) == p.charAt(j)) {
            i++;
            j++;
            // 这里要注意,i和j都已经执行了自增操作。
            if (p.charAt(i) == p.charAt(j))
                next[i] = next[j];
            else
                next[i] = j;
        } else {
            j = next[j];// 回退,KMP的核心代码。
        }
    }
}

如果前面查找的还是相同的,我们该怎么办呢?是不是需要写个递归,继续往前找?实际上是不需要的,比如模式串 "aaaaaaaaaab" ,如果没优化他是这样的。

如果优化之后,当最后一个 a 匹配不成功的时候,他会回退到前面一个 a ,实际上前一个 a 在计算的时候已经回退过了,就是 -1 ,所以不需要递归,直接赋值就行。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多