相信不少人在学数据结构的时候都被KMP算法搞的迷迷糊糊的,原理看的似懂非懂,代码写不出来,或者写出来了也不知道为什么就可以这么写。本文力求尽可能通俗详细的讲解KMP算法,让你不再受到KMP算法的困扰。
所谓暴力匹配,就是从文本串的首端开始依次检查子串是否与模式串匹配,如果不匹配就将模式串往后移一个位置,从头开始匹配,直到在某处成功匹配或匹配到末尾也没能成功匹配。如下图:

设文本串为T,模式串为P,i为文本串中的下标,j为模式串中的下标,文本串的长度为m,模式串的长度为n,则代码如下:
int bruteForce(std::string t, std::string p) {
int i = 0, j = 0;
int m = t.length(), n = p.length();
while (i < m && j < n) {
if (t[i] == p[j]) {
i++; j++;
} else {
i = i - j + 1;
j = 0;
}
}
return j == n ? i - j : -1;
}
那么暴力匹配的时间效率如何呢?不难发现,每一次匹配中,我们都需要花费\(O(n)\)的时间成本来判断子串是否与模式串匹配,而总共的判断次数最多为\(m-n+1\),由于实际情况下有\(m>>n\),因此\(m-n+1\)近似等于\(m\),整个暴力匹配的时间复杂度为\(O(mn)\),显然不理想。
经过观察,我们不难发现,暴力匹配方法做了很多次不必要的匹配。在第一轮发现不匹配的时候,我们无需只将模式串后移一个位置,而是后移到文本串中下标为3的位置(第二个A),并直接从文本串中下标为5的位置(第2个C)开始匹配。从相对运动的角度来讲,也就是将j前移为2,而i不用回退。

事实上,之所以这么做,是因为模式串中j前面的某些字符恰好与模式串的某个前缀相等。如果你想到了这点,那你的想法刚好就跟发明KMP算法的那三个人的想法一样了(认真)。KMP算法就利用了这一点,每次匹配失败的时候不直接从头开始继续匹配,而是将j回溯到这个前缀后面的字符,而i不用回退,以解决暴力匹配算法的这一痛点。如图:

为了应对各种匹配失败的情况,我们需要另开一个与模式串等长的数组next,其中next[j]表示P[j]与T[i]匹配失败的情况下,j要移动到的下标。(显然,对于任意的j,一定有next[j] < j)按照上面那个性质,P[next[j]]之前的p个字符也与P[j]左边的p个字符相等。(其中p为P[next[j]]之前的字符数量)(这一点非常重要,可以说是next数组构建算法的灵魂!)
接下来的一个问题就是,如何判定某次匹配过程失败后,j该移到哪个位置呢?
我们可以用递推的思路来求解。
考虑模式串的第一个字符就不与文本串中的相应字符匹配的情况。如图:

这个时候我们需要将i往后移,不妨将next[0]设为-1。(后面你就会看到这样做自有其精妙之处)
再来考虑next[k]已知的情况,如何求得next[k+1]呢?分两种情况讨论:
第一种情况,P[k]==P[next[k]],如下图。由上面那条性质,P[k]之前的p个字符与P[next[k]]之前的p个字符相等。而P[k]又是等于P[next[k]]的,因此,P[k+1]之前的p+1个字符与P[next[k]+1]之前的p+1个字符相等。所以,next[k+1]应该设为next[k]+1,以符合上面那条性质。

第二种情况,P[k]!=P[next[k]],如下图。(这里我用不同的颜色标出来了)

怎么办呢?再考虑P[next[next[k]]]与P[k]之间的关系。

