探秘KMP算法:解密字符串匹配的黑科技

KMP算法

在正式进入KMP算法之前,不得不先引经据典一番,因为直接去理解KMP,你可能会很痛苦(别问,问就是我也痛苦过)。所以做好前面的预热工作非常非常重要,为了搞明白KMP,在没见到KMP算法的完整代码之前,请耐心的将前面的东西看完。

一些相关的概念

学习KMP算法,得明白它主要得作用是什么,或者说它得出现是为了能更好得解决哪些方面得问题。

  • 字符串

字符串这个概念就不过多介绍了,毕竟,相信点开这篇文章的,怎么说也是掌握 了至少一门编程语言的coder了吧,那对于诸如此简单的知识点相信还是没什么大问题的。

  • 文本串

也称为主串或者模式串,也就是一串参照字符,可能有些地方叫法不一样,为了统一,以下统称为文本串。
比如:【abaabaabeca】可以看作是一文本串。

  • 模式串

可能也有叫匹配串或者模板串啥的,用于在文本串中进行匹配的一行查找字符串,一般会作为被参照的字符串在文本串中逐一匹配,比如:【abaabe】作为一行【abaabaabeca】的模式串。

  • 字符串匹配

换句话说,字符串的匹配可以理解为根据某种规则在一行所给字符串中进行查找的过程。拿上面的示例字符串来讲,我们需要在文本串【abaabaabeca】中查找模式串【abaabe】是否在其中出现过,返回出现的首索引,显然【abaabe】在【abaabaabeca】中第4(索引为3)个位置开始匹配成功。如上就是一个字符串匹配的过程。那么如何进行匹配?一般都有哪些方法?这个问题,下文即是答案。

问题引入

为了更好的讲解KMP算法,我们假设有这样的一道题:
给定一个文本串T,以及一个模板串P,所有字符串中只包含大小写英文字母
模板串 P文本串 S中多次作为子串出现。
若该模板串在文本串中出现过,请返回首次出现的下标位置,否则返回**-1**。

问题解决

根据题意,一种比较常见的解法是利用BF算法,也就是直接进行暴力匹配,具体的匹配过程是怎么样的,我们不妨还是以之前的两串为例,即文本串T = 【abaabaabeca】,模板串P=【abaabe】。暴力匹配过程如下图。

BF匹配过程

请在此添加图片描述

第一步:初始时,i,j指向两串的第一个字符处,i=1,j=1;此时字符T[a]==P[a]匹配,指针i,j均后移一位,i=2,j=2

第二步:i,j指向两串的第二个字符处,i=2,j=2;此时字符T[b]==P[a]匹配,指针i,j均后移一位,i=3,j=3

第三步:i,j指向两串的第三个字符处,i=3,j=3;此时字符T[a]==P[a]匹配,指针i,j均后移一位,i=4,j=4

…跳过第一趟均是匹配的步骤…,此时i=6,j=6,T[a]!=P[e],显然出现了失配的情况。为了能继续匹配后面的字符,指针i应当回退到T串中第二个位置处,j指针应当回退到P串中第一个位置【即回到模板串的开头】重新开始匹配。如下图:

请在此添加图片描述

请在此添加图片描述

第二趟匹配开始,此时i=2,j=1,T[b]!=P[a],出现失配情况,i指针继续回退到下一个位置,指针j再次回退到开头。即i回退到i=3的位置,j=1,过程和第一趟匹配是一样的原理。
经过简单的模拟会发现,文本串中的i指针每次回退的位置为i-j+2,而模板串指针j每次均需要回退到串的开头,通过上述匹配过程也发现了,BF算法中各指针j频繁的回退到开头的这个过程其实是真的够暴力。因此,引出了今天的主角KMP算法

  • 通过上面的演示,不难得出BF算法的代码。
//BF算法C++
int BF(char s[],char p[])
{int i=1,j=1;while(i<s[0] && j<p[0]){if(s[0]==p[0]){i++;j++;}else{i = i-j+2;j=1;}}if(j>p[0]){return (i-p[0]);}else{return -1;}
}

C++实现

