秒懂算法 | KMP算法(Java描述)

news2024/10/1 15:22:42

Knuth-Morris-Pratt 算法(简称 KMP)是由高德纳(Donald Ervin Knuth)和沃恩·普拉特在1974年构思,同年詹姆斯·H·莫里斯也独立地设计出该算法,最终三人于1977年联合发表。该算法较Brute-Force算法有较大改进,主要是消除了目标串指针的回溯,从而使算法效率有了某种程度的提高。

01、KMP算法(Java描述)

如何消除了目标串指针的回溯呢?先看一个示例,假设目标串s="aaaaab",模式串t="aaab",看其匹配过程:

(1)当进行第一趟匹配时,失配处为i=3/j=3。尽管本趟匹配失败了,但得到这样的启发信息,s的前3个字符"s0s1s2"与t的前3个字符"t0t1t2"相同,显然"s1s2"="t1t2"是成立的。

(2)从t中观察到"t0t1"="t1t2",这样就有"s1s2"="t1t2"="t0t1"。按照BF算法下一趟匹配应该从s1/t0比较开始,而此时已有"s1s2"="t0t1",没有必要再做重复比较,下一步只需将s3与t2开始比较即做s3/t2的比较,如图1所示。

这种“观察信息”就是失配处为si/tj时,需要找出tj前面有多少个字符与t开头的字符相同。采用一个next数组表示,即next[j]=k表示有"t0t1…tk-1"="tj-ktj-k+1…tj-1"成立,那么如何求next[j]呢?

■ 图1 利用启发信息进行的匹配

考虑失配处模式串t中字符tj前面的子串"t0t1…tj-1",定义其前缀(真前缀更加准确些)为除了自身以外全部头部组合,即以首字符t0开头的除了自身以外的子串。定义其后缀(真后缀更加准确些)为除了自身以外全部尾部组合,即以尾字符tj-1结尾的除了自身以外的子串。定义其中最长的相同前、后缀为M串,则next[j]就是该M串的长度。next[j]=k的含义如图4.9所示,其中前缀和后缀可以重叠。

■ 图2 next[j]=k的含义

例如,t="abcdabd",求next[6]的过程是,t6='d',t中它前面的串是"abcdab"(恰好含6个字符),其前缀有"a","ab","abc","abcd","abcda"(注意前缀不包含自身"abcdab"),其后缀有"b","ab","dab,"cdab","bcdab"(注意后缀不包含自身"abcdab"),前、后缀中相同只有"ab",它就是t6的M串,含2个字符,所以next[6]=2。

在求模式串t中tj的M串时需要注意以下几点:

(1)M串至多从t1开始的,也就是说,j-k≥1,或者k<j。

(2)M串与t中字符位置相关,除了t0外,每个位置都有一个M串(M串可以为空,此时k=0)。

(3)如果tj有多个相同的前、后缀,应该取最大长度的相同前、后缀作为M串。

(4)next[j]=k中k表示M串中的字符个数。

归纳起来,求模式t的next[j](0≤j≤m-1)数组的公式如下:

对于模式串t="abcac",求其next数组的过程如下:

(1)对于序号0,规定next[0]=-1。

(2)对于序号1,置next[1]=0,实际上next[1]总是为0。

(3)对于序号2,t2前面的子串为"ab"(含2个字符),前缀为"a",后缀为"b",对应的M串为空,置next[2]=0。

(4)对于序号3,t3前面的子串为"abc"(含3个字符),前缀为"a"和"ab",后缀为"c"和"bc",对应的M串为空,置next[3]=0。

(5)对于序号4,t4前面的子串为"abca"(含4个字符),前缀为"a"、"ab"和"abc",后缀为"a"、"ca"和"bca",相同的前、后缀只有"a",对应的M串为"a",它只有一个字符,置next[4]=1。

这样模式串t对应的next数组如表1所示。

表1 模式串的next数组值

求模式串t的next数组的算法如下:

public static void GetNext(String t,int nest[])
{
int j = 0,k = -1;
Next[0] = -1;
while(j < t.length() - 1)
{
if(k == 1 ||t.length() - 1)
{
j++;k++;
next[j] = k;
}
else k = next[k];
}

}

