字符串匹配——KMP算法

时间:2022-08-28 12:45:59

关于KMP算法的分析,我觉得这两篇博客写的不错:

http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html

http://blog.csdn.net/v_JULY_v/article/details/6545192

下面的笔记也是参考了这两篇博客的。

KMP算法是最有名的字符串匹配算法了。它是BF算法的改进版,至于是如何改进的,先引用上述第二篇博客里的一段话:

“在继续分析之前,咱们来思考这样一个问题:为什么快排或者堆排序比直接的选择排序快?直接的选择排序,每次都是重复地比较数值的大小,每扫描一次,只得出一个最大(小值),再没有其它的成果。而快排,每扫一次,将数据分成了两部分,右边的数据都大于左边的数据,所以在下一次比较的时候,这两部分元素之间就不用比较了。再看堆排序,建堆的过程也是O(n)的比较,但比较的结果得到了最大(小)堆这种三角关系,之后的比较就不用再每一个都需要比较了。

由上述思考,咱们总结出了一点优化的归律:采用一种简单的数据结构或者方式,将每次重复性的工作得到的信息记录得尽量多,方便下一次做同样的工作,这样将带来一定的优化。”

总结上面这段话的核心思想,就是把循环中要重复做的工作提取到循环外完成,从而提高效率。

下面用一个例子演示KMP匹配的过程。其中涉及的三个数据如下:

source:源串,即要匹配的母船

pattern:模式串,即子串

next数组:其每个元素对应pattern的每个字符,表示当该字符pattern[j]不匹配source[i]时,应该从pattern的哪个下标开始重新匹配当前的source[i],具体求法后面介绍

本例中source为ABCABCABDE,pattern为ABCABD,第一次匹配,前5个字符均匹配成功,匹配第6个字符时如下:

字符串匹配——KMP算法

字符串匹配——KMP算法

此时发现source[5]和pattern[5]不匹配,BF算法的做法是从source[1]开始从新匹配pattern,即像下面这样:

字符串匹配——KMP算法

字符串匹配——KMP算法

而KMP算法则不会再去重新比较source[1...4]了,它会利用next的信息调整pattern。因为next[5]=2,所以下一次匹配将当前的source[5]和pattern[2]比较,即像下面这样:

字符串匹配——KMP算法

字符串匹配——KMP算法

这时发现source[5]和pattern[2]匹配,继续往下比较,直至pattern中所以元素都比较完了,如下:

字符串匹配——KMP算法

字符串匹配——KMP算法

此时已经在母串中找到了子串,i=9,m=6,所以返回的下标为i-m=3。

这样整个匹配过程就完了,感觉不过瘾吗,前面两篇推荐的博客里有更长的匹配串分析。

把上面的过程翻译成代码,也就是KMP算法的实现,如下:

int KMP(char *source, char *pattern)
{
int i, j, m, n;
int *next; i = 0;
j = 0;
m = strlen(pattern);
n = strlen(source);
next = (int *)malloc(m * sizeof(int)); Next(pattern, next);<span style="white-space:pre"> </span>// 这是求next数组的函数,后面解释 while (i < n && j < m)
{
if (j == -1 || source[i] == pattern[j])
{
++i;
++j;
}
else
{
j = next[j];
}
} free(next); if (j == m)
{
return i - m;
}
else
{
return -1;
}
}

下面来看next数组是如何求得的。

如前所述,next数组里保存的就是pattern[j]与母串中的字符不匹配时,该用哪个下标继续和这个字符匹配。若next[j]=k,则有:pattern[0...k-1] = pattern[j-k...j-1],而且这个k是最大的,即不会有pattern[0...k] =pattern[j-k-1...j-1]。

反过来想,如果已经知道了pattern[0...k-1] = pattern[j-k...j-1],next[j]是多少呢?要分两种情况考虑:

1. pattern[k] != pattern[j],则next[j] = k

2. pattern[k] = pattern[j],则next[j] = next[k]

第二种情况是因为,若pattern[k] = pattern[j]时,也使next[j]=k;则在pattern[j]于母串不匹配的情况下,再拿pattern[next[j]]即pattern[k]去比较的话,肯定也是不匹配的。这是隐藏的重复工作。使next[j] = next[k]就能避免这种重复工作。

那么怎么又能保证pattern[j]不会等于pattern[next[k]]呢?不急,一步一步想:最开始有next[0]=-1,如果pattern[1]=pattern[0],那么由规则2,next[1]=next[0]=-1;再如果pattern[2]=pattern[1],那么next[2]=next[1]=-1。看到了吗,三个一样的字符,如果是next依次为前一个元素的情况,最终都会是最前面那个next值,这就保证了最大的减少重复工作。

好吧,上面的解释很挫,意会吧。其实就跟离散树一样,同一个集合的元素都有相同的根。

有了上面的规则后,求next数组就比较方便了。用动态规划的策略很好实现:

void Next(char *pattern, int *next)
{
int i, j, n; i = 0;
j = -1;
next[0] = -1;
n = strlen(pattern); while (i < n - 1)
{
if (j == -1 || pattern[i] == pattern[j])
{
++i;
++j; if (pattern[i] != pattern[j])
{
next[i] = j;
}
else
{
next[i] = next[j];
}
}
else
{
j = next[j];
}
}
}

