【算法详解】理解KMP,真的那么难吗?—— 一篇讲透它的核心思想

【算法详解】理解KMP,真的那么难吗?—— 一篇讲透它的核心思想
在这里插入图片描述



🫧 励志不掉头发的内向程序员个人主页
 ✨️ 个人专栏: 《C++语言》《Linux学习》

🌅偶尔悲伤,偶尔被幸福所完善


👓️博主简介:

在这里插入图片描述


文章目录


前言

本文用尽量详细的语言来讲解说明 kmp 算法内容,学习之前需要知道一点点动态规划的基础,如果不知道最好去了解了解。我们一起来看看算法吧。
在这里插入图片描述

一、相关概念

在学习 kmp 算法之前,我们得先提前了解最基本的 “ 动态规划 ” 的知识,否则可能学习的时候会有一些困难,因为它的原理类似于动态规划。

字符串:

  • 用字符构成的的序列就是字符串。

这个概念很简单,但是我们这里有个小技巧:就和动态规划那样,在字符串匹配问题中,我们会让字符串的下标从 1 开始,这样便于我们处理一些边界问题。因此,在输入字符串时,我们一般会在前面加上一个空格,这样字符串就会从 1 开始计数了。

string s; cin >> s;int n = s.size(); s =' '+ s;

子串:

  • 选取字符串中连续的一段。

例如:

字符串: abcdefg;
子串:cdefg、cde、abcdefg、a、b 等等;

前缀:

  • 从字符串首端开始,到某一个位置结束的子串。

字符串长度为 i 的前缀,就是字符串 [ 1, i ] 区间的子串。

例如:

字符串:abcdefg;
长度为 3 的前缀为:abc;
长度为 5 的前缀为:abcde;
长度为 7 的前缀为:abcdefg(字符串本身)

注意:
这里默认字符串前面有一个空格,所以下标从 1 开始而非 0。

真前缀:

  • 不包含字符串本身的前缀。

后缀:

  • 从字符串某一个位置开始,到字符串末端的结束的子串。

字符串长度为 i 的后缀,就是字符串 [ n − i + 1, n ] 区间的子串。

例如:

字符串:abcdefg;
长度为 3 的后缀为:efg;
长度为 5 的后缀为:cdefg;
长度为 7 的后缀为:abcdefg(字符串本身)

真后缀:

  • 不包含字符串本身的后缀。

真公共前后缀 / border 以及最长真公共前后缀的长度 / π:

  • 字符串的真公共前后缀为⼀个子串 ,满足既是真前缀,又是真后缀,也称为字符串的 border。
  • 在一个字符串中,最长的真公共前后缀的长度用 π 表示。

例如:

字符串:aabaaba;
真前缀:a、aa、aab、aaba、aabaa、aabaab、aabaaba;
真后缀:a、ba、aba、aaba、baaba、abaaba、aabaaba;

从上面我们可以发现,真前缀和真后缀相同的有 a、aaba。最长真公共前后缀的长度为 4。所以上述字符串的 π 值为 4 。

  • 性质:

传递性:字符串 s 的 border 的 border 也是字符串 s 的 border。

在这里插入图片描述

字符串匹配:

  • 字符串匹配又称模式匹配。给定两个字符串 S 和 T ,需要在主串 S 中找到模式串 T 。

例如:

主串 S = “abcdefcde”
模式串 T = “cde”
如果下标从 1 开始计数,模式串会在主串 3、7 位置出现。

关于字符串匹配,大家肯定能想到暴力解法,那就是拿着模式串,在主串中一个位置一个位置判断。但是,暴力解法极限情况,时间开销会达到 O(n × m)。而接下来要学习的 kmp,能在 O(n + m) 的线性时间内,找到所有的匹配位置,以及维护出更多的信息。

二、前缀函数

  • 字符串每一个前缀子串的 π 值。

以字符串 aabaab 为例,π[i] 表示:字符串 s 长度为 i 的前缀,最长的 border 长度(最长真公共前后缀)。

下标(i)123456
前缀子串aaaaabaabaaabaaaabaab
π010123

原理:

在这里插入图片描述

技巧:

  • 从大到小枚举字符串 s 某个前缀的所有 border。

假设我们此时生成了一个字符串 s 的前缀函数表,我们可以利用这张表,从大到小拿到某个前缀所有的 border。

原理就是 border 的传递性:字符串 border 的 border 还是 border。

在这里插入图片描述


到最后如果是没有 π 时,我们函数就会跳到我们提前给字符串预留的边界情况发生的 0 下标位置上。

