KMP算法的来龙去脉

时间:2022-12-26 00:33:11

1. 引言

字符串匹配是极为常见的一种模式匹配。简单地说,就是判断主串TT中是否出现该模式串PP,即PP为TT的子串。特别地,定义主串为T[0…n−1]T[0…n−1],模式串为P[0…p−1]P[0…p−1],则主串与模式串的长度各为nn与pp。

暴力匹配

暴力匹配方法的思想非常朴素:

  1. 依次从主串的首字符开始,与模式串逐一进行匹配;
  2. 遇到失配时,则移到主串的第二个字符,将其与模式串首字符比较,逐一进行匹配;
  3. 重复上述步骤,直至能匹配上,或剩下主串的长度不足以进行匹配。

下图给出了暴力匹配的例子,主串T="ababcabcacbab",模式串P="abcac",第一次匹配:

KMP算法的来龙去脉

第二次匹配:
KMP算法的来龙去脉

第三次匹配:
KMP算法的来龙去脉

C代码实现:

int brute_force_match(char *t, char *p) {
int i, j, tem;
int tlen = strlen(t), plen = strlen(p);
for(i = 0, j = 0; i <= tlen - plen; i++, j = 0) {
tem = i;
while(t[tem] == p[j] & j < plen) {
tem++;
j++;
}
// matched
if(j == plen) {
return i;
}
}
// [p] is not a substring of [t]
return -1;
}

时间复杂度i在主串移动次数(外层的for循环)有n−pn−p次,在失配时j移动次数最多有p−1p−1次(最坏情况下);因此,复杂度为O(n∗p)O(n∗p)。


我们仔细观察暴力匹配方法,发现:失配后下一次匹配,

  • 主串的起始位置 = 上一轮匹配的起始位置 + 1;
  • 模式串的起始位置 = 首字符P[0]

如此未能利用已经匹配上的字符的信息,造成了重复匹配。举个例子,比如:第一次匹配失败时,主串、模式串失配位置的字符分别为 a 与 c,下一次匹配时主串、模式串的起始位置分别为T[1]P[0];而在模式串中c之前是ab,未有重复字符结构,因此T[1]P[0]肯定不能匹配上,这样造成了重复匹配。直观上,下一次的匹配应从T[2]P[0]开始。

2. KMP算法

KMP思想

根据暴力方法的缺点,而引出KMP算法的思想。首先,一般化匹配失败,如下图所示:
KMP算法的来龙去脉

在暴力匹配方法中,下一次匹配开始时,主串指针会回溯到i+1,模式串指针会回退到0。那么,如果不让主串指针发生回溯,模式串的指针应回退到哪个位置才能保证正确匹配呢?首先,我们从上图中可以得到已匹配上的字符:

T[i…i+j−1]=P[0…j−1]T[i…i+j−1]=P[0…j−1]

KMP算法思想便是利用已经匹配上的字符信息,使得模式串的指针回退的字符位置能将主串与模式串已经匹配上的字符结构重新对齐。当有重复字符结构时,下一次匹配如下图所示:

KMP算法的来龙去脉

从图中可以看出,下一次匹配开始时,主串指针在失配位置i+j,模式串指针回退到m+1;模式串的重复字符结构:

T[i+j−m−1…i+j−1]=P[j−m−1…j−1]=P[0…m](1)(1)T[i+j−m−1…i+j−1]=P[j−m−1…j−1]=P[0…m]

且有

T[i+j]≠P[j]≠P[m+1]T[i+j]≠P[j]≠P[m+1]

那么应如何选取mm值呢?假定有满足式子(1)(1)的两个值m1>m2m1>m2,如下图所示:
KMP算法的来龙去脉

如果选取m=m2m=m2,则会丢失m=m1m=m1的这一种字符匹配情况。由数学归纳法容易知道,应取所有满足式子(1)(1)中最大的mm值。


KMP算法中每一次的匹配,

  • 主串的起始位置 = 上一轮匹配的失配位置;
  • 模式串的起始位置 = 重复字符结构的下一位字符(无重复字符结构,则模式串的首字符)

模式串P="abcac"匹配主串T="ababcabcacbab"的KMP过程如下图:
KMP算法的来龙去脉

部分匹配函数

根据上面的讨论,我们定义部分匹配函数(Partial Match,在数据结构书[2]称之为失配函数):