//BF算法Java
public int BF(String T,String P){int j=0,i=0;int sn = T.length();int pn = P.length();//转字符数组char[] text = T.toCharArray();char[] patt = P.tpCharArray();while(i<sn && j<pn){if(text[i]==patt[j]){j++;i++;}else{i = i-j+1;j=0;}}if(j==pn){return i-j;}else{return -1;}
}

Java实现

KMP匹配过程

kmp算法中,相比于BF,一个很大的变化是指针的回退情况,上面也看到了,暴力的解法中,频繁的回退指针严重影响了算法的性能,其实很多匹配过程是多余的,比如BF中首次失配后,指针ij所指向位置的字符都是第一趟匹配以及匹配过的,而经过第二趟,第三趟,第…趟匹配之后,出现了很多重复匹配的情况,这也是导致暴力解法效率低下的原因。那么找到了原因,是不是可以考虑从指针回退位置入手,有没有一种算法或者技巧,能将指针在字符串中重复匹配的情况尽可能降到最低呢?观察下面两种回退情况。

  • BF中首次失配回退位置

请在此添加图片描述

  • KMP算法中,首次失配回退位置

请在此添加图片描述

为什么指针j会回退到P中第三个位置,而不是其他地方?观察匹配情况不难看出,其实在j指针之前的两个字符【a,b】(绿色部分)已经和指针i指向前面两个字符(绿色部分)匹配,所以自然没必要再重复匹配,j也就可以回退到第三个位置继续匹配过程。

  • 怎么知道两个指针前面的字符是否一样(匹配)呢?不急,请接着往下看。

回顾上面BF匹配的原理,为什么指针i,和指针j会同时走到当前位置??是不是只有当前位置前面的字符都匹配成功,i,j 指针才会不断后移,以至走到当前位置。否则就会执行回退操作,直到再次匹配。看图:

请在此添加图片描述

也就是说,我们大可不必在指向文本串T的指针i和指向模板串P的指针j中判断前面的字符是不是一样,而只需要在模板串中进行比较即可【如果还是不太了解的,建议多画图进行模拟加深印象】。可以看到,上图中蓝色部分字符左右两边绿色部分的串是相等的,而他们都在指针j之前,那么,我们不妨将j指针前面的那部分字符串命为P'【P点撇】,至此,再比较P'前缀后缀即可。在本例中,P'=[abaab]。重点关键词【前缀】【后缀】。

  • 关于前缀后缀

前缀:从前往后取的若干个字符
后缀:从后往前取的若干个字符
需要注意的是,这里的前缀后缀均不包含当前字符本身。
文字多是晦涩难懂,何不看看下面的这份表格?

字符串【abaab】的前后缀:

编号长度前缀后缀前后缀是否相等
11ab不相等
22abab相等
33abaaab不相等
44abaabaab不相等

长度表示前缀或者后缀字符的长度。通过上面的表格可以知道字符e之前字符串的前缀后缀最长为2
则相应的,模板串指针j可以回退到的位置为:2+1=3,即回退到模板串中第三个字符的位置。也就是下图中蓝色格子的a字符位置。注意,这个过程中,文本串的指针是不需要进行任何回退操作的。

请在此添加图片描述

至此,我们将引出在KMP算法中一个很重要的概念:【next数组】,这个自是个人习惯的叫法,在不同的资料或者文章里面每个人的叫法不一样,所以不必纠结于这种没什么营养的点。知道它的原理才是主要。

kmp算法核心:next数组

就像名称一样,next即下一个之意,通俗一点来说,就是在字符串匹配的过程中的失配情况下,模板串指针需要回退的位置,这个【位置】的值,就是next数组中的值。换句话说,next数组决定了失配时模板串指针的指向,而该指针的指向,直接决定了整个匹配过程的成败,或者说效率!!

  • 实例模拟,求解next数组的值’

为了更深刻的理解求解next数组的过程,先放过字符串【abaab】,我们来一个相对长一点的字符串进行模拟。该字符串为P = 【abaabcac

j12345678
Pabaabcac
next[j]01122312

具体的推导过程:用k表示j回退的位置且j=1,k=0,next[1]=0


Ⅰ.j=1,k=0,∵k=0,∴next[2]=1,∴++k=1,++j=2


Ⅱ.j=2,k=1,P[a]!=P[b],∴k = next[k]k = next[1]=0,j=2不变