string s;// 生成好的前缀函数int pi[N];// 长度为 i 的前缀中,所有 border 的长度voidget_border(int i){int j = pi[i];while(j){ cout << j; j = pi[j];}}

三、计算前缀函数

我们在看完上面的内容后我们就来看看前缀函数怎么计算。

计算前缀函数:

  • 计算前缀函数包含动态规划的思想,那就是推导状态转移方程。

对于字符串 s:

  1. 状态表示:
    π[i] 表示:字符串 s 长度为 i 的前缀,最长的 border 长度(最长真公共前后缀)。

在计算完 i - 1 位置的前缀函数后,此时开始计算 i 位置的 π 值。

在这里插入图片描述

我们可以发现,我们 i 位置前上一个位置的真前缀和和真后缀和都存在 π[i - 1] 中。换句话说,如果 i 位置元素和 j 位置的元素相同,那就是 i 位置的 π 值了。

在这里插入图片描述


如果 i 和 j 不相同,我们可以继续往上一层的 π 值去寻找,也就是 π[ π [ i - 1 ] ]。

在这里插入图片描述


此时如果 s[ j -1 ] == s[ i ] ,此时便得出了 i 位置的 π 值了,否则我们在往下推。

  1. 状态转移方程:
  • 我们发现,如果将长度为 i 的前缀中的 border 删去最后一个字符,就变成了长度为 i - 1 的前缀中的 border;
  • 那么,我们就可以从大到小枚举长度为 i - 1 的前缀中所有的 border,然后判断这个 border 的下⼀个字符是否和 s[i] 相等:
    a. 如果相等,说明这个就是最长的;
    b. 如果不相等,那就继续判断下⼀个 border,直到将所有的 border 验证完毕。
string s;int pi[N];voidget_pi(int i){ cin >> s;int n = s.size(); s =' '+ s;for(int i =2; i <= n; i++){int j = pi[i -1];while(j && s[i]!= s[j +1]) j = pi[j];if(s[i]== s[j +1]) j++; pi[i]= j;}}

当然,我们还可以进行优化,我们可以发现,当我们找到 i - 1 的时候,此时的 j 刚好就是在我们下一次要查找的 π 值。

string s;int pi[N];voidget_pi(int i){ cin >> s;int n = s.size(); s =' '+ s;for(int i =2, j =0; i <= n; i++){while(j && s[i]!= s[j +1]) j = pi[j];if(s[i]== s[j +1]) j++; pi[i]= j;}}

时间复杂度:
模拟一遍过程会发现,指针每次向后移动一位后,指针最多会向后移动一位,然后继续往前跳。因此两个指针最差情况下会遍历字符串两遍,时间复杂度为 O(2n) = O(n)。

四、用前缀函数解决字符串匹配

设主串 S = “abcabaaaba”,模式串 T = “aba”,主串的长度为 n,模式串的长度为 m。

将两个字符串拼起来:S = T + ‘#’ + S = “aba#abcabaaaba”,对于新的字符串,可以在 O(n + m) 时间内生成前缀函数:

下标1234567891011121314
π00101201231123

前缀函数等于模式串长度的位置 i ,就是能够匹配的末端。在主串中,出现的位置就是 i − 2 × m 。那么,有了前缀函数之后,不仅能知道匹配了几次,还能知道每次匹配的起始位置。

注意:
当 i > m + 1 时,如果出现了 π == m 时,那就是出现了匹配的情况,匹配出现的位置为 i - 2 × m。例如上面 m == 3,π 为 3 都是匹配的,比如 i ==10、i == 14 的位置都能匹配上,它们分别对应主串的 4(10 - 2 × 3) 和 8(14 - 2 × 3)位置。

五、kmp 算法模板

题目来源: 洛谷

题目链接:P3375 【模板】KMP - 洛谷

【题目描述】

【输入格式】

【输出格式】

【示例】

我们按照上面的实现方式即可完成此题。

#include<iostream>#include<string>usingnamespace std;constint N =2e6+10; string s, t;int n, m;int pi[N];intmain(){ cin >> s >> t; n = s.size(); m = t.size(); s =' '+ t +'#'+ s;for(int i =2; i <= n + m +1; i++){int j = pi[i -1];while(j && s[j +1]!= s[i]) j = pi[j];if(s[j +1]== s[i]) j++; pi[i]= j;if(j == m){ cout << i -2* m << endl;}}for(int i =1; i <= m; i++) cout << pi[i]<<" "; cout << endl;return0;}

六、next 数组版本

next 数组版本其实和上面是一样的,只不过是把上面的过程拆成两步来写:

  1. 先预处理模式串 t 的前缀函数 - next 数组;
  2. 在暴力匹配的过程中,用生成的 next 数组,加速匹配。