此时的思路与上面相似,如果P[k]==P[next[next[k]]],就将next[k+1]设为next[next[k]]+1,否则就依次检查next[next[next[k]]]、next[next[next[next[k]]]]、...
不难看出,接受检查的下标是依次递减的,但是递减也得有个限度;另外next[0]永远为-1,因此递减到-1的时候,就说明一直检查到P的第一个字符也没检查到与P[k]相等的字符。此时next[k+1]前面有0个字符与P中长度为0的前缀相等。因此j需要回溯到0,将next[k+1]设为0。
将以上思路稍作整理,可得在next[k]已知的情况下,求得next[k+1]的步骤:
next[k]。next[k+1]设为0。P[k]是否等于P[t]。如果等于,就将next[k+1]设为t+1;否则,将t设为next[t],跳转到第2步。细心的你可能已经发现了,既然next[0]为-1,-1再加上1刚好也等于0,因此两个条件可以合并起来,上述步骤可以优化一下:
next[k]。P[k]等于P[t],就将next[k+1]设为t+1。next[t],跳转到第2步。现在你应该看到将next[0]设为-1这种做法的巧妙之处了吧!
这样,由于next[0]事先约定为-1,而由next[0]可以求得next[1],由next[1]可以求得next[2]...,因此我们就可以得出构建next数组的步骤:
next数组,令其长度为n。next[0]设为-1。next[k]。P[k]等于P[t],就将next[k+1]设为t+1。next[t],跳转到第5步。代码实现:
std::vector<int> buildNext(std::string p) {
int n = p.length();
std::vector<int> next(n);
next[0] = -1;
for (int k = 0; k < n - 1; k++) {
int t = next[k];
while (t != -1 && p[k] != p[t]) {
t = next[t];
}
next[k + 1] = t + 1;
}
return next;
}
有了next数组,一切都好办了。
每次匹配的时候,如果匹配成功了就i与j同时往后移一个位置,匹配失败的话j设为next[j]。如果j为-1的话,i就往后移,同时j设为0。
int kmp(std::string t, std::string p) {
int m = t.length(), n = p.length();
int i = 0, j = 0;
auto next = buildNext(p);
while (i < m && j < n) {
if (j < 0 || t[i] == p[j]) {
i++; j++;
} else {
j = next[j];
}
}
return j == n ? i - j : -1;
}
不难看出,KMP算法的空间复杂度(不计T和P本身所占的内存空间)为\(O(n)\),这是来自next数组所占用的空间开销。
那么时间复杂度为多少呢?网上大多数博文直接在这里放个结论,缺少必要的分析,读者只是知道了结论,至于为什么是这样则是一头雾水。
整个KMP算法的时间复杂度分为以下两部分:
next数组的时间复杂度;其中,构建next数组的时间复杂度为多少呢?
这主要取决于给next数组各项赋值的时间复杂度和对t赋值的次数。
显而易见,前者的时间复杂度为\(O(n)\)。那后者的时间复杂度怎么计算呢?
注意到,每次for循环的结尾,有一个next[k + 1] = t + 1;的语句,而下一次for循环开始时,由于k自增了1,因此int t = next[k];里的next[k]其实就是上一次循环里的next[k + 1],这条语句执行后的新t其实就是旧t加上1,可以等效的认为对t进行了一次++运算。显而易见,t++的次数为n-1。而while循环里面t = next[t];的最坏次数怎么计算呢?我们知道,next[t]是必然小于t的,所以这条语句执行后t是要往回跳的。但是跳一次跨越的步数是大于等于1的,而往回跳的极限是-1,所以同样的长度,往前跳的次数是n-1,往后跳的次数必然不超过n-1,所以对t赋值的次数(不如说是t跳跃的次数)不会超过2n-2,当然就是\(O(n)\)量级的。所以,构建next数组的时间复杂度为\(O(n)\)。
而匹配的时间复杂度又是多少呢?
这主要取决于while循环执行的次数,而while循环是否执行取决于i和j的取值,因此这也取决于对i和j赋值的次数。
对i赋值的操作只有i++这一条语句,显然这条语句最多会执行m次。
对j的赋值(或者说是跳跃)呢,分析思路与上述类似,包括往前跳跃(j++)和往后跳跃(j = next[j])。其中前者是与i“携手并进”的,因此执行次数也不会超过m。往后跳跃的次数同样不会超过往前跳跃的次数(原因与上述分析一致)。因此,j的跳跃次数也是\(O(m)\)量级的。
因此,匹配的时间复杂度是\(O(m)\)。
综上所述,整个KMP算法的时间复杂度为\(O(m+n)\),比暴力算法的\(O(mn)\)要好得多。
考虑下面的情况:
文本串:AAAABAAAAA
模式串:AAAAA
如果我们用KMP算法进行匹配的话,会由于T[4] != P[4]发生一次匹配失败:

根据next数组的指示,将会由P[3]继续匹配T[4]:

然后是P[2]、P[1]、P[0],最后因为P[0]与T[4]匹配失败而开始T[5]与P[0]的比对。
但是,明眼人一眼就能看出,T[4]与P[4]比对失败后可以直接进行T[5]与P[0]之间的比对,不需要进行T[4]与P[3]、P[2]...P[0]之间的比对了,因为P[4]和P[3]、P[2]...P[0]是一样的,既然T[4]与P[4]比对失败了,那么T[4]与P[3]、P[2]...P[0]之间的比对就一定会失败,就像推销员给你推销某样产品,你不感兴趣,对方一直喋喋不休,只会让你感到厌烦。
那怎样才能在一次比对失败后不再比对P中相同的字符,而是从不相同的字符开始比对呢?换句话说,如何在比对失败后,能够让j一次性跳转到不一样的字符呢?我们只需要对构建next数组的代码稍作修改。在给next[j+1]赋值的时候,我们还需要检查P[k+1]是否等于P[t+1]。如果等于的话,就赋值为next[t+1]。否则才赋值为t+1。如图:

