默认所有字符串的下标从 \(1\) 开始。

\(\text{KMP(Knuth-Morris-Pratt)}\) 算法,能够在 \(O(|s| + |p|)\) 的时间复杂度内求出模式串 \(p\) 在文本串 \(s\) 中的出现次数、位置等信息。

众所周知,暴力地进行匹配(也就是对 \(s\) 的每一位都进行与 \(p\) 的比对)的最坏时间复杂度是 \(O(|s| \times |p|)\),那么 \(\text{KMP}\) 算法是如何将时间复杂度优化这么多的呢?

从暴力的过程入手:不难发现,在匹配的过程中,有很多重复的比对。举个例子,令 $s = $ abab,$p = $ ab,当我们匹配到 \(s[2]\) 时,明明已经知道它和 \(p[2]\) 相等,而 \(p[2] \ne p[1]\),却还是要在匹配一次。这样的例子数不胜数,放到较长的 \(s\)\(p\) 中,浪费的资源会更多。而 \(\text{KMP}\) 算法就解决了这个问题。

先引入一个 \(nxt[]\) 数组,令 \(suf(i)\) 表示 \(p[1 \dots i]\),即 \(p\) 以第 \(i\) 个字符为结尾的前缀,\(nxt[i]\) 表示 \(suf(i)\) 的每个前缀和后缀的最长公共元素长度,特别地,\(nxt[1] = 0\)

真前缀:不等于原字符串的前缀。

真后缀:不等于原字符串的后缀。

再举例:对于 $p = $ abcabcd

\(nxt[1] = 0\)

\(nxt[2] = 0\)

\(nxt[3] = 0\)

\(nxt[4] = 1,(p[1] = p[4])\)

\(nxt[5] = 1,(p[1 \dots 2] = p[4 \dots 5])\)

\(nxt[6] = 3,(p[1 \dots 3] = p[4 \dots 6])\)

有了 \(nxt[]\),在失配之后就可以整些高大上的操作了。

再再举例: $s = $ abcabcacd,$p = $ abcabcd 时(配对成功往后跳即可,这里只列举失配的情况)

\(i = 7, k = 6\) 时,\(s[i] \ne p[k + 1]\),让 \(k = nxt[k] = 3\),此时 \(s[i] = s[k + 1]\),继续匹配。

可以发现,\(nxt[]\) 帮助我们跳过了 \(s[3 \dots 5]\) 的再次匹配的过程,进而将匹配的时间复杂度优化到了 \(O(|s|)\)

清楚了这个逻辑,不难写出匹配的代码:

int KMP(char *s, char *p) { //求解出现次数
    int res = 0, k = 0, slen = strlen(s + 1), plen = strlen(p + 1);
    for (int i = 1; i <= slen; i++) {
        while (k && s[i] != p[k + 1]) k = nxt[k]; // k==0时就不必再跳了,不然会死循环
        if (s[i] == p[k + 1]) k++;
        if (k == plen) res++;
    }
    return res;
}

接下来,难点来到了如何求解 \(nxt[]\)

这里给出递推求 \(nxt[]\) 的方法;

还是就着上面的例子来解释:\(p =\) abcabcd

\(nxt[1] = 0\)

\(i = 2, k = 0, p[i] \ne p[k + 1] \Rightarrow nxt[2] = k = 0\)

\(i = 3, k = 0, p[i] \ne p[k + 1] \Rightarrow nxt[3] = k = 0\)

\(i = 4, k = 0, p[i] = p[k + 1] \Rightarrow nxt[4] = k = 1\)

\(i = 5, k = 1, p[i] = p[k + 1] \Rightarrow nxt[5] = k = 2\)

\(i = 6, k = 2, p[i] = p[k + 1] \Rightarrow nxt[6] = k = 3\)

\(i = 7, k = 3, p[i] \ne p[k + 1] \Rightarrow k = nxt[k] = 0, p[i] \ne p[k + 1] \Rightarrow nxt[7] = k = 0\)

不难发现,\(nxt[]\) 的求解过程,其实就是 \(p\)\(p\)\(\text{KMP}\)

求解代码如下:

void getnxt(char *p) [ //求解nxt[]
    nxt[1] = 0;
    int plen = strlen(p + 1), k = 0;
    for (int i = 2; i <= plen; i++) {
        while (k && p[i] != p[k + 1]) k = nxt[k];
        if (p[i] == p[k + 1]) k++;
        nxt[i] = k;
    }
]

至此,我们便可以在 \(O(|s| + |p|)\) 的时间复杂度内进行字符串匹配了~

原文地址:http://www.cnblogs.com/chy12321/p/16885352.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性