目录
KMP算法实际上解决的是一个字符串匹配的问题,即从一个目标字符串(通常非常长)中找到与给定字符串(也称为模式串)相匹配的字串的位置,例如:

如果用人脑去找,很容易找出模式串在目标串出现的位置有第5个和第21个,但是当目标串非常长的时候,显然人脑搜索就不太现实,那么如何来找呢?
首先我们想到的第一个方法就是暴力搜索,即一个一个地把目标串和模式串从头匹配到尾
第一轮对比在匹配到第5个时发现不匹配,即模式串的A和目标串的B不同,那么就进入下一轮对比,把模式串整个后移一位,即

然后继续从模式串的第一位开始比对,这样一轮一轮地匹配,直到搜索到目标串的结尾,但是很显然,这样做的效率太低了,接下来介绍的KMP算法将会大大提升搜索的效率
KMP算法的核心思想:利用已匹配的信息来减少回溯的次数
那么如何利用已匹配的信息来减少回溯的次数呢?
我们先来看看什么是已匹配的信息:
在第一轮比对中,已经将前四个字符匹配完毕,即ABAB是匹配的,唯一不匹配的字符是第五个字符
那么此时已匹配的信息就是ABAB
如果我们能让模式串在移动的过程中不再进行这步

而是直接向后移动到这里

将AB模式串的AB直接移动到目标串上可以匹配AB的部分,再去匹配后面的字符,这样无疑减少了无效比对的步骤,提高了算法的效率
那么如何实现将模式串直接移动到可以匹配一部分字符的位置呢?
我们来看第一轮比对中找到的已匹配信息:ABAB
不难发现第1个字符到第2个字符和第3个字符到第4个字符是相等的,都为AB,那么如果在第五个字符失配时直接把模式串往后移动两位,使得模式串的第1位到第2位替代原本第3位到第4位的位置,就做到了减少无效比对的次数
而这样做的原理就是模式串的第1位到第2位和第3位到第4位相同,即模式串前4位中前面2位(第1位和第2位)和后面两位(第3位和第4位)相同,那么如果我们能知道模式串中每个前缀子串(例如本例子中就是:A,AB,ABA,ABAB,ABABC)前n位和后n位相同的情况,就能在任意位置失配的时候,将模式串往前直接移动n位(这个n取决于失配的位置),从而达到快速匹配的效果了
那么接下来的工作就显而易见了,统计模式串的每一个前缀子串前n位和后n位相同的情况
首先要介绍前缀和后缀的概念:
实际上前缀和后缀就是上文中说的前n位和后n位,举个例子:

