字符串匹配: KMP算法

字符串匹配: 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];}
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • KMP算法目的:尽快解决字符串匹配问题,时间复杂度为O(m+n),而常规的简单匹配算法时间复杂度:O(m*n) 这...
    安静1337阅读 973评论 0 51
  • 字符串匹配(查找)算法是一类重要的字符串算法(String Algorithm)。有两个字符串, 长度为m的hay...
    曾会玩阅读 593评论 0 2
  • 许多人知道叶芝,源于他的一首诗《当你老了》。 当你老了,头发花白,睡意沉沉, 倦坐在炉边,取下这本书来, 慢慢读着...
    布谷鸟唐山阅读 214评论 0 3
  • "我家儿子是一遇到背书就哭一场" "孩子早上读英语抵触情况又重复,昨天听写错的单词,罚他写10遍,他抄了,但今天说...
    夏天里的雪02阅读 943评论 1 7
  • 多久了 我都没变 爱你这回事 整整六年 你最好 做好准备 我没有打算 停止一切 下班回来,小区楼下的发廊里兀自播放...
    枳淮南阅读 298评论 0 4