1769 字
6 分钟
KMP

问题导入#

给定一个文本串 SS 和一个模式串 PP,在 SS 中找到所有 PP 出现过的位置。

暴力匹配算法#

想当然地考虑,我们可以从 SS 的第一个字符开始,逐个与 PP 的字符比较,如果相同,则继续比较下一个字符,否则从 SS 的下一个字符开始重新比较。

实现如下:

vector<int> match(string S, string P) {
    int n = S.size(), m = P.size(), i = 0, j = 0;
    vector<int> ans;
    while (i < n) {
        if (S[i] == P[j]) {
            i++;
            j++;
            if (j == m) {
                ans.push_back(i - m);
                i -= j - 1;
                j = 0;
            }
        } else {
            i -= j - 1;
            j = 0;
        }
    }
    return ans;
}

易得,暴力匹配算法的时间复杂度为 O(mn)O(mn)

KMP 算法#

首先,我们注意到,在暴力匹配算法中,在最坏情况下,每次匹配失败后,我们都要回退到 ij+1i-j+1 这个位置来重新匹配。 而此时我们实际上已经知道了 S[ij+1i]S[i-j+1\dots i] 的所有信息,我们完全没有必要重新获取一遍这些信息。 换句话说,我们能不能做到不让 ii 指针左移?

举例:对于主串 S=abababcS=\text{abababc} 和模式串 T=ababcT=\text{ababc},当我们比较 S[4]T[4]S[4]\neq T[4] 时,对于暴力匹配算法,我们接下来会比较 S[1]S[1]T[0]T[0]。 而事实上,我们已经知道了 S[03]S[0\dots 3]TT 的全部内容,经过观察,我们发现只需要继续比较 S[4]S[4]T[2]T[2] 即可,因为 S[2,3]S[2,3]T[0,1]T[0,1] 是完全匹配的。 那我们要怎么让计算机知道这件事呢?

先假设我们有一个 神奇数组 magic,它可以告诉我们,如果我们匹配失败了,根据已知信息,我们可以跳过多少字符,直接开始下一个字符的比较。 用更书面化的语言来说,这个神奇数组告诉了我们在当前匹配失败后,模式串指针可以跳转到哪里,保证前面的字符都是匹配的。

此时借助这个神奇数组和一些想当然,就可以得出以下算法:

vector<int> kmp(string S, string P) {
    vector<int> ans;
    vector<int> magic;
    int n = S.size(), m = P.size(), i = 0, j = 0;
    while (i < n) {
        if (S[i] == P[j]) {
            i++;
            j++;
            if (j == m) {
                ans.push_back(i - m);
                j = magic[j];
                // 如果我们对模式串匹配成功了,可以假设是在模式串末尾加一个与任意字符都匹配失败的字符,对应的神奇数组跳转到的位置保证前面的字符都是匹配的。
            }
        } else {
            if (j > 0)
                j = magic[j];
            else
                i++;
                // 如果我们第一个字符就匹配失败了,那就只能从主串中下一个位置继续开始了
        }
    }
    return ans;
}

由于主串指针的单调性,KMP 算法的时间复杂度是 O(n)O(n) 的。

显然我们不能只靠假设就得出来神奇数组,做事情是要有根据的。

前缀函数#

在学习怎么求神奇数组前,我们需要一点前置知识:前缀函数

给定一个长度为 NN 的字符串 SS,其前缀函数定义为一个长度为 NN 的数组 π\pi,其中 π[i]\pi[i] 被定义为子串 S[0i]S[0\dots i] 的最长的相等的真前缀与真后缀的长度。详尽具体的定义见前缀函数

这里直接给出最优算法,详细推导证明省略。我们需要知道,这个算法可以在 O(N)O(N) 的时间求得一个字符串的前缀函数。

vector<int> prefix_function(string s){
int n=s.size();
vector<int> pi(n);
for(int i=1;i<n;i++){
int j=pi[i-1];
while(j>0&&s[i]!=s[j])j=pi[j-1];
if(s[i]==s[j])j++;
pi[i]=j;
}
return pi;
}

注意到,如果一个字符串的前缀 S[0k1]S[0\dots k-1] 和后缀 S[NkN1]S[N-k \dots N-1] 完全相同,在主串和模式串的匹配中,就可以借此性质来实现神奇数组的功能。 对于模式串指针 jj,当我们在匹配失败时,若有 P[0k1]P[0\dots k-1]P[jkj1]P[j-k\dots j-1] 完全相同,那就可以跳过这一部分,直接在当前位置继续匹配 P[k]P[k]

失配函数#

在 KMP 算法中,我们把所谓的神奇数组叫做失配函数,通常指的是 KMP 匹配失败时,模式串应该跳转到哪里,常写成 next[i]fail[i] 等形式。

从数值上来说,显然,我们有 fail[i]=π[i1]fail[i]=\pi[i-1]

举例:对于模式串 P=ababacaP=\text{ababaca},它的前缀函数为 πP={0,0,1,2,3,0,1}\pi_{P}=\{0,0,1,2,3,0,1\},它的失配函数为 failP={1,0,0,1,2,3,0,1}fail_{P}=\{-1,0,0,1,2,3,0,1\}

我们令 fail[0]=1fail[0]=-1 表示对于主串当前位置的匹配彻底失败,应该从下一个位置再重新开始匹配。也可以想象成在模式串前面加一个与任意字符都匹配的字符。 而 fail[n]fail[n] 则表示的是我们匹配成功后可以直接跳转到 fail[n]fail[n] 处开始新的一轮匹配,而不一定要从第一位开始。

失配函数的求法可以先求出前缀函数再移项得到,也可以想象成模式串自己与自己匹配。类比 KMP 算法代码可以得出:

vector<int> failure_function(string s){
int n=s.size();
vector<int> fail(n+1);
fail[0]=-1;
int i=0,j=-1;
while(i<n){
if(j==-1||s[i]==s[j]){
i++;
j++;
fail[i]=j;
}else j=fail[j];
}
return fail;
}

易得,求解失配函数的时间复杂度是 O(n)O(n) 的。

算法模板#

结合以上内容,我们可以得到一份 KMP 算法模板,综合来看,时间复杂度是 O(m+n)O(m+n) 的。

vector<int> failure_function(string s){
int n=s.size();
vector<int> fail(n+1);
fail[0]=-1;
int i=0,j=-1;
while(i<n){
if(j==-1||s[i]==s[j]){
i++;
j++;
fail[i]=j;
}else j=fail[j];
}
return fail;
}
vector<int> kmp(string S, string P) {
    vector<int> ans;
    vector<int> fail=failure_function(P);
    int n = S.size(), m = P.size(), i = 0, j = 0;
    while (i < n) {
        if (S[i] == P[j]) {
            i++;
            j++;
            if (j == m) {
                ans.push_back(i - m);
                j = fail[j];
            }
        } else {
            if (j > 0)
                j = fail[j];
            else
                i++;
        }
    }
    return ans;
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

KMP
https://leaf146.cn/posts/KMP
作者
LeAf146
发布于
2026-07-31
许可协议
MIT

部分信息可能已经过时