上述算法的思路是先置next[0]=-1(为了区分j的不同取值,取值-1表示j=0 的特殊情况),再由next[j]求next[j+1](1≤j≤m-1),初始k置为-1(表示从j=0开始求其他next元素值)。

假设next[j]=k,即有"t0t1…tk-1"="tj-ktj-k+1…tj-1"成立:

(1)若tj=tk,可以推出"t0t1…tk-1tk"="tj-ktj-k+1…tj-1tj"(共k+1个字符)成立,说明字符tj+1的M串的长度为k+1,所以置j++,k++,next[j]=k,如图3所示。

■ 图3 tj=tk的情况

(2)若tj≠tk,则说明tj+1之前不存在长度为next[j]+1(或者k+1)的和t0起匹配的子串。那么是不是必须从k=0开始试探来求next[j+1]呢?

可以这样来提高效率,如果next[k]=k'(k'<k),说明字符tk有一个长度为k'的M前缀,若tj=tk',那么就有next[j+1]=next[k']+1,推导过程如图4.11所示。当然,若tj≠tk',需要置k'=next[k']继续做下去,最多到k'=-1为止,这时的结果是next[j+1]=k'+1=0。

■ 图4 tj≠tk的情况

由这两种情况分析可知,GetNext()算法的时间复杂度为O(m),m为t的长度。

例如,t="aaaabaaaaabc",按照GetNext算法求出nextt[0..8]如表2所示,现在由next[9]求next[10]。此时next[9]=4,j=9,k=4,由于t[9]≠t[4],置k=next[k]=3,而t[9]=t[3]成立,所以执行j++,k++,next[10]=next[4]+1=4,如图5所示。从中看出不需要从k=0开始,而是从k=next[k]的位置开始比较效率更高。

表2 模式串的next数组部分值

■ 图5 求next[10]

下面讨论KMP算法的一般情形,设目标串s="s0s1…sn-1",模式串t="t0t1…tm-1",在进行一趟匹配(该趟以si-j/t0开始比较的)时,出现如图6所示的情况。

■ 图6 主串和模式串匹配的一般情况

此时失配处为si/tj,显然有"t0t1…tj-1"="si-jsi-j+1…si-1",假设next[j]=k(k<j),考虑k的各种情况:

(1)若next[j]≠j-1,即有"t0t1…tj-2"≠"t1t2…tj-1"(含j-1个字符,若相等则next[j]=j-1),则回溯到si-j+1开始与t匹配必然“失配”,理由很简单,由这两个式子可知一定有"t0t1…tj-2"≠"si-j+1si-j+2…si-1"(含j-1个字符),既然如此,回溯到si-j+1开始与t匹配可以不做,如图7所示。简单地说,若next[j]≠j-1,则按BF算法做下一趟即si-j+1/t0开始的比较是不必要的(因为这一趟一定失败)。

■ 图7 说明回溯到si-j+1是没有必要的

(2)若next[j]≠j-2,即有"t0t1…tj-3"≠"t2t3…tj-1"(含j-2个字符,若相等则next[j]=j-2),则回溯到si-j+2开始与t匹配必然“失配”,因为有"t2…tj-1"="si-j+2…si-1",很容易推出"t0t1…tj-3"≠"si-j+2…si-1"。简单地说,若next[j]≠j-2,则按BF算法做再下一趟即si-j+2/t0开始的比较的这一趟是不必要的。

(3)以此类推,直到对于某一个值k,有next[j]=k成立,也就是说有"t0t1…tk-1"="tj-ktj-k+1…tj-1"(含k个字符,多于k个字符时不成立),这样有"tj-ktj-k+1…tj-1"="si-ksi-k+1…si-1"="t0t1…tk-1",说明下一次可直接比较si和tk,这样可以直接把当前趟匹配“失配”时的模式t从当前位置直接右滑j-k位。而这里的k即为next[j],如图8所示。然后继续做下去。

■ 图8 模式串右滑j-k位

从中看出,与BF算法相比,KMP算法不仅仅减少了一趟中字符比较次数,还可能会减少匹配的趟数。如果说BF算法是i=0,1,2,…,连续匹配的,而KMP算法是跳跃的匹配,上述过程也证明这些跳过的趟是不必要的匹配。

