美文网首页
字符串匹配: KMP算法

字符串匹配: KMP算法

作者: Shadow0x70 | 来源:发表于2018-06-12 09:31 被阅读0次

字符串匹配: KMP算法


学习于 从头到尾彻底理解KMP
结合自己的理解, 本文致力于从简介绍

先给出模板代码
void KMP(char *s, char *t, int *p);
在文本串 s 中寻找模板串 t 的匹配, 需要长度至少为strlen(t)+1的辅助空间
串 s 和 t 的下标都是从0开始 (从1开始见后)

void KMP(char *s, char *t, int *f)
{
    int lens = strlen(s), lent = strlen(t);

    f[1] = 0;
    for(int i = 1, j = 0; i < lent; f[++i]=j) {
        while(j && t[j] != t[i]) j = f[j];
        if(t[i] == t[j]) j++;
    }

    for(int i = 0, j = 0; i < lens; i++) {
        while(j && s[i] != t[j]) j = f[j];
        if(s[i] == t[j]) j++;
        if(j == lent) { /*匹配成功*/ j = f[j];}
    }
}

问题原型: 字符串匹配

给定文本串 s 和模板串 t
在 s 中查找 t 出现了多少次或者出现的位置
比如在文章中查找一个单词

暴力方案

最容易想到的就是暴力匹配, 从 s 串的每一个位置开始匹配 t 串

void str_match(char *s, char *t)
{
    int lens = strlen(s), lent = strlen(t);
    for(int i = 0; i < lens; i++)
    for(int j = 0; j < lent && i+j < lens; j++) {
        if(s[i+j] != t[j]) break;
        if(j+1 == lent) /*匹配成功*/;
    }
}

暴力匹配的时间复杂度是 O(lens*lent)
Si 表示 s 串上的匹配位置, Tj 表示 t 串上的匹配位置
发现, 当前位置匹配成功时, Si++, Tj++
而匹配失败时, Tj 回到0, 同时 Si 也会"回溯", 退回到 i+1, 重新开始匹配
因此时间复杂度为O(lens*lent)

而KMP算法, 则利用已经匹配的信息, 实现了 Si 不回溯, 优化了性能

KMP算法

在暴力方法中, 对于两个串:
0 1 2 3 4 5 6 7 8 9
a b a b c a b a b d
a b a b d
匹配到 Si = Tj = 4 时匹配失败, 然后 Si=1, Tj=0
相当于将串 t 右移一位然后从头开始匹配
0 1 2 3 4 5 6 7 8 9
a b a b c a b a b d
_ a b a b d
然而实际上我们知道这时必然会匹配失败

流程模拟

KMP算法的做法是直接移动到:
0 1 2 3 4 5 6 7 8 9
a b a b c a b a b d
_ _ a b a b d
并且 Si 不改变, Tj=2, 继续和 Si=4 匹配, 再次失败, 则:
0 1 2 3 4 5 6 7 8 9
a b a b c a b a b d
_ _ _ _ a b a b d
Tj=0, 而如果这时再次失败, 则 Si++

原理理解

当第一次匹配失败时, 完全不需要将 t 只右移一位, 这由 t 自身决定:
a b a b d 如果在t[4]匹配失败, 说明与之匹配的s的前四位已知, 是 a b a b
右移一位会使ba匹配, 所以必定会失败

而右移两位同样是由 t 自身决定:
a b a b d 如果在t[4]匹配失败, 说明与之匹配的s的前四位已知, 是 a b a b
串 s 在 Si 之前的这个长度为4的子串的后缀与
串 t 在 Tj 之前的这个长度为4的子串的前缀
有长度为2的相同部分a b

所以这时, 无需重新匹配已经匹配过的部分, 可以直接右移两位
核心便在于, 已经匹配成功的子串的相同的前缀和后缀的长度 f
所以这个辅助空间f数组, f[i]表示串 t [0, i-1]子串的相同前缀后缀长度
知道了这个长度, 就是匹配失败时右移的长度, 就可以直接"跳跃式"移动

可以写出伪代码:

KMP(s, t, f)
{
    for(i = 0, j = 0; i < lens; i++)
    {
        while(j && s[i] != t[j]) j = f[j];
        if(s[i] == t[j]) j++;
        if(j == lent) {/*匹配成功*/ j = f[j];}
    }
}

每一次for循环结束 Si++, 或是 SiTj 匹配成功, 或是 SiTj=0 匹配失败

  • 第一个while是不断失配时, 串 t 沿着 f 数组右移, 直到 Tj=0
  • 之后判断能否匹配, 如果能匹配, 则应有 Si++, Tj++
  • 如果不能匹配, 则此时 Tj 一定为 0, 此时失配, 仍然应该 Si++
  • 如果 Tj 到了末尾, 说明匹配成功, 并主动右移一次 (避免下一次while中t[j]越界)

再举个例子

s: a b c a b c d e f g a b c a b c a b c a b c a b c
t: a b c a b c a b
f: 0 0 0 0 1 2 3 4 5
Si=6, Tj=6 时匹配失败
已经成功匹配的长度为6的子串a b c a b c的 f 值为3
便有 Tj=3, 此时 Si 不变, 相当于串 t 右移三位(6-3=3):
a b c a b c d e f g a b c a b c a b c a b c a b c
_ _ _ a b c a b c a b
这时再次失配, 而这时成功匹配的长度为3的子串a b c的 f 值为0
便有 Tj=0, 此时 Si 不变, 相当于串 t 右移三位(3-0=3):
a b c a b c d e f g a b c a b c a b c a b c a b c
_ _ _ _ _ _ a b c a b c a b
Tj=0 时匹配失败, 则 Si++, 直到
a b c a b c d e f g a b c a b c a b c a b c a b c
_ _ _ _ _ _ _ _ _ _ a b c a b c a b
(SiTj匹配成功则同时加一)
这时在 Si=17, Tj=7 第一次匹配成功
但是对串 s 的匹配还没有结束, 这时仍然沿着 f 移动
相当于在 Tj=8 处匹配失败, 已经匹配成功的为 t, 而 t 相同的前缀和后缀长度为5
所以有 Tj=5, 相当于右移三位(8-5=3):
a b c a b c d e f g a b c a b c a b c a b c a b c
_ _ _ _ _ _ _ _ _ _ _ _ _ a b c a b c a b
这时会从 Si=17, Tj=5 匹配到 Si=20 再次成功
...... ......