Ⅲ.j=2,k=0,∵k=0,∴next[3]=1,∴++k=1,++j=3


Ⅳ.j=3,k=1,P[a]==P[a],∴next[4]=2,++j=4,++k=2


Ⅴ.j=4,k=2,P[b]!=P[a],∴k = next[k]即k = next[2]=1,j=4```不变

省略中间步骤VI到XI【11】(别问为什么,舍友关灯睡觉了)

最后一步.j=7,k=1,∵P[a]==P[a],∴next[8] = 2,匹配结束。

你需要注意的是,上面的过程中,j的位置是从1开始的,而不是0,因此,k=0表示此时指针仍指向第一个字符,由于一个字符,也就不存在前缀后缀,所以next[1]=0恒成立。也是因为这个起始位置的不同选择,会导致在代码和next数组的求解上有所差异。

想把next数组理解的更深刻,还是需要多练,搞清楚前缀和后缀的概念很重要!!!

  • 得到next数组的参考代码如下:
public static int[] Next(char[] p) {int pLen = p.length;int[] next = new int[pLen];int k = -1;int j = 0;// next数组中next[0]为-1,注意不是0了哦next[0] = -1;while (j < pLen - 1) {if (k == -1 || p[j] == p[k]) {next[++j] = ++k;} else {k = next[k];}}return next;}

Java版本

int Next(char t[],int next[])
{int i=1,j=0;next[1]=0;//注意这里next[1]=0,不是-1哦while(i<t[0]){if(j==0 || t[i]==t[j]){next[++j] = ++k;}else{j = next[j];}}
}

C++版本


  • KMP算法完整代码

搞清楚了next数组的求解,kmp算法也就是信手拈来的事了。
同样给出Java和C++版本的参考代码。

public static  int indexOf(String T, String P) {int i = 0, j = 0;char[] text = source.toCharArray();char[] ptt = pattern.toCharArray();int sLen = text.length;int pLen = ptt.length;int[] next = Next(ptt);while (i < sLen && j < pLen) {//匹配成功if (j == -1 || src[i] == ptn[j]) {i++;j++;} else {j = next[j];}}if (j == pLen) {return i - j+1;}return -1;}

Java版本参考

int KMP(char s[],char t[],int next[])
{int j=1,i=1;while(i<=s[0] && j<=t[0]){if(j==0 || s[i]==t[j]){i++;j++;}else{j = next[j];}}if(j>t[0]){return (i-t[0]);}else{return 0;}
}
一些可能存在的疑惑

看了上面两种代码的实现,可能会疑惑,为什么next[0]的值会有两种情况?什么原因产生的。

其实这个问题的原因很简单,就是下标初值得取值不同导致next数组会不一样,具体得,我们还是用上面得串来看看,下标从0开始的情况下,它对应的next数组是什么样的。

j01234567
Pabaabcac
next[j]-10011201

初始:next[0]=-1,j=0,k=-1


Ⅰ.j=0,k=-1,,∴next[++j] = ++k,即next[1]=0


Ⅱ.j=1,k=0,P[1]!=P[0],且k!=-1,∴k = next[k]==next[0] = -1


Ⅲ.j=1,k=-1,∴next[++j] = ++k,即next[2] = 0


中间步骤…


最后得到的next数组的值:【-1,0,0,1,1,2,0,1】。有没有发现,这串next数组的值与前面【0,1,1,2,2,3,1,2】之间有上面关系么,我们可以将【-1,0,0,1,1,2,0,1】的值全部左移一位,你会发现结果就是【0,1,1,2,2,3,1,2】。所以两种写法并没有严格的多错之分,不过需要特别注意在细节上的处理,不然求出的next数组可能会出错,直接导致整个kmp算法错误。


递推证明

请在此添加图片描述

为了防止被劝退,特地把数学证明放在了最后。

复杂度分析
  • BF算法

$O(mxn)$

  • KMP算法

$O(m+n)$

补上八股文

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。

关于KMP算法的优化

由于是常数上的优化,并不能改变原有的复杂度,所以就不再写了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/601653.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTML5+CSS3+JS小实例:圣诞按钮

实例:圣诞按钮 技术栈:HTML+CSS+JS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0&…

牛顿:Archetype AI 的开创性模型,实时解读真实世界的新宠儿

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

【Java】maven传递依赖冲突解决

传递依赖的概念&#xff1a; 传递依赖:&#xff1a; A.jar 依赖 B.jar, B.jar 依赖 C.jar, 这个时候我们就说B是A的直接依赖, C是A传递依赖; 传递依赖可能会产生冲突: 联系着上面, 新导入一个jar包D.jar, D依赖C.jar, 但是B依赖的1.1版本, 而D依赖的是1.2版本, 这时候C这个j…

深入理解指针2:数组名理解、一维数组传参本质、二级指针、指针数组和数组指针、函数中指针变量

目录 1、数组名理解 2、一维数组传参本质 3、二级指针 4、指针数组和数组指针 5、函数指针变量 1、数组名理解 首先来看一段代码&#xff1a; int main() {int arr[10] { 1,2,3,4,5,6,7,8,9,10 };printf("%d\n", sizeof(arr));return 0; } 输出的结果是&…

【Linux进阶之路】地址篇

文章目录 一、ipv4地址1. 基本概念2. 分类3.CIDR4.特殊的ip地址 二、IP协议1. 协议字段2.分片与重组3.路由 三、NAT技术1.公有和私有2.NAT3.NAPT 四、ARP协议1.MAC地址2.ARP 五、DHCP协议六、DNS协议尾序 一、ipv4地址 1. 基本概念 概念&#xff1a;IP地址&#xff0c;英文全…

2024-04-07 作业

作业要求&#xff1a; 1> 思维导图 2> 自由发挥应用场景实现一个登录窗口界面。 【可以是QQ登录界面、也可以是自己发挥的登录界面】 要求&#xff1a;尽量每行代码都有注释 作业1&#xff1a; 作业2&#xff1a; 运行代码&#xff1a; #include "myqwidget.h&quo…

前端三剑客 —— JavaScript (第二节)

目录 内容回顾 数据类型 基本数据类型&#xff1a; 引用数据类型&#xff1a; 常见运算 算术运算符 比较运算符 逻辑运算符 赋值运算符 自增/减运算符 三目运算符 位运算符 内容回顾 1.概述 2.基本数据 1.使用方式&#xff08;行内、页面、外部&#xff09; 2.对话框…

不要再使用 @Builder 注解了!有深坑呀!

曾经&#xff0c;我在《千万不要再随便使用 lombok 的 Builder 了&#xff01;》 一文中提到 Builder 注解的其中一个大坑会导致默认值失效&#xff01; 最近阅读了 《Oh !! Stop using Builder》 发现 Builder 的问题还不止一个&#xff0c;Builder 会让人误以为是遵循构建器…

Linux——计算机进程基础知识

计算机基础知识 1.计算机组成五大部件: (1) 运算器 &#xff1a;也叫算数逻辑单元&#xff0c;完成对数据的各种常规运算&#xff0c;如加减乘除&#xff0c;也包括逻辑运算&#xff0c;移位&#xff0c;比较等。 (2) 控制器 &#xff1a; 它是整个计算机系统的控制中心&…

基于LNMP环境上线QQ农场

目录 一.介绍 二. 环境准备 三.安装Mysql数据库 四.安装PHP 五.安装Nginx 六.测试Nginx服务于PHP服务是否能关联 七.项目上线 QQ农场源码&#xff1a;做本项目默认操作者有一定的基础知识与理解能力 链接&#xff1a;https://pan.baidu.com/s/1HF8GZ-yvNh7RbJ61nXOW-g?…

Postman 脚本的奥秘:JavaScript 的内置对象和方法

postman 的前后置脚本中是完全支持 JavaScript 编写代码&#xff0c;JavaScript 有很多内置的对象和方法&#xff0c;可以帮助我们完成各种任务&#xff0c;比如生成随机数和测试响应数据 生成随机数 使用Math.random()方法来生成一个 0 到 1 之间的随机小数&#xff0c;比如…

Python应用|获取矢量数据的多边形个数

import geopandas as gpd #首先导入geopandas包# 读取Shapefile文件 shp_file "E:\python\china" gdf gpd.read_file(shp_file)# 获取多边形个数 num_polygons len(gdf)print("Number of polygons:", num_polygons) 图1|输出结果