综上,KMP算法的过程是,设s为目标串,t为模式串,并设i指针和j分别指示目标串和模式串中正待比较的字符(i和j的均从0开始)。

(1)若有si=tj,则i和j分别增1。

(2)否则,失配处为si/tj,i不变,j退回到j=next[j]的位置(即模式串右滑),再比较si和tj,若相等则i、j各增1,否则j再次退回到下一个j=next[j]的位置,依次类推,直到出现下列两种情况之一:一种情况是j退回到某个j=next[j]位置时有si=tj,则指针各增1后继续匹配;另一种情况是j退回到j=-1时,此时令i、j指针各增1,即下一次比较si+1和t0。

简单地说,KMP算法利用已经部分匹配的有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置。

对应的KMP算法如下:

public static int KMP(String s,String t)
{
int [] next=new intMaxSize];
int i=0,j=0;
GetNext(t,next);
while (i<s.length()&& j<t.length()
{
if(j=--1| s.charAt(i)--t.charAt(i);
{
i++;
j++;
}
else j = next[j];
}
if(j>=t.length()) return(i-t.length();
else return(-1);
}

设目标串s的长度为n,模式串t长度为m,在KMP算法中求next数组的时间复杂度为O(m),在后面的匹配中因主串s的下标i不减即不回溯,比较次数可记为n,所以KMP算法总的时间复杂度为O(n+m)。

【例1】设主串s="ababcabcacbab",模式串t="abcac"。给出KMP进行模式匹配的过程。

解:模式串对应的next数组如表4.1所示,其采用KMP算法的模式匹配过程如图4.16所示。首先i=0,j=0,匹配到i=2/j=2失败为止。i值不变(不回溯到前面),修改j=next[j]=0,匹配到i=6/j=1失败为止。i值不变(不回溯到前面),修改j=next[j]=1,匹配到i=10/j=5(t的字符比较完),返回i-t.length=5,表示t是s的子串,且位置为5。

图9 KMP算法的模式匹配过程

上述next数组在某些情况下尚有缺陷。例如,设主串s="aaabaaaab",模式串t="aaaab"。t对应的next数组如表3所示

表3模式串t的next数组值

两串匹配的过程如图10所示,从中看到,当i=3/j=3时,s3≠t3,由next[j]的指示还需进行i=3/j=2,i=3/j=1,i=3/j=0等3次比较。实际上,因为模式中的第1、2、3个字符和第4个字符都相等,因此,不需要再和主串中第4个字符相比较,而可以将模式一次向右滑动4个字符的位置直接进行i=4/j=0时的字符比较。

■ 图10 KMP算法的模式匹配过程

上述示例中存在的问题可以通过改进next数组得到解决,将next数组改为nextval数组,与next[0]一样,先置nextval[0]=-1。假设求出next[j]=k,现在失配处为si/tj,即si≠tj,

(1)如果有tj=tk成立,可以直接推出si≠tk成立,没有必要再做si/tk的比较,直接置nextval[j]=nextval[k]](nextval[next[j]]),即下一步做si/tnextval[j]的比较。

(2)如果有tj≠tk,没有改进的,置nextval[j]=next[j]。

改进后的求nextval数组的算法如下:

public static void GetNextval(String t,int []nextval)
{
intj=0,k=-1;
nextval[0]=-l;
while (j<t.length()-I)
{
if(k==-1||t.charAt(j)==t.charAt(k))
{
j++;k++;
if(t.charAt(j)!=t.charAt(k))nextval[j]=k;
else
nextvallj]=nextval[k];
}
}
}

改进后的KMP算法如下:

public static int KMP1(String s,String t)
{
int [] nextval=new intMaxSize];
int i=0,j=0;
GetNextval(t,nextval);
while (i<s.length() && j<t,length())
{
if(j==-l] s.charAt(i)==t.charAt(j))
{
i++;
j++;
}
else j = nextval[j];
}
if(j >= t.length())
return(i-t,length());
else return(-1);
}

与改进前的KMP算法一样,本算法的时间复杂度也为O(n+m)。

【例2】设目标串为s="abcaabbabcabaacbacba",模式串t="abcabaa"。计算模式串t的nextval函数值。并画出利用KMP算法进行模式匹配时每一趟的匹配过程。

解:模式串t的nextval函数值如表4所示。

