字符串匹配的殿堂级算法:KMP算法详解(Java实现版)

2023-12-29 03:20

本文主要是介绍字符串匹配的殿堂级算法:KMP算法详解(Java实现版),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

KMP的原理

模拟过程1

模拟过程2

KMP算法正确性的简单证明

什么是LPS数组

计算LPS数组

java实现LPS数组

leetcode1392题.最长快乐前缀

java实现KMP算法


期末考的小高峰结束咯,我又来写博客啦。今天带来的是历史上第一个线性的字符串匹配算法——KMP算法。

KMP的原理

先举两个字符串进行KMP匹配中的例子。

第一个:

第二个:

上面是两个字串t在与各自的源字符串s匹配过程中t的移动跳跃轨迹,有没有看出什么规律?没有吗,别着急,我再给你上个颜色。

第一个: 

第二个:

是不是有点意思了?第一行源字符串s中,第二个被紫色标记的子串是已匹配部分中和t的部分前缀一样的字串,我们下一次进行匹配时就可以跳过这个子串从下一个字符开始匹配。第一个例子中我们字符A就不用匹配了,第二个例子中我们AB这个字符就不用匹配了,我们都直接从这个子串的后一个字符开始继续去尝试匹配。 

现在我们只是把这个规律给找出来,至于为什么要这样做,为什么这样是正确的我们稍后再讲。我们先给一个定义,这种既是前缀也是后缀的子串我们叫border(非自身性,非空性)。

模拟过程1

我们来模拟一下这个匹配的过程。s串中用i来代表索引的移动,t串中用j来代表索引的移动。

当我们到了s串中E的位置时,我们发现和t串不匹配了,我们就开始寻找s串中已匹配部分中border的位置,即寻找那个既是前缀也是后缀的字串。 

我们发现在这个字符串中,border有两种,A或者AA,这种情况下我们关心的是最长的那个。于是我们要从AA的下一个位置继续进行匹配。

我们为什么要关心最长的那个border呢?我们来想象一下,如果我们关心短的那个border的话,我们t串跳的位置就如下图所示。

 

我们发现它比我们AA作为border跳的要靠后, 这种情况下我们就有可能漏掉解,因为我们跳过了一些字符没进行匹配,考虑最长的border保证了重合的部分尽可能长,保证不漏掉解。这一点我们下面还会进一步解释。

模拟过程2

我们发现这个字符串也有两个border,AB和ABAB。如刚才所说,我们关注最长的ABAB。

 

 

这个模拟主要是想说明在已匹配的部分ABAB有重叠没关系,只要保证既是前缀也是后缀就好了。 如果没太看懂什么叫重叠没关系,下文的简单证明中有进一步说明。

为了实现KMP算法,我们就需要弄明白如何计算出最长的border,我们先来证明一下为什么这样做是正确的。

KMP算法正确性的简单证明

首先是需要在s中找到和t完美匹配的字串。 

 紫色表示匹配成功的部分,红色表示匹配失败的部分。

找到匹配成功的紫色部分中最长的border,标为黄色部分。为了表示清晰,我们这里两个黄色的部分是不重叠的,有一小段紫色把它们隔开了,即使是重叠的也没关系,上文的模拟过程2已经讲过了。

 直接从红色的字符继续匹配。

为什么考虑最长的border是向前移动最少的方案?

我们需要证明最长的border使匹配不会漏掉解,让我们用反证法假设在更早的地方能匹配到解。

假设我们在已知的最长的黄色border前的绿色位置就匹配成功了 ,从绿色部分开始出发往后都匹配成功了。

把我们t串的黄色部分往后延展一下,反正都匹配成功了。 

 把s串开头部分对应上绿色,这时候我们发现我们找到了一个由绿色和黄色组成的更长的border,这和我们的前提黄色是最长的border矛盾了,由此得证。

什么是LPS数组

s和t在匹配过程中,前面几个前缀都匹配成功,在红色部分匹配失败。

我们需要计算t的每一个前缀的最长的border,所以我们计算的不是一个值,而是一组值。我们把这些值存在LPS数组中,LPS(Longest proper Prefix which is also Suffix)的中文含义就是非空的、不是自身的、最长的、既是前缀也是后缀的子串。

我们来具体的举个例子看看LPS数组。

当只有一个字符A时,它无法充当自己的border,所以LPS数组对应的值为0。来到B,B这里存储的是AB这个前缀的border,从A开始看,A != B,再往后走就又是AB这个子串本身了,所以存储的值也是0。