next 数组的本质也是求解我们的前缀函数。我们来举一个例子:

在这里插入图片描述


我们先把 t 的 next 数组求出来。

在这里插入图片描述


此时我们拿 t 来和 s 进行匹配。

在这里插入图片描述


当我们发现匹配不上的时候,如果是暴力匹配,那 t 就得从头开始继续往后匹配,但是由于我们求了 t 的前缀数组,所以我们可以利用数组来加速匹配。

在这里插入图片描述
#include<iostream>#include<string>usingnamespace std;constint N =2e6+10; string s, t;int n, m;int ne[N];intmain(){ cin >> s >> t; n = s.size(); m = t.size(); s =' '+ s; t =' '+ t;// 预处理模式串的 next 数组for(int i =2, j =0; i <= m; i++){while(j && t[i]!= t[j +1]) j = ne[j];if(t[i]== t[j +1]) j++; ne[i]= j;}// 利⽤ next 数组匹配for(int i =1, j =0; i <= n; i++){while(j && s[i]!= t[j +1]) j = ne[j];if(s[i]== t[j +1]) j++;if(j == m){ cout << i - m +1<< endl;}}for(int i =1; i <= m; i++) cout << ne[i]<<" "; cout << endl;return0;}

七、周期和循环节


总结

以上便是 kmp 算法的原理啦,还是有一点点绕的,大家可以多琢磨琢磨,代码不是很难写,但是理解不容易,希望大家多加思考,我们下一章节再见。

🎇坚持到这里已经很厉害啦,辛苦啦🎇ʕ • ᴥ • ʔづ♡ど

Read more

最新电子电气架构(EEA)调研-3

而新一代的强实时性、高确定性,以及满足CAP定理的同步分布式协同技术(SDCT),可以实现替代TSN、DDS的应用,且此技术已经在无人车辆得到验证,同时其低成本学习曲线、无复杂二次开发工作,将开发人员的劳动强度、学习曲线极大降低,使开发人员更多的去完成算法、执行器功能完善。 五、各大车厂的EEA 我们调研策略是从公开信息中获得各大车厂的EEA信息,并在如下中进行展示。 我们集中了华为、特斯拉、大众、蔚来、小鹏、理想、东风(岚图)等有代表领先性的车辆电子电气架构厂商。        1、华为 图12 华为的CCA电子电气架构              (1)华为“计算+通信”CC架构的三个平台                         1)MDC智能驾驶平台;                         2)CDC智能座舱平台                         3)VDC整车控制平台。        联接指的是华为智能网联解决方案,解决车内、车外网络高速连接问题,云服务则是基于云计算提供的服务,如在线车主服务、娱乐和OTA等。 华

By Ne0inhk
Apache IoTDB 架构特性与 Prometheus+Grafana 监控体系部署实践

Apache IoTDB 架构特性与 Prometheus+Grafana 监控体系部署实践

Apache IoTDB 架构特性与 Prometheus+Grafana 监控体系部署实践 文章目录 * Apache IoTDB 架构特性与 Prometheus+Grafana 监控体系部署实践 * Apache IoTDB 核心特性与价值 * Apache IoTDB 监控面板完整部署方案 * 安装步骤 * 步骤一:IoTDB开启监控指标采集 * 步骤二:安装、配置Prometheus * 步骤三:安装grafana并配置数据源 * 步骤四:导入IoTDB Grafana看板 * TimechoDB(基于 Apache IoTDB)增强特性 * 总结与应用场景建议 Apache IoTDB 核心特性与价值 Apache IoTDB 专为物联网场景打造的高性能轻量级时序数据库,以 “设备 - 测点” 原生数据模型贴合物理设备与传感器关系,通过高压缩算法、百万级并发写入能力和毫秒级查询响应优化海量时序数据存储成本与处理效率,同时支持边缘轻量部署、

By Ne0inhk
SQL Server 2019安装教程(超详细图文)

SQL Server 2019安装教程(超详细图文)

SQL Server 介绍) SQL Server 是由 微软(Microsoft) 开发的一款 关系型数据库管理系统(RDBMS),支持结构化查询语言(SQL)进行数据存储、管理和分析。自1989年首次发布以来,SQL Server 已成为企业级数据管理的核心解决方案,广泛应用于金融、电商、ERP、CRM 等业务系统。它提供高可用性、安全性、事务处理(ACID)和商业智能(BI)支持,并支持 Windows 和 Linux 跨平台部署。 一、获取 SQL Server 2019 安装包 1. 官方下载方式 前往微软官网注册账号后,即可下载 SQL Server Developer 版本(

By Ne0inhk