表4 模式串t的nextval函数值

利用KMP算法的匹配过程如图11所示,从中看到匹配效率得到进一步的提高。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

门电路OD门

漏极开路输出的门电路&#xff08;OD门&#xff09; 为了满足输出电平的变换&#xff0c;输出大负载电流&#xff0c;以及实现“线与”功能&#xff0c;将CMOS门电路的输出级做成漏极开路的形式&#xff0c;称为漏极开路输出的门电路&#xff0c;简称OD&#xff08;Open&#x…

【JVM】1. JVM与Java体系结构

文章目录 1.1. 前言&#x1f349;1.2. 参考书目&#x1f349;1.3. Java及JVM简介&#x1f349;1.4. Java发展的重大事件&#x1f349;1.5. 虚拟机与Java虚拟机&#x1f349;1.6. JVM的整体结构&#x1f349;1.7. Java代码执行流程&#x1f349;1.8. JVM的架构模型&#x1f349;…

4. QT中的鼠标键盘事件 --- 鼠标拖拽案例

1. 说明 在QT的控件或者窗口当中&#xff0c;如果对于当前鼠标或者键盘的功能需要自己定义&#xff0c;可以重写父类当中对应虚函数&#xff0c;主要包括以下几个&#xff1a; //键盘按键按下 virtual void keyPressEvent(QKeyEvent *event); //键盘按键抬起 virtual void ke…

为什么C++这么复杂还不被淘汰?

C是一门广泛使用的编程语言&#xff0c;主要用于系统和应用程序的开发。尽管C具有一些复杂的语法和概念&#xff0c;但它仍然是编程界的重量级选手&#xff0c;在编程语言排行榜中一直位居前列。为什么C这么复杂还不被淘汰呢&#xff1f; C有以下优势 1、C具有高性能 C是一门编…

unity进阶学习笔记:photonServer测试

photonServer是由photon发布的一个网络框架&#xff0c;其封装了UDP和TCP通信机制让用户可以直接调用API实现网络游戏通信 1 photonServer下载安装 进入Photon官网的SDK选项&#xff0c;选择下载Server。目前Server版本已经更新到v5&#xff0c;这里我为了和教程保持一致下载…

Unittest接口测试生成报告和日志方法

HTML报告 直接把HTMLTestRunner.py放入工程目录即可报告脚本封装 #HTNL格式报告now datetime.datetime.now().strftime(%Y-%m-%d_%H_%M_%S)htmlreport reportpath "/" now r"result.html"print("测试报告生成地址&#xff1a;%s"% htmlre…

Revit干货 | 系统族、内建族、可载入族一次性搞清楚!

对于使用人数较多的revit软件&#xff0c;其中的许多概念与我们常用的CAD完全不同&#xff0c;以至于让许多工程师觉得revit软件有点高深莫测&#xff0c;不可琢磨&#xff0c;从而有了抗拒心理。 Revit软件中的重要概念&#xff1a; “族”是revit软件中的很重要也很基本的概念…

程序的各种段以及堆栈相关问题

C中一般有三种变量&#xff1a; 局部变量全局变量静态变量 C中一般有五个内存段&#xff1a; 代码段 也叫文本段&#xff0c;包含frequently executed code通常是只读的&#xff08;未了避免程序被错误改写&#xff09;代码段是不包含程序变量&#xff08;如局部变量、全局变…

【数据结构】带头双向链表,真正的六边形战士

文章目录 概要整体架构流程小结 概要 *数据结构中的链表在实际开发中应用非常广泛&#xff0c;但写一个链表并不是一件简单的事情。链表有八种结构&#xff0c;对于刚入门的新手来说&#xff0c;往往会先经历单链表的折磨。 而今天我要讲的带头双向链表非常适合新手学习&#…

C++6.类和对象(下)

1.友元函数&#xff0c;一般现在类的最上面。 2.函数的参数中&#xff0c;如果不改变&#xff0c;尽量加上const修饰。 3.对于自定义类型&#xff0c;使用运算符进行操作基本上都要用运算符重载&#xff0c;但是有些运算符重载会默认自动生成。 4.初始化列表&#xff0c;是成…

【wpf】列表类,用相对源时,如何绑定到子项