算法实现

预处理

首先应该对于串 t 预处理出来 f 数组
f[i]表示串 t [0, i-1]子串的最长相同前缀后缀的长度, 所以它有效的范围是[1, strlen(t)]
并且注意前缀后缀不能是自身, 故 f[i] <= i-2

f[0]没有意义, 设为0即可, 初始化f[1] = 0
之后求出 f 数组的过程, 相当于串 t 自己与自己匹配:
尝试用已经求出 f 值的部分来匹配正在求的 f 值

你有可能会想到递推, 对于 f[i], 可以根据 f[i-1] 和 t[i-1] == t[f[i-1]] 来得出
反例: a a b a a a

因此代码写起来和匹配串 s 很像, 伪代码:

KMP_pre(t, f)
{
    f[1] = 0;
    for(i=1, j=0; i < lent; i++)
    {
        while(j && t[i] != t[j]) j = f[j];
        if(t[i] == t[j]) j++;
        f[i+1] = j;
    }
}

伪代码里, 第 i 次循环求的是 [0, i] 的最长相同前缀后缀, 所以赋值给 f[i+1]
这里, i 就相当于 Si, j 便是 Tj
a b c a b c a b
当有 f[1] = 0 时, 这时我们在求 f[2], 表示 a b 的最长相同前缀后缀长度
Si=1, Tj=0, 相当于这样的比较:
a b c a b c a b
_ a
"自己与自己匹配", 显然这一次会匹配失败, 因此 f[2] = 0, Tj 也就不会增加
a b c a b c a b
_ _ a b
Tj不变, Si++, 相当于模板串右移一位, 只不过这一次模板串会逐增而已
a b c a b c a b
_ _ _ a b c

匹配

看过前面的原理理解, 应该已经明白了
并且想要顺利看懂算法实现-预处理, 也要求应该先明白匹配过程
或许, 这是这篇文章思路不合理的地方吧

模板代码

void KMP(char *s, char *t, int *f)
{
    int lens = strlen(s), lent = strlen(t);

    f[1] = 0;
    for(int i = 1, j = 0; i < lent; f[++i]=j) {
        while(j && t[j] != t[i]) j = f[j];
        if(t[i] == t[j]) j++;
    }

    for(int i = 0, j = 0; i < lens; i++) {
        while(j && s[i] != t[j]) j = f[j];
        if(s[i] == t[j]) j++;
        if(j == lent) { /*匹配成功*/ j = f[j];}
    }
}

如果你习惯从 1 开始字符串下标的话, 明白算法原理之后, 就很容易改得出来这些细节了:

void KMP(char *s, char *t, int *f)
{
    int lens = strlen(s+1), lent = strlen(t+1);

    f[1] = 1;
    for(int i = 2, j = 1; i <= lent; f[++i]=j) {
        while(j > 1 && t[j] != t[i]) j = f[j];
        if(t[i] == t[j]) j++;
    }

    for(int i = 1, j = 1; i <= lens; i++) {
        while(j > 1 && s[i] != t[j]) j = f[j];
        if(s[i] == t[j]) j++;
        if(j > lent) { /*匹配成功*/ j = f[j];}
    }
}

相关文章

  • KMP算法文章合集

    字符串的查找:朴素查找算法和KMP算法 暴力匹配算法与KMP算法(串的匹配) 字符串查找算法BF和KMP 字符串匹...

  • leetcode字符串匹配算法之KMP算法

    本篇介绍一种高效的字符串匹配算法——KMP算法。 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J....

  • 字符串匹配与KMP算法

    1.朴素字符串匹配算法 2.KMP算法 求前缀函数 实现KMP算法 3.测试代码

  • 常见算法题之字符串

    1、KMP算法 参考:july大神的KMP博客细节不摆,该算法由暴力字符串来匹配,具体是由字符串匹配的暴力写法而来...

  • KMP算法(字符串匹配问题)

    一、是什么? 注意,是KMP算法,不是MMP哈,我没有骂人。KMP算法是用来做字符串匹配的,除了KMP算法分,还有...

  • KMP算法——寻找子串位置

    KMP算法——寻找子串位置 1、KMP算法简介: KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J....

  • KMP算法

    KMP算法是解决字符串匹配问题的有高效算法 代码:

  • KMP字符串匹配算法

    KMP字符串匹配算法 先总结一下之前的几种字符串匹配算法 1 BF算法, 最简单的字符串匹配算法, 可以直接使用s...

  • 09--KMP

    [toc] KMP算法原理 KMP思想 假设字符串abcdefgab和模式串abcdex,进行匹配,当匹配到x位置...

  • KMP算法理解

    文章大纲:1.KMP算法概念2.KMP算法中最核心的next[] 数组是如何生成的3.使用KMP算法 匹配字符串 ...

网友评论

      本文标题:字符串匹配: KMP算法

      本文链接:https://www.haomeiwen.com/subject/rtcleftx.html