f(j)={max{m}−1P[0…m]=P[j−m…j],0≤m<jelsef(j)={max{m}P[0…m]=P[j−m…j],0≤m<j−1else

其表示字符串P[0…j]P[0…j]的前缀与后缀完全匹配的最大长度,也表示了模式串中重复字符结构信息。KMP中大名鼎鼎的next[j]函数表示对于模式串失配位置j+1,下一轮匹配时模式串的起始位置(即对齐于主串的失配位置);则

next[j]=f(j)+1next[j]=f(j)+1

如何计算部分匹配函数呢?首先来看一个例子,模式串P="ababababca"的部分匹配函数与next函数如下:

j 0 1 2 3 4 5 6 7 8 9  
P[j] a b a b a b a b c a  
f(j) -1 -1 0 1 2 3 4 5 -1 0  
next[j] 0 0 1 2 3 4 5 6 0 1  

模式串的f(j)满足P[0…f(j)]=P[j−f(j)…j]P[0…f(j)]=P[j−f(j)…j],在计算f(j+1)分为两类情况:

  • 若P[j+1]=P[f(j)+1]P[j+1]=P[f(j)+1],则有P[0…f(j)+1]=P[j−f(j)…j+1]P[0…f(j)+1]=P[j−f(j)…j+1],因此f(j+1)=f(j)+1
  • 若P[j+1]≠P[f(j)+1]P[j+1]≠P[f(j)+1],则要从P[0…f(j)]P[0…f(j)]中找出满足P[f(j+1)]=P[j+1]f(j+1),从而得到P[0…f(j+1)]=P[j+1−f(j+1)…j+1]P[0…f(j+1)]=P[j+1−f(j+1)…j+1]

其中,根据f(j)的定义有:

P[j]=P[f(j)]=P[f(f(j))]=⋯=P[fk(j)]P[j]=P[f(j)]=P[f(f(j))]=⋯=P[fk(j)]

其中,fk(j)=f(fk−1(j))fk(j)=f(fk−1(j))。通过上面的例子可知,函数fk(j)fk(j)是随着kk递减的,并最后收敛于-1。此外,P[j]p[j+1]相邻;因此若存在P[f(j+1)]=P[j+1],则必有

f(j+1)=fk(j)+1f(j+1)=fk(j)+1

为了求满足条件的最大的f(j+1),因fk(j)fk(j)是随着kk递减的,故应为满足上式的最小kk值。

综上,部分匹配函数的计算公式如下:

f(j)={fk(j−1)+1−1minkP[fk(j−1)+1]=P[j]elsef(j)={fk(j−1)+1minkP[fk(j−1)+1]=P[j]−1else

代码实现

部分匹配函数(失配函数)的C实现代码:

int *fail(char *p) {
int len = strlen(p);
int *f = (int *) malloc(len * sizeof(int));
f[0] = -1;
int i, j;
for(j = 1; j < len; j++) {
for(i = f[j-1]; ; i = f[i]) {
if(p[j] == p[i+1]) {
f[j] = i + 1;
break;
}
else if(i == -1) {
f[j] = -1;
break;
}
}
}
return f;
}

KMP的C实现代码:

int kmp(char *t, char *p) {
int *f = fail(p);
int i, j;
for(i = 0, j = 0; i < strlen(t) && j < strlen(p); ) {
if(t[i] == p[j]) {
i++;
j++;
}
else if(j == 0)
i++;
else
j = f[j-1] + 1;
}
return j == strlen(p) ? i - strlen(p) : -1;
}

时间复杂度fail函数的复杂度为O(p)O(p),kmp函数的复杂度为O(n)O(n),所以整个KMP算法的复杂度为O(n+p)O(n+p)。

KMP算法的来龙去脉的更多相关文章

  1. 【模式匹配】KMP算法的来龙去脉

    1. 引言 字符串匹配是极为常见的一种模式匹配.简单地说,就是判断主串\(T\)中是否出现该模式串\(P\),即\(P\)为\(T\)的子串.特别地,定义主串为\(T[0 \dots n-1]\),模 ...

  2. 深入理解KMP算法之续篇

    前言: 纠结于KMP已经两天了,相较于本人之前博客中提到的几篇博文,本人感觉这篇文章更清楚地说明了KMP算法的来龙去脉. http://www.cnblogs.com/*/archive/ ...

  3. KMP算法具体解释&lpar;转&rpar;

    作者:July. 出处:http://blog.csdn.net/v_JULY_v/. 引记 此前一天,一位MS的朋友邀我一起去与他讨论高速排序,红黑树,字典树,B树.后缀树,包含KMP算法,只有在解 ...

  4. 简单有效的kmp算法

    以前看过kmp算法,当时接触后总感觉好深奥啊,抱着数据结构的数啃了一中午,最终才大致看懂,后来提起kmp也只剩下“奥,它是做模式匹配的”这点干货.最近有空,翻出来算法导论看看,原来就是这么简单(先不说 ...

  5. KMP算法

    KMP算法是字符串模式匹配当中最经典的算法,原来大二学数据结构的有讲,但是当时只是记住了原理,但不知道代码实现,今天终于是完成了KMP的代码实现.原理KMP的原理其实很简单,给定一个字符串和一个模式串 ...

  6. 萌新笔记——用KMP算法与Trie字典树实现屏蔽敏感词(UTF-8编码)

    前几天写好了字典,又刚好重温了KMP算法,恰逢遇到朋友吐槽最近被和谐的词越来越多了,于是突发奇想,想要自己实现一下敏感词屏蔽. 基本敏感词的屏蔽说起来很简单,只要把字符串中的敏感词替换成"* ...

  7. KMP算法实现

    链接:http://blog.csdn.net/joylnwang/article/details/6778316 KMP算法是一种很经典的字符串匹配算法,链接中的讲解已经是很明确得了,自己按照其讲解 ...

  8. 数据结构与算法JavaScript &lpar;五&rpar; 串&lpar;经典KMP算法&rpar;

    KMP算法和BM算法 KMP是前缀匹配和BM后缀匹配的经典算法,看得出来前缀匹配和后缀匹配的区别就仅仅在于比较的顺序不同 前缀匹配是指:模式串和母串的比较从左到右,模式串的移动也是从 左到右 后缀匹配 ...

  9. 扩展KMP算法

    一 问题定义 给定母串S和子串T,定义n为母串S的长度,m为子串T的长度,suffix[i]为第i个字符开始的母串S的后缀子串,extend[i]为suffix[i]与字串T的最长公共前缀长度.求出所 ...

随机推荐

  1. C&num;使用二叉树算法设计一个无限分级的树表

    效果图: 数据库: 操作树的示意图: 控制器代码: using Dw.Business; using Dw.Entity; using Dw.Utilities; using System; usin ...

  2. Redis的WEB界面管理工具phpRedisAdmin

    下载地址:http://down.admin5.com/php/75024.html 官方网址:https://github.com/ErikDubbelboer/phpRedisAdmin

  3. HDU5869树状数组&plus;gcd预处理

    比赛的时候知道用树状数组,但有点乱不知道怎么处理. 统计不同的gcd的个数其实就是用树状数组统计区间内不同的数的模板题啊... 复杂度O(nlogn) #include <bits/stdc++ ...

  4. jmap命令

    一.jmap -heap PID using parallel threads in the new generation.  ##新生代采用的是并行线程处理方式 using thread-local ...

  5. scrapy使用爬取多个页面

    scrapy是个好玩的爬虫框架,基本用法就是:输入起始的一堆url,让爬虫去get这些网页,然后parse页面,获取自己喜欢的东西.. 用上去有django的感觉,有settings,有field.还 ...

  6. 四、oracle 用户管理&lpar;Profile&rpar;

    oracle 用户管理 :profile + tablespace + role + user  一.使用profile管理用户口令概述:profile是口令限制,资源限制的命令集合,当建立数据库时, ...

  7. 1740&colon; &lbrack;Usaco2005 mar&rsqb;Yogurt factory 奶酪工厂

    1740: [Usaco2005 mar]Yogurt factory 奶酪工厂 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 119  Solved:  ...

  8. L与&lowbar;T

    https://www.cnblogs.com/xxn-180727/p/9378519.html _T( ) 是一个适配的宏,当工程采用Unicode字符时 _T()就是 L,会将多字节的字符串转化 ...

  9. &lbrack;BZOJ4700&rsqb;适者&lpar;CDQ分治&plus;DP&sol;李超线段树&rpar;

    如果没有秒杀,就是经典的国王游戏问题,按t/a从小到大排序即可. 考虑删除两个数i<j能给答案减少的贡献:S[i]*T[i]+P[i-1]*A[i]-A[i]+S[j]*T[j]+P[j-1]* ...

  10. Linux中关机,重启,注销命令

    关机: shutdown -h now  #立刻关机重启,工作中常用 shutdown -h +1    #1分钟后关机 init 0 halt                        #立即停 ...