前言 在之前的一篇文章 &#xff1a;《【wpf】深度解析&#xff0c;Binding是如何寻找数据源的》https://blog.csdn.net/songhuangong123/article/details/126195727#:~:text%E3%80%90wpf%E3%80%91%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90%EF%BC%8CBinding%E6%98%AF%E5%A6%82%E4…

读书|林曦:她把自己的生活,过成了无用但丰盈的美学

时代在以加速度的方式变化&#xff0c;让人难以从容。而当我们陷于横向的比较系统&#xff0c;权衡着卷、躺、润时&#xff0c;也有人在探寻另一条纵向的路——向古人学习&#xff0c;以传统美学关照和滋养当下生活。      立夏之际&#xff0c;水墨画家林曦的新作《无用之…

33岁跳槽无路,濒临绝望之际受贵人指点,成功上岸阿里(测试岗)

写在前面 马上过34岁生日了&#xff0c;和大家聊聊最近的情况&#xff0c;半年前还在迷茫该学什么&#xff0c;怎样才能走出现在的困境&#xff0c;半年后已经成功上岸阿里&#xff0c;感谢在这期间帮助我的每一个人 开始 30多岁工作的时候总是有种力不从心的感觉&#xff0…

网络安全工程师需要考什么证吗?

目前网络安全行业&#xff0c;国内都有哪些证书可以考。 一、CISP-PTE &#xff08;国家注册渗透测试工程师&#xff09; CISP-PTE即注册信息安全渗透测试工程师&#xff0c;该证书由中国信息安全测评中心颁发&#xff0c;证书是国内唯一认可的渗透测试认证&#xff0c;专业性…

AIOps社区低代码项目火热报名中|中科院开源之夏2023,万元现金等你来拿!

活动介绍 开源之夏是由中科院软件所“开源软件供应链点亮计划”发起并长期支持的一项暑期开源活动&#xff0c;旨在鼓励在校学生积极参与开源软件的开发维护&#xff0c;培养和发掘更多优秀的开发者&#xff0c;促进优秀开源软件社区的蓬勃发展&#xff0c;助力开源软件供应链…

告别Excel,免费大数据分析与可视化工具,让你的论文图表“高大上”

数据分析工具很多&#xff0c;可以分为表格、数据库、BI工具、编程等四大工具。每个大类又有很多的工具&#xff0c;例如表格包括Excel、WPS、Google Sheets、Airtable等。编程工具包括Python和R。 搞科研几年了&#xff0c;笔者一直都是在使用Excel做数据分析和可视化&#xf…

Go语言中sync.Map、sync.Pool和Context的用法

目录 【sync.Map】 实现线程安全的 map 类型 使用 sync.Map 实现并发读写的map 【sync.Pool】 使用 带缓冲channel 实现对象池 使用 sync.Pool 创建临时对象池 【Context 上下文】 Context应用&#xff1a;实现带超时功能的远程调用 Context应用&#xff1a;监控指令…

电脑找不到MSVCR120.dll怎么办/MSVCR120.dll是什么?

电脑提示找不到MSVCR120.dll怎么办&#xff1f;你是否也遇到这个问题&#xff0c;MSVCR120.dll是什么文件呢&#xff1f;小编今天就把MSVCR120.dll文件丢失的修复方法给大家。msvcr120.dll是Microsoft Visual C 的一部分&#xff0c;它是Windows操作系统中的一个动态链接库文件…

计算卸载-论文05-双层优化(无线充电与卸载)

标题&#xff1a;《A Divide-and-Conquer Bilevel Optimization Algorithm for Jointly Pricing Computing Resources and Energy in Wireless Powered MEC》 期刊&#xff1a;IEEE TRANSACTIONS ON CYBERNETICS&#xff0c;2022 一、理论梳理 问题&#xff1a;相比于移动云…

基于Python长时间序列遥感数据处理及在全球变化、物候提取、植被变绿与固碳分析、生物量估算与趋势分析等领域中的应用

植被是陆地生态系统中最重要的组分之一&#xff0c;也是对气候变化最敏感的组分&#xff0c;其在全球变化过程中起着重要作用&#xff0c;能够指示自然环境中的大气、水、土壤等成分的变化&#xff0c;其年际和季节性变化可以作为地球气候变化的重要指标。此外&#xff0c;由于…