到了下一个A,ABA对应的最长的既是前缀也是后缀的border为A,长度是1,对应的LPS的值也就是1。

到了ABAB这个前缀,最长的border为AB,长度为2,对应的LPS值为2。 

 

到了ABABA这个前缀,最长的border为ABA,长度是3,对应的LPS数组值就是3。 

 

以此类推计算t的每一个前缀中最长的border,我们给出最终的结果。

有了LPS这个数组存储每一个前缀中最长的border,我们在让s和t匹配的时候,不管前缀是多少的时候匹配失败的,我们都可以直接快速的找到下一次比较的位置。

什么是LPS数组

s和t在匹配过程中,前面几个前缀都匹配成功,在红色部分匹配失败。

我们需要计算t的每一个前缀的最长的border,所以我们计算的不是一个值,而是一组值。我们把这些值存在LPS数组中,LPS(Longest proper Prefix which is also Suffix)的中文含义就是非空的、不是自身的、最长的、既是前缀也是后缀的子串。

我们来具体的举个例子看看LPS数组。

当只有一个字符A时,它无法充当自己的border,所以LPS数组对应的值为0。来到B,B这里存储的是AB这个前缀的border,从A开始看,A != B,再往后走就又是AB这个子串本身了,所以存储的值也是0。

到了下一个A,ABA对应的最长的既是前缀也是后缀的border为A,长度是1,对应的LPS的值也就是1。

到了ABAB这个前缀,最长的border为AB,长度为2,对应的LPS值为2。 

 

到了ABABA这个前缀,最长的border为ABA,长度是3,对应的LPS数组值就是3。 

 

以此类推计算t的每一个前缀中最长的border,我们给出最终的结果。

有了LPS这个数组存储每一个前缀中最长的border,我们在让s和t匹配的时候,不管前缀是多少的时候匹配失败的,我们都可以直接快速的找到下一次比较的位置。

计算LPS数组

LPS[i - 1] = a意思是i前面的前缀中(也就是从0到i - 1索引的位置这段前缀)最长的border的长度为a,LPS[0]的值肯定为0,因为我们对border的定义就说了border不能是前缀本身。我们现在希望能通过LPS[i - 1]的值推出LPS[i]的值,也就是从LPS[0]开始递推出LPS[1] 、LPS[2].....

黄色子串表示已匹配的前缀中最长的border,t[a]是第一个黄色子串的下一个字符(因为黄色子串长度为a,而数组的索引从0开始),t[i]是第二个黄色子串的下一个字符,如果二者相等就递推出了LPS[i] 的值(也就是索引从0到i这段前缀中的border值)为 a + 1。LPS[i] 比 LPS[i - 1]最多大1。

那如果二者不相等呢?LPS[i]就是0吗?

不是的,我们来举个例子。

索引所在的B前面的前缀ABACABA的LPS值为3,由刚才的递推方法,我们发现ABA这个值为3的border并不是B所在的前缀ABACABAB的border,我们这个最长的border匹配失败了,我们就去看次长的border,看次次长的border....很显然除了ABA这个长度为3的border,还有AB这个长度为2的border和B所在的前缀匹配成功了,于是我们这个前缀的最长的border为2。

也就是最长的border的t[i]和t[a]匹配失败后我们就看次长的border,假设我们次长的border为下图绿色部分,如果这个次长的border匹配成功的话,那么这个border就是索引所在的前缀的最长的border。绿色的部分是黄色部分的前缀也是后缀,也是黄色部分最长的,那么a = LPS[a - 1]看t[a],不断地更新a的值。

java实现LPS数组

leetcode1392题.最长快乐前缀

1392. 最长快乐前缀 - 力扣(LeetCode) 

「快乐前缀」 是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。

给你一个字符串 s,请你返回它的 最长快乐前缀。如果不存在满足题意的前缀,则返回一个空字符串 "" 。

示例 1:

输入:s = "level"
输出:"l"
解释:不包括 s 自己,一共有 4 个前缀("l", "le", "lev", "leve")和 4 个后缀("l", "el", "vel", "evel")。最长的既是前缀也是后缀的字符串是 "l" 。

示例 2:

输入:s = "ababab"
输出:"abab"
解释:"abab" 是最长的既是前缀也是后缀的字符串。题目允许前后缀在原字符串中重叠。提示:
  • 1 <= s.length <= 105
  • s 只含有小写英文字母
