文章目录

  • 学习资源
  • 什么是KMP
  • 什么是前缀表
  • 为什么一定要用前缀表
  • 如何计算前缀表
  • 前缀表有什么问题
  • 使用next数组来匹配
  • 放码过来
    • 构造next数组
      • 一、初始化
      • 二、处理前后缀不相同的情况
      • 三、处理前后缀相同的情况
    • 使用next数组来做匹配
    • 代码总览
    • 测试代码
  • 时间复杂度分析

学习资源

  1. 字符串:KMP是时候上场了(一文读懂系列)- 代码随想录
  2. 字符串:都来看看KMP的看家本领!- 代码随想录

什么是KMP

KMP算法是由这三位学者发明的:Knuth,Morris和Pratt,因此,用这三位学者名字的首字母组合成,来命名该算法。

KMP主要应用在字符串匹配上。KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。

什么是前缀表

next数组就是一个前缀表(prefix table)。

前缀表是用来回溯的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

为了清楚的了解前缀表的来历,举一个例子:

要在文本串:aabaabaafa中查找是否出现过一个模式串:aabaaf

如动画所示:

动画里,特意把 子串aa 标记上了,这是有原因的,大家先注意一下,后面还会说道。

可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。

但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

此时就要问了前缀表是如何记录的呢?

首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。(MyNote:文本串不用跳转)

那么什么是前缀表:下表i之前(包括i)的字符串中,有多大长度的相同前缀后缀

(MyNote:本文“下表”的通假于“下标”。)

为什么一定要用前缀表

前缀表那为啥就能告诉我们 上次匹配的位置,并跳过去呢?

回顾一下,刚刚匹配的过程在下表5的地方遇到不匹配,模式串是指向f,如图:

然后就找到了下表2,指向b,继续匹配,如图:

以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!

下表5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。

所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。

如何计算前缀表

接下来就要说一说怎么计算前缀表。如图:

一、长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意这里计算相同前后缀,不算重复的字符)

二、长度为前2个字符的子串aa,最长相同前后缀的长度为1。

三、长度为前3个字符的子串aab,最长相同前后缀的长度为0。

以此类推:

四、长度为前4个字符的子串aaba,最长相同前后缀的长度为1。

五、长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。

六、长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:

可以看出前缀表里的数值代表着就是:当前位置之前的子串有多大长度相同的前缀后缀

再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:

找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。

为什么要看前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。

所以要看前一位的 前缀表的数值。

前一个字符的前缀表的数值是2, 所有把下表移动到下表2的位置继续比配。可以再反复看一下上面的动画。

最后就在文本串中找到了和模式串匹配的子串了。

前缀表有什么问题

来看一下刚刚求的这个前缀表有什么问题呢?

看这个位置红框的位置,如果要找下表1 所对应 前缀表里的数值的时候,前缀表里的数值依然是1,然后就要跳到下表1的位置,如此就形成了一个死循环

**如何怎么避免呢,就把前缀表里的数值统一减一, 开始位置设置为-1 **。 这一点对理解后面KMP代码很重要!!

改为如图所示:

这样就避免的死循环,只不过后续取 前缀表里的数值的时候,要记得再+1,才是我们想要的值。

最后得到的新前缀表在KMP算法里通常用一个next数组来表示。

注意这个next数组就根据模式串求取的。

使用next数组来匹配

有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。

注意next数组是新前缀表(旧前缀表统一减一了)。

匹配过程动画如下:

放码过来

下文统称haystack为文本串, needle为模式串。

haystack, needle出处。

构造next数组

定义一个方法getNext来构建next数组,参数为一个名为next数组,和一个字符串。代码如下:

private void getNext(int[] next, String s) {}

构造next数组其实就是计算模式串s,前缀表的过程。主要有如下三步:

  1. 初始化
  2. 处理前后缀不相同的情况
  3. 处理前后缀相同的情况

一、初始化

定义两个指针i和j:

  • j指向前缀终止位置(严格来说是终止位置减一的位置),
  • i指向后缀终止位置(与j同理)。

(通常是先i后j,为什么这里相反,接下来看代码就清楚了。)

然后还要对next数组进行初始化赋值,如下:

int j = -1;
next[0] = j;
  • j 初始化为 -1原因是前文说过前缀表要统一减一的操作(避免死循环得情况),所以j初始化为-1。

  • next[] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j),next[0]初始化为j 。

二、处理前后缀不相同的情况

因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。(这里可能一开始不适应理解,不用急。)

所以遍历模式串s的循环下表i 要从 1开始,代码如下:

for(int i = 1; i < s.length(); i++) { // 注意i从1开始

如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要回退。

如何回退?next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。

那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。

所以,处理前后缀不相同的情况代码如下:

while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后缀不相同了j = next[j]; // 回退
}

三、处理前后缀相同的情况

如果s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。

代码如下:

if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后缀j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]

最后整体构建next数组的函数代码如下:

private void getNext(int[] next, String s) {int j = -1;next[0] = j;for(int i = 1; i < s.length(); i++) { // 注意i从1开始while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后缀不相同了j = next[j]; // 向前回溯}if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后缀j++;}next[i] = j; // 将j(前缀的长度)赋给next[i]}
}

代码构造next数组的逻辑流程动画如下:

得到了next数组之后,就开始用它做匹配。

使用next数组来做匹配

在文本串haystack里找是否出现过模式串needle。定义两个下表j 指向模式串起始位置,i指向文本串其实位置。

那么j初始值依然为-1,这是因为next数组里记录的起始位置为-1

i就从0开始,遍历文本串,代码如下:

for (int i = 0; i < haystack.length(); i++) { // 注意i就从0开始

接下来就是 haystack.charAt(i) 与 needle.charAt(j + 1) (因为j从-1开始的) 进行比较。

如果 haystack.charAt(i) 与 needle.charAt(j + 1) 不相同,j就要从next数组里寻找下一个匹配的位置。

代码如下:

while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) { // 不匹配j = next[j]; // j 寻找之前匹配的位置
}

如果 haystack.charAt(i) 与 needle.charAt(j + 1) 相同,那么i 和 j 同时向后移动, 代码如下:

if (haystack.charAt(i) == needle.charAt(j + 1)) { // 匹配,j和i同时向后移动 j++;
}

如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。

本题要在文本串字符串中找出模式串出现的第一个位置(从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。

代码如下:

if (j == (needle.length() - 1) ) { // 文本串s里出现了模式串treturn (i - needle.length() + 1);
}

代码总览

public class KMP {private void getNext(int[] next, String s) {int j = -1;next[0] = j;for(int i = 1; i < s.length(); i++) { // 注意i从1开始while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后缀不相同了j = next[j]; // 向前回溯}if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后缀j++;}next[i] = j; // 将j(前缀的长度)赋给next[i]}}public int strStr(String haystack, String needle) {if (needle.length() == 0) {return 0;}int[] next = new int[needle.length()];getNext(next, needle);int j = -1; // // 因为next数组里记录的起始位置为-1for (int i = 0; i < haystack.length(); i++) { // 注意i就从0开始while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) { // 不匹配j = next[j]; // j 寻找之前匹配的位置}if (haystack.charAt(i) == needle.charAt(j + 1)) { // 匹配,j和i同时向后移动 j++; }if (j == (needle.length() - 1) ) { // 文本串s里出现了模式串treturn (i - needle.length() + 1); }}return -1;}
}

测试代码

import static org.junit.Assert.*;import org.junit.Test;public class KMPTest {@Testpublic void test() {KMP k = new KMP();assertEquals(2, k.strStr("hello", "ll"));assertEquals(-1, k.strStr("aaaaa", "bba"));assertEquals(3, k.strStr("aabaabaafa", "aabaaf"));}}

时间复杂度分析

假设文本串长度为n,模式串长度为m。因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),但之前还要单独生成next数组,时间复杂度是O(m),所以整个KMP算法的时间复杂度是O(n+m)的。

暴力的解法显而易见是O(n * m),所以KMP在字符串匹配中极大的提高的搜索的效率

KMP子字符串匹配算法学习笔记相关推荐

  1. kmp 模式匹配算法学习笔记

    模式匹配算法,就是给出两个字符串,找出长串中首次出现完全与短串相同字符串的位置.这里将长称成为主串,设为S,短串称模式串,设为T. 一般的模式匹配可以直接用暴力破解,将S串与T串一位位地进行匹配,一旦 ...

  2. Java字符串类学习笔记

    String String特性 String:字符串,使用一对""引起来表示. String声明为final的,不可被继承 String实现了Serializable接口:表示字符 ...

  3. python 正则表达式判断字符串_python学习笔记|字符串与正则表达式

    总结一些字符串常用操作以及正则表达式相关 字符串基本操作 获取字符串长度 len(text) 字符串分割 word = text.split(" ") len(word) # 单词 ...

  4. [转载] python numpy 子数组_Python学习笔记3:Numpy入门

    参考链接: Python中的numpy.logaddexp2 参考<Python:数据科学手册>一书,仅作个人学习及记录使用,若有侵权,请联系后台删除. 1 理解Python中的数据类型 ...

  5. python定义变量字符串_Python学习笔记二(变量和字符串)

    2017年年终确定的从2018年开始学习一门新的语言.随着机器学习人工智能的日渐深入,是时候有必要掌握以下Python了.博客今天更新第二篇学习记录,关于Python的变量和变量中字符串的使用,所有学 ...

  6. 我的子平真诠学习笔记

    子平真诠 一.心得: https://blog.csdn.net/humors221/article/details/113913548 二.释疑笔记: (一)​ https://blog.csdn. ...

  7. python自定义函数复制字符串_Python学习笔记摘要(一)类型 字符串 函数 列表 深浅拷贝...

    python中的对象和类型 在python中,认为系统中的每一个"东西"都是一个对象,在python中,"对象"有着特殊的意义,python中的对象有: 一个标 ...

  8. angularjs1访问子组件_Vue学习笔记之组件的应用

    Vue组件的应用: 1.基础使用:第一步创建组件,第二步注册组件,第三步使用组件.在注册组件是需要用到template的属性. 全局组件和局部组件 组件的嵌套(父子组件):注意先后顺序,先声明,后面才 ...

  9. 四、MySQL分页查询 + 子查询复习 学习笔记 (复习连接查询相关内容 详解)

    8:分页查询 应用场景:当要显示的数据,一页显示不全,需要分页提交sql请求 语法: SELECT 查询列表 FROM 表名 [JOIN type JOIN 表2 ON 连接条件 WHERE 筛选条件 ...

最新文章

  1. C++11 :STL中的 iota ()函数
  2. 年薪70W架构师:全套英语学习资源泄露,手慢则无!(禁止外传)
  3. Python之操作RabbitMQ
  4. 29 仿京东放大镜案例
  5. M1芯片搞数据科学好使吗?5种基准测试给你答案
  6. MATLAB在温室中的应用,基于MATLAB的日光温室内气温的图形显示方法与流程
  7. 啊哈算法-擒贼先擒王(并查集)
  8. windows主机的linux虚拟机中使用neovim复制、粘贴
  9. 通过angularjs的directive以及service来实现的列表页加载排序分页
  10. IT人母亲的美国之行(3)
  11. 二维数组遍历_布尔值数组的状态压缩
  12. AI学习——感知机和BPNN算法
  13. 今日股市板块利好早知道,全球科技巨头聚齐联手保护云数据
  14. 前端面经 | 腾讯实习生校招面试心路历程
  15. Bookmark Sentry – 检查重复、删除死链书签 Chrome扩展
  16. 58同城产品2面面经
  17. 一文搞定Centos7.x安装ELK的7.6.2版本以及Cerebro集群监控
  18. 银光类似web visio的节点连线控件Essential Diagram免费下载地址
  19. 51单片机中断详解(上)
  20. 调节盘的三维建模及加工工艺规程设计

热门文章

  1. Wince6.0 cleartype
  2. 210板wince键盘驱动分析和移植
  3. arcgis坡度结果有误或z因子前有感叹号
  4. 一步步编写操作系统 36 一级页表与虚拟地址1
  5. idea提示“ cannot access xxxxxxxx.class“的解决方法,idea的bug
  6. 【Python CheckiO 题解】Index Power
  7. 【POJ - 2377】Bad Cowtractors (最大生成树,并查集)
  8. 50个最有用的Matplotlib数据分析与可视化图
  9. ubuntu服务器创建共享文件夹,Ubuntu samba安装创建共享目录及使用
  10. 安卓用于组件传递参数的对象是_入门篇:7.组件2:Android Service-service的数据传递与通信...