很显然对于这个模式串的不同前缀子串,它们的前缀后缀也是不同的,而我们要统计的就是每个前缀子串的最长相等前后缀长度,为什么是最长呢?
这是因为根据这种方法得到的匹配结果在失配位置前是能保证一定匹配的,例如第一轮匹配的中在第5位时失配,那么根据这种方法找到的下一个匹配位置一定能保证在目标串的第5位之前还能与模板串匹配的部分是一定匹配的,即
那么最长实际上能保证在以这种方式快速推进模式串的过程中不会漏掉和模式串完全匹配的字串,因为:每个前缀子串的相等前后缀长度越长,(即子串中的重复度越高),模式串在失配时按照此方法前进的距离越短,如果在一个前缀子串中前后缀相同的最大长度为3,而我们记录的前后缀相同长度为1,那么在这个前缀子串的下一个字符失配时,
按照此方法就会把模式串中第1位的字符匹配到前缀子串最后一个字符原本的位置,
而实际上正确的做法是把该模式串的第3个字符匹配到最后一个字符原本的位置,
若按照前面那种方法就可能错过匹配的子串
明白了这一点后,我们就可以整理出模式串中所有前缀子串的最大前后缀相等长度了,我们用数组的形式储存(就是所谓的next数组)
那么我们应该如何使用这个整理出来的next数组呢?
例如在第一轮匹配中,我们发现模式串的第5位不匹配,
那么我们只需要查询失配字符之前的前缀子串的最大前后缀长度(即失配字符的前一位对应的next数组的值)为2,然后将模式串的第2位匹配到失配字符之前的前缀子串的末位
这样就达成了最开始期待的效果,以此类推,在之后遇到不匹配的字符时,按照这种方法来推进,从而实现快速匹配的效果,这就是KMP算法
明白了KMP算法的理论部分,接下来就是用代码去实现了,首先要实现的是将统计出模式串的所有前缀子串的最大前后缀长度,也就是next数组,最容易想到的方法当然是暴力搜索,把所有前缀和后缀罗列出来,一个个去比对,找出最长的长度,但是当模式串较长时,暴力搜索的效率就显得很低下,那么要如何实现快速找到最长前后缀呢?
还是运用KMP算法的核心思想:利用已匹配的信息来减少回溯的次数
首先next数组的第一个元素一定是0(因为只有一个字符的子串连前后缀都没有,更没有相同的前后缀了),那么我们得出一个前缀子串加上下一个字符后形成的前缀子串的最大前后缀长度的方法就是根据该前缀子串的最大前后缀长度来考虑,分为两种情况:
1、最简单的情况,当前前缀子串的下一个字符和该前缀子串的第n + 1位(n为当前前缀子串的最大前后缀子串的长度)相同,那么加上这个字符后形成的新的前缀子串的最大前后缀长度就是n + 1,例如(当前前缀子串为ABA(最大前后缀长度为1),下一个字符为B)
翻译成代码就很简单了:
if(pattern[i] == pattern[j])
{
j++;
}
i代表新的字符,j代表最大前后缀长度,也就是上面提到的n,pattern就是模式串,由于数组下标从0开始,所以所有的第j+1位在数组中的表现形式都是next[j],以下出现的同理
2、比较复杂,也是整个算法的难点,当前前缀子串的下一个字符和该前缀子串的第n + 1位(n为当前前缀子串的最大前后缀子串的长度)不相同,这时就要往回找,直到找到和新的字符能匹配的位置,而往回找的过程也是要遵循一个基本原则:不匹配字符之前的部分一定能保证匹配,而我们在之前找到的next部分数组恰好记录了前面所有子串的最大前后缀长度,那么我们就可以利用这点来进行往回找能与新字符匹配的字符而能遵循基本原则并且快速,举个例子:
如果X不是B那就继续按照这个规则往前找,直到找到相同的字符或者找到第一个字符(即j变成0,在代码中的体现就是&&左边的j>0),这里显然需要的是一个循环,翻译成代码就是:
while(j > 0 && pattern[i] != pattern[j])
{
j = next[j - 1];
}
这样我们已经基本将next数组的构造过程梳理完了,最后一步就是根据这个next数组来实现快速匹配了,我们先将上面介绍过的方法搬运下来:
查询失配字符之前的前缀子串的最大前后缀长度(即失配字符的前一位对应的next数组的值)为n,然后将模式串的第n位匹配到失配字符之前的前缀子串的末位(在代码中的体现就是失配字符target[i]和第n+1个字符pattern[j]比对),翻译成代码如下:
for (int i = 0, j = 0; i < target.length(); i++)
{
while (j > 0 && target[i] != pattern[j])
{
j = next[j - 1];
}
if (target[i] == pattern[j])
{
j++;
}
if (j == pattern.length())
{
cout << "Found pattern at " << i - j + 1 << endl;
j = next[j - 1];//这一步就是继续往后找是否还有匹配的子串
}
}
return 0;
}
target是目标串
这样KMP算法就实现了,下面是完整代码和运行结果:
(用vector容器代替了原本的数组,不用手动管理内存)
#include <iostream>
using namespace std;
#include <vector>
vector<int> Next(string pattern)
{
vector<int> next;
next.push_back(0); //next容器的首位必定为0
for (int i = 1, j = 0; i < pattern.length(); i++)
{
while (j > 0 && pattern[j] != pattern[i])
{
j = next[j - 1];
}
if (pattern[i] == pattern[j])
{
j++;
}
next.push_back(j);
}
return next;
}
int main()
{
string pattern = "ABABC", target = "ABABA ABABCABAA CAADB ABABCAADKDABC";
vector<int>next = Next(pattern);
for (int i = 0, j = 0; i < target.length(); i++)
{
while (j > 0 && target[i] != pattern[j])
{
j = next[j - 1];
}
if (target[i] == pattern[j])
{
j++;
}
if (j == pattern.length())
{
cout << "Found pattern at " << i - j << " position" << endl;
j = next[j - 1];
}
}
return 0;
}
运行结果:

我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复
在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定
我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项
我查看了Stripedocumentationonerrors,但我仍然无法正确处理/重定向这些错误。基本上无论发生什么,我都希望他们返回到edit操作(通过edit_profile_path)并向他们显示一条消息(无论成功与否)。我在edit操作上有一个表单,它可以POST到update操作。使用有效的信用卡可以正常工作(费用在Stripe仪表板中)。我正在使用Stripe.js。classExtrasController5000,#amountincents:currency=>"usd",:card=>token,:description=>current_user.email)