class Solution {public String longestPrefix(String s) {int []lps = getLPS(s);int len = lps[s.length() - 1];//java的substring是前闭后开的return s.substring(0, len);}private int[]getLPS(String t){int [] lps = new int[t.length()];//lps每个值怎么求for(int i = 1; i < t.length(); i++){int a = lps[i - 1];while(a > 0 && t.charAt(i) != t.charAt(a)){a =lps[a - 1];}if(t.charAt(i) == t.charAt(a)){lps[i] = a + 1;}}return lps;}
}

java实现KMP算法

public class KMP {public static int kmp(String s, String t){if(s.length() < t.length()) return -1;if(t.length() == 0) return 0;int []lps = getLPS(t);int i = 0, j = 0;while(i < s.length()){if(s.charAt(i) == t.charAt(j)){i++;j++;if(j == t.length()){return i - t.length();}}else if(j > 0){j = lps[j - 1];}else {i++;}}return -1;}private static int[]getLPS(String t){int [] lps = new int[t.length()];//lps每个值怎么求for(int i = 1; i < t.length(); i++){int a = lps[i - 1];while(a > 0 && t.charAt(i) != t.charAt(a)){a =lps[a - 1];}if(t.charAt(i) == t.charAt(a)){lps[i] = a + 1;}}return lps;}public static void main(String[]args){String s = "ABABDABACDABABCABAB";String t = "ABABCABAB";int result = kmp(s, t);System.out.println("The index of the first occurrence of the pattern is: " + result);}
}

这篇关于字符串匹配的殿堂级算法:KMP算法详解(Java实现版)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/548191

相关文章

Docker镜像修改hosts及dockerfile修改hosts文件的实现方式

《Docker镜像修改hosts及dockerfile修改hosts文件的实现方式》:本文主要介绍Docker镜像修改hosts及dockerfile修改hosts文件的实现方式,具有很好的参考价... 目录docker镜像修改hosts及dockerfile修改hosts文件准备 dockerfile 文

Java利用docx4j+Freemarker生成word文档

《Java利用docx4j+Freemarker生成word文档》这篇文章主要为大家详细介绍了Java如何利用docx4j+Freemarker生成word文档,文中的示例代码讲解详细,感兴趣的小伙伴... 目录技术方案maven依赖创建模板文件实现代码技术方案Java 1.8 + docx4j + Fr

SpringBoot首笔交易慢问题排查与优化方案

《SpringBoot首笔交易慢问题排查与优化方案》在我们的微服务项目中,遇到这样的问题:应用启动后,第一笔交易响应耗时高达4、5秒,而后续请求均能在毫秒级完成,这不仅触发监控告警,也极大影响了用户体... 目录问题背景排查步骤1. 日志分析2. 性能工具定位优化方案:提前预热各种资源1. Flowable

CSS will-change 属性示例详解

《CSSwill-change属性示例详解》will-change是一个CSS属性,用于告诉浏览器某个元素在未来可能会发生哪些变化,本文给大家介绍CSSwill-change属性详解,感... will-change 是一个 css 属性,用于告诉浏览器某个元素在未来可能会发生哪些变化。这可以帮助浏览器优化

Python基础文件操作方法超详细讲解(详解版)

《Python基础文件操作方法超详细讲解(详解版)》文件就是操作系统为用户或应用程序提供的一个读写硬盘的虚拟单位,文件的核心操作就是读和写,:本文主要介绍Python基础文件操作方法超详细讲解的相... 目录一、文件操作1. 文件打开与关闭1.1 打开文件1.2 关闭文件2. 访问模式及说明二、文件读写1.

基于SpringBoot+Mybatis实现Mysql分表

《基于SpringBoot+Mybatis实现Mysql分表》这篇文章主要为大家详细介绍了基于SpringBoot+Mybatis实现Mysql分表的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录基本思路定义注解创建ThreadLocal创建拦截器业务处理基本思路1.根据创建时间字段按年进

详解C++中类的大小决定因数

《详解C++中类的大小决定因数》类的大小受多个因素影响,主要包括成员变量、对齐方式、继承关系、虚函数表等,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录1. 非静态数据成员示例:2. 数据对齐(Padding)示例:3. 虚函数(vtable 指针)示例:4. 继承普通继承虚继承5.

前端高级CSS用法示例详解

《前端高级CSS用法示例详解》在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交互和动态效果的关键技术之一,随着前端技术的不断发展,CSS的用法也日益丰富和高级,本文将深... 前端高级css用法在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将