但是直接这样改的话,每次for循环后的t就不一定等于上一次循环的t加1了,所以我们要显式的维护变量t。
std::vector<int> buildNext() {
int n = p.length();
std::vector<int> next(n);
next[0] = -1;
int t = -1;
for (int k = 0; k < n - 1; k++) {
while (t != -1 && p[k] != p[t]) {
t = next[t];
}
next[k + 1] = p[k + 1] == p[t + 1] ? next[t + 1] : t + 1;
t++;
}
return next;
}
显然,时间复杂度是不变的,但是因为跳跃次数减少了,整个算法的效率也会提升。
目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非
1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva
我一直在尝试用Ruby实现Luhn算法。我一直在执行以下步骤:该公式根据其包含的校验位验证数字,该校验位通常附加到部分帐号以生成完整帐号。此帐号必须通过以下测试:从最右边的校验位开始向左移动,每第二个数字的值加倍。将乘积的数字(例如,10=1+0=1、14=1+4=5)与原始数字的未加倍数字相加。如果总模10等于0(如果总和以零结尾),则根据Luhn公式该数字有效;否则无效。http://en.wikipedia.org/wiki/Luhn_algorithm这是我想出的:defvalidCreditCard(cardNumber)sum=0nums=cardNumber.to_s.s
下面是我写的一个计算斐波那契数列中的值的方法:deffib(n)ifn==0return0endifn==1return1endifn>=2returnfib(n-1)+(fib(n-2))endend它工作到n=14,但在那之后我收到一条消息说程序响应时间太长(我正在使用repl.it)。有人知道为什么会这样吗? 最佳答案 Naivefibonacci进行了大量的重复计算-在fib(14)fib(4)中计算了很多次。您可以将内存添加到您的算法中以使其更快:deffib(n,memo={})ifn==0||n==1returnnen
为了防止在迁移到生产站点期间出现数据库事务错误,我们遵循了https://github.com/LendingHome/zero_downtime_migrations中列出的建议。(具体由https://robots.thoughtbot.com/how-to-create-postgres-indexes-concurrently-in概述),但在特别大的表上创建索引期间,即使是索引创建的“并发”方法也会锁定表并导致该表上的任何ActiveRecord创建或更新导致各自的事务失败有PG::InFailedSqlTransaction异常。下面是我们运行Rails4.2(使用Acti
我正在开发一个类似微论坛的项目,其中一个特殊用户发布一条快速(接近推文大小)的主题消息,订阅者可以用他们自己的类似大小的消息来响应。直截了当,没有任何形式的“挖掘”或投票,只是每个主题消息的响应按时间顺序排列。但预计会有很高的流量。我们想根据它们引起的响应嗡嗡声来标记主题消息,使用0到10的等级。在谷歌上搜索了一段时间的趋势算法和开源社区应用示例,到目前为止已经收集到两个有趣的引用资料,但我还没有完全理解它们:Understandingalgorithmsformeasuringtrends,关于使用基线趋势算法比较维基百科页面浏览量的讨论,在SO上。TheBritneySpearsP
我收到错误:unsupportedcipheralgorithm(AES-256-GCM)(RuntimeError)但我似乎具备所有要求:ruby版本:$ruby--versionruby2.1.2p95OpenSSL会列出gcm:$opensslenc-help2>&1|grepgcm-aes-128-ecb-aes-128-gcm-aes-128-ofb-aes-192-ecb-aes-192-gcm-aes-192-ofb-aes-256-ecb-aes-256-gcm-aes-256-ofbRuby解释器:$irb2.1.2:001>require'openssl';puts
文章目录一.Dijkstra算法想解决的问题二.Dijkstra算法理论三.java代码实现一.Dijkstra算法想解决的问题解决的问题:求解单源最短路径,即各个节点到达源点的最短路径或权值考察其他所有节点到源点的最短路径和长度局限性:无法解决权值为负数的情况二.Dijkstra算法理论参数:S记录当前已经处理过的源点到最短节点U记录还未处理的节点dist[]记录各个节点到起始节点的最短权值path[]记录各个节点的上一级节点(用来联系该节点到起始节点的路径)Dijkstra算法步骤:(1)初始化:顶点集S:节点A到自已的最短路径长度为0。只包含源点,即S={A}顶点集U:包含除A外的其他顶
对于体育新闻中文文本的关键字提取,常用的算法包括TF-IDF、TextRank和LDA等。它们的基本步骤如下:1.TF-IDF算法: -将文本进行分词和词性标注处理。-统计每个词在文本中的词频(TF)。-计算每个词在整个语料库中出现的文档频率(DF)和逆文档频率(IDF)。-计算每个词的TF-IDF值,并按照值的大小进行排序,选择排名前几的词作为关键字。2.TextRank算法:-将文本进行分词和词性标注处理。-将分词结果转化成图模型,每个词语为节点,根据词语之间的共现关系建立边。-对图模型进行迭代计算,计算每个节点的PageRank值,表示该节点的重要性。-选择排名前几的节点作为关键字。3.
我正在尝试计算由二进制形式的1和0的P数表示的数字的数量。如果P=2,则表示的数字为0011、1100、0110、0101、1001、1010,所以计数为6。我试过:[0,0,1,1].permutation.to_a.uniq但这不是大数的最佳解决方案(P可以什么可能是最好的排列技术,或者我们是否有任何直接的数学来做到这一点? 最佳答案 Numberofpermutationcanbecalculatedusingfactorial.a=[0,0,1,1](1..a.size).inject(:*)#=>4!=>24要计算重复项,