是不是感觉和KMP的框架很像,其实就是一回事,都是子串匹配,都利用了next数组。

不同之处在于:KMP是source匹配pattern,Next是一直从pattern的头部开始匹配后面所有的序列;在Next里面,next数组还没有完全生成,是利用前面生成的next加上匹配结果生成下一个next,这就是动规的策略。

字符串匹配——KMP算法的更多相关文章

  1. 字符串匹配KMP算法详解

    1. 引言 以前看过很多次KMP算法,一直觉得很有用,但都没有搞明白,一方面是网上很少有比较详细的通俗易懂的讲解,另一方面也怪自己没有沉下心来研究.最近在leetcode上又遇见字符串匹配的题目,以此 ...

  2. 字符串匹配KMP算法

    1. 字符串匹配的KMP算法 2. KMP算法详解 3. 从头到尾彻底理解KMP

  3. 字符串匹配--kmp算法原理整理

    kmp算法原理:求出P0···Pi的最大相同前后缀长度k: 字符串匹配是计算机的基本任务之一.举例,字符串"BBC ABCDAB ABCDABCDABDE",里面是否包含另一个字符 ...

  4. 字符串匹配KMP算法的C语言实现

    字符串匹配是计算机的基本任务之一. 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD&quot ...

  5. 字符串匹配KMP算法的讲解C&plus;&plus;

    转自http://blog.csdn.net/starstar1992/article/details/54913261 也可以参考http://blog.csdn.net/liu940204/art ...

  6. 字符串匹配KMP算法(转自阮一峰)

    转自 http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html 字符串匹配是计算 ...

  7. 【Luogu P3375】字符串匹配KMP算法模板

    Luogu P3375 模式串:即题目中的S2所代表的意义 文本串:即题目中的S1所代表的意义 对于字符串匹配,有一种很显然的朴素算法:在S1中枚举起点一位一位匹配,失配之后起点往后移动一位,从头开始 ...

  8. 字符串匹配—KMP算法

    KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特-莫里斯-普拉特操作(简称KMP算法).KMP算法的核心是利用匹配失败后 ...

  9. &lt&semi;字符串匹配&gt&semi;KMP算法为何比暴力求解的时间复杂度更低&quest;

    str表示文本串,m表示模式串; str[i+j] 和 m[j] 是正在进行匹配的字符; KMP的时间复杂度是O(m+n)  ,  暴力求解的时间复杂度是O(m*n) KMP利用了B[0:j]和A[i ...

随机推荐

  1. spring事务详解(转载&plus;高亮)

    spring提供的事务管理可以分为两类:编程式的和声明式的.编程式的,比较灵活,但是代码量大,存在重复的代码比较多:声明式的比编程式的更灵活.编程式主要使用transactionTemplate.省略 ...

  2. Git 执行 「fork 出来的仓库」和「最新版本的原仓库」内容同步更新

    当我们在 GitHub 上 fork 出一个仓库后,如果原仓库更新了,此时怎样才能保证我们 fork 出来的仓库和原仓库内容一致呢?我们一般关注的是仓库的 master(主干分支)的内容,通过以下步骤 ...

  3. VC&plus;&plus;全局变量初始化

    目录 第1章说明    2 1.1 程序启动    2 1.2 强符号.弱符号    2 1.3 动态初始化顺序    3 1.4 exe调用dll    4 1.5 禁用动态初始化    4 1.6 ...

  4. 简单实体Json序列化&lpar;输出JSON的属性可变&rpar;

    简单实体Json序列化(输出JSON的属性可变) 一.先看效果 可以看出 , 我们在序列化一个对像时, 只给出了 我们想要 输出的两个字段名,  实际实体有5个属性, 经过可变属性序列化后的JSON ...

  5. JAVA提高十九:WeakHashMap&amp&semi;EnumMap&amp&semi;LinkedHashMap&amp&semi;LinkedHashSet深入分析

    因为最近工作太忙了,连续的晚上支撑和上班,因此没有精力来写下这篇博客,今天上午正好有一点空,因此来复习一下不太常用的集合体系大家族中的几个类:WeakHashMap&EnumMap&L ...

  6. ubuntu opengl 开发

    开发环境: eclipse,需要安装C++开发插件,在自带的源中查找安装C++开发工具包即可 下载安装gl库: sudo apt-get install libgl1-mesa-dev 下载安装glu ...

  7. C&num;中生成GUID的四种格式

    var uuid = Guid.NewGuid().ToString(); // 9af7f46a-ea52-4aa3-b8c3-9fd484c2af12 var uuidN = Guid.NewGu ...

  8. Integer 原码解读

    有一天,突然发现,阅读原码可以发现很多有趣的东西.在Java中,我们知道很多东西都是封装好的,拿来即用,当你有一天去研究它拿来的东西是如何具体操作的,将会是非常有趣的事情. 在上一篇研究HashMap ...

  9. noip第9课资料

  10. java 生成随机数字

    for(int i=0;i<size1;i++){ int n = (int)(java.lang.Math.random()*99); LinkNode newLink = new LinkN ...