复杂的字符串算法——KMP算法

字符串算法


  • 模式匹配(Pattern Matching):在一篇长度为 𝑛 的文本 𝑆 中,找某个长度为 𝑚 的关键词 𝑃。𝑃 可能多次出现,都需要找到。
  • 最优的模式匹配算法复杂度:
    𝑂(𝑚+𝑛),因为至少需要检索文本 𝑆 的 𝑛 个字符和关键词 𝑃 的 𝑚 个字符。

一、暴力的模式匹配算法(暴力法)

1. 思路

        从 𝑆 的第一个字符开始,逐个匹配 𝑃 的每个字符。例如 𝑆 = “𝑎𝑏𝑐𝑥𝑦𝑧123”,𝑃 = “123”。第 1 轮匹配的两个字符就不同,𝑃[0]≠𝑆[0],称为“失配”,后面的 𝑃[1]、𝑃[2] 不用再比较了。一共比较 6+3=9 次:前 6 轮比较 𝑃 的第 1 个字符,第 7 轮比较 𝑃 的 3 个字符。

2. 理想情况

        这个匹配的过程,可以做个形象的比喻:把 𝑃 看成一个滑块,在轨道 𝑆 上滑动,直到完全匹配。但是举得这个例子比较特殊,𝑃 和 𝑆 的字符基本都不一样。在每轮匹配时,往往第 1 个字符就对不上,用不着继续匹配 𝑃 后面的字符。所以此时,计算复杂度差不多是 𝑂(𝑛) 的,这已经是字符串匹配能达到的最优复杂度了。所以,如果字符串 𝑆、𝑃 符合上述的特征,暴力法是很好的算法。

3. 糟糕情况

        例如 𝑃 的前 𝑚−1 个都容易找到匹配,只有最后一个不匹配,那么复杂度就退化成 𝑂(𝑛𝑚)。例如 𝑆 = “𝑎𝑎𝑎𝑎𝑎𝑎𝑎𝑎𝑏”,𝑃 = “𝑎𝑎𝑏”。下图中 𝑖 指向 𝑆[𝑖],𝑗 指向 𝑃[𝑗](0≤𝑖<𝑛,0≤𝑗<𝑚)。第 1 轮匹配后,在 𝑖=2,𝑗=2 的位置失配。第 2 轮让 𝑖 回溯到 1,𝑗 回溯到 0,重新开始匹配。最后经过 7 轮,共匹配 7×3=21 次,远远超过上面例子中的 9 次。

二、KMP算法

1. 优势

        KMP 是一种在任何情况下都能达到 𝑂(𝑛+𝑚) 复杂度的算法。

        用 KMP 算法时,指向 𝑆 的 𝑖 指针不会回溯,而是一直往后走到底。与朴素方法比较,这大大加快了匹配速度。

        在朴素方法中,每次新的匹配都需要对比 𝑆 和 𝑃 的全部 𝑚 个字符,这实际上做了重复操作。例如第一轮匹配 𝑆 的前 3 个字符 “𝑎𝑎𝑎” 和 𝑃 的 “𝑎𝑎𝑏”,第二轮从 𝑆 的第 2 个字符 ‘𝑎’ 开始,与和 𝑃 的第一个字符 ‘𝑎’ 比较,这其实不必要,因为在第一轮比较时已经检查过这两个字符,知道它们相同。如果能记住每次的比较,用于指导下一次比较,使得 𝑆 的 𝑖 指针不用回溯,就能提高效率。

2. 不回溯的指针

分为两种情况:

(1)P 在失配点之前的每个字符都不同

        例如 𝑆 = “𝑎𝑏𝑐𝑎𝑏𝑐𝑑”,𝑃 = “𝑎𝑏𝑐𝑑”,第一次匹配的失配点是 𝑖=3,𝑗=3。失配点之前的 𝑃 的每个字符都不同,𝑃[0]≠𝑃[1]≠𝑃[2];而失配点之前的 𝑆 与 𝑃 相同,即 𝑃[0]=𝑆[0]、𝑃[1]=𝑆[1]、𝑃[2]=𝑆[2]。下一步如果按朴素方法,𝑗 要回到位置 0,𝑖 要回到 1,去比较 𝑃[0] 和 𝑆[1]。但 𝑖 的回溯是不必要的。由 𝑃[0]≠𝑃[1]、𝑃[1]=𝑆[1] 推出 𝑃[0]≠𝑆[1],所以 𝑖 没有必要回到位置 1。同理,𝑃[0]≠𝑆[2],𝑖 也没有必要回溯到位置 2。所以 𝑖 不用回溯,继续从 𝑖=3、𝑗=0 开始下一轮的匹配。

        看下面的示意图可以更好的理解。当 𝑃 滑动到左图位置时,𝑖 和 𝑗 所处的位置是失配点, 𝑆 与 𝑃 的阴影部分相同,且阴影内部的 𝑃 的字符都不同。下一步直接把 𝑃 滑到 𝑆 的 𝑖 位置,此时 𝑖 不变、𝑗 回到 0,然后开始下一轮的匹配。

(2)P 在失配点之前的字符有部分相同

这种情况要再细分为两种情况。

1> 相同的部分是前缀(位于 𝑃 的最前面)和后缀(在 𝑃 中位于 𝑗 前面的部分字符)。

前缀和后缀的定义:

字符串 𝐴和 𝐵,若存在 𝐴=𝐵𝐶,其中 𝐶 是任意的非空字符串,称 𝐵 为 𝐴 的前缀;

同理可定义后缀,若存在 𝐴=𝐶𝐵, 𝐶 是任意非空字符串,称 𝐵 为 𝐴 的后缀。

从定义可知,一个字符串的前缀和后缀不包括自己。

        当 𝑃 滑动到下面左图位置时,𝑖 和 𝑗 所处的位置是失配点,𝑗 之前的部分与 𝑆 匹配,且子串 1(前缀)和子串 2(后缀)相同,设子串长度为 𝐿。下一步把 𝑃 滑到右图位置,让 𝑃 的子串 1 和 𝑆 的子串 2 对齐,此时 𝑖 不变、𝑗=𝐿,然后开始下一轮的匹配。注意,前缀和后缀可以部分重合。 

2> 相同部分不是前缀后缀。 下面左图,𝑃 滑动到失配点 𝑖 和 𝑗,前面的阴影部分是匹配的,且子串 1 和 2 相同,但是 1 不是前缀(或者 2 不是后缀),这种情况与“1>. 𝑃 在失配点之前的每个字符都不同”类似,下一步滑动到右图位置,即 𝑖 不变,𝑗 回溯到 0。请你自己分析其正确性。

 3. Next[] 数组

        根据前面的分析,会发现不回溯 𝑖 完全可行的。

        KMP 算法的关键在于模式 𝑃 的前缀和后缀,计算每个 𝑃[𝑗] 的前缀、后缀,记录在 Next[] 数组中,Next[𝑗] 的值等于 𝑃[0]∼𝑃[𝑗−1] 这部分子串的前缀集合和后缀集合的最长交集的长度,我们把这个最长交集称为“最长公共前后缀”。在有的资料中,也有把 Next[] 命名为 shift[] 或者 fail[] 的。

        例如 𝑃 = “𝑎𝑏𝑐𝑎𝑎𝑏”,计算过程如下表,每一行的带下划线的子串是最长公共前后缀。

jP[0] ~ P[j-1]前缀

后缀

Next[j]
1a0
2abab0
3abca, abbc, c0
4abcaa, ab, abcbca, ca, a1
5abcaaa, ab, abc, abcabcaa, caa, aa, a1
6abcaabaab, abc, abca, abcaabcaab, caab, aab, ab, b2

         所以,Next[] 只和 𝑃 有关,我们应该可以通过预处理 𝑃 得到 Next[]。

        下面介绍一种复杂度只有 𝑂(𝑚) 的极快的计算 Next[] 的方法,它巧妙地利用了前缀和后缀的关系,从 Next[i] 递推到 Next[i+1]。

        就假设当前我们已经计算出了 Next[i],它对应 𝑃[0]∼𝑃[𝑖−1] 这部分子串的后缀和前缀,见下面图(1)所示。后缀的最后一个字符是 𝑃[𝑖−1]。阴影部分 𝑤 是最长交集,交集 𝑤 的长度为Next[i],这个交集必须包括后缀的最后一个字符 𝑃[𝑖−1] 和前缀的第一个字符 𝑃[0]。前缀中阴影的最后一个字符是 𝑃[𝑗],𝑗 = Next[i]-1。

        图(2) 推广到求 Next[i+1],它对应 𝑃[0]∼𝑃[𝑖] 的后缀和前缀。此时后缀的最后一个字符是 𝑃[𝑖],与这个字符相对应,把前缀的 𝑗 也往后移一个字符,𝑗 = Next[i]。我们要判断两种情况:

1> 若 𝑃[𝑖]=𝑃[𝑗],则新的交集等于“阴影 𝑤+𝑃[𝑖]”,交集的长度 Next[i+1] = Next[i]+1。如图(2)所示。

2> 若 𝑃[𝑖]≠𝑃[𝑗],说明后缀的“阴影 𝑤+𝑃[𝑖] ”与前缀的“阴影 𝑤+𝑃[𝑗] ”不匹配,只能缩小范围找新的交集。把前缀往后滑动,也就是通过减小 𝑗 来缩小前缀的范围,直到找到一个匹配的 𝑃[𝑖]=𝑃[𝑗] 为止。

        减小 𝑗?要如何减小 𝑗 呢?

        减小 𝑗 只能在 𝑤上继续找最大交集,这个新的最大交集是 Next[j],所以更新 j’ = Next[j]。下图(2)画出了完整的子串 𝑃[0]∼𝑃[𝑖],最后的字符 𝑃[𝑖] 和 𝑃[𝑗] 不等。斜线阴影 𝑣 是 𝑤上的最大交集,下一步判断:若 𝑃[𝑖]=𝑃[𝑗’],则 Next[i+1] 等于 𝑣 的长度加 1,即Next[j’]+1;若 𝑃[𝑖]≠𝑃[𝑗’],继续更新 𝑗’。

        所以我们只要重复以上操作,逐步扩展 𝑖,就可以求得所有的Next[i]。有了 Next[] 数组,就能在失配的时候,直接跳到 Next[] 所指向的下一个位置啦!

4. KMP 模板代码

题目描述:请你在 𝑆 中找到所有的 𝑃。

输入描述:两行,第一行是字符串 𝑆,第二行是模式串 𝑃。

输出描述:输出每个 𝑃 在 𝑆 中的位置。

        下面给出 KMP 的模板代码,包括 getNext()kmp() 两个函数。getNext() 预计算 Next[]数组,是前面图解思路的完全实现。kmp() 函数在 𝑆 中匹配所有的 𝑃,注意每次匹配到的起始位置是 𝑠[𝑖+1−𝑝𝑙𝑒𝑛],末尾是 𝑠[𝑖]。

#include <stdio.h>
#include <string>
using namespace std;const int N = 1005;
char str[N], pattern[N];
int Next[N];// 计算Next[1]~Next[plen]
void getNext(char* p, int plen) {       Next[0] = 0; Next[1] = 0;for (int i = 1; i < plen; i++) {    //把i的增加看成后缀的逐步扩展int j = Next[i];           //j的后移:j指向前缀阴影w的后一个字符while (j && p[i] != p[j])  //阴影的后一个字符不相同j = Next[j];           //更新jif (p[i] == p[j])    Next[i + 1] = j + 1;else                 Next[i + 1] = 0;}
}
// 在s中找p
void kmp(char* s, char* p) {           int last = -1;int slen = strlen(s), plen = strlen(p);getNext(p, plen);       //预计算Next[]数组int j = 0;for (int i = 0; i < slen; i++) {     //匹配S和P的每个字符while (j && s[i] != p[j])   //失配了。注意j==0是情况(1)j = Next[j];            //j滑动到Next[j]位置if (s[i] == p[j])  j++;     //当前位置的字符匹配,继续if (j == plen) {            //j到了P的末尾,找到了一个匹配//匹配了一个,在S中的起点是i+1-plen,末尾是i。如有需要可以打印:printf("Location=%d, %s\n", i + 1 - plen, &s[i + 1 - plen]);}}
}int main() {scanf("%s", str);       //读串Sscanf("%s", pattern);   //读模式串Pkmp(str, pattern);return 0;
}

三、小结

        前面解析了KMP算法的思想,并给出了模板代码,KMP 确实是一个很烧脑的算法,代码短却逻辑复杂。

        KMP 非常好用,我们经常在网页或文档中搜关键词,这就是 KMP 的应用场合。还有,由于 KMP 算法只需要预处理模式串(关键词)𝑃,而不需要预处理 𝑆。所以使用上述代码的 kmp() 函数时,𝑆 可以是一个很长的、动态增加的文本串,使得我们可以从头到尾逐个匹配 𝑆 的所有字符,从中找到关键词 P。KMP 的效率也极高,例如在 100 万字的一篇文章中,找出一个关键词出现的所有位置,计算次数 100 万次,不到 0.1 秒。

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

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

相关文章

AHB传输---突发操作

突发操作 在本协议中定义了4拍、8拍和16拍的突发&#xff0c;以及未定义长度的突发和单次传输。它支持增量和包装突发&#xff1a; 增量突发访问连续位置&#xff0c;每个传输的地址是前一个地址的增量。包装突发在跨越地址边界时会包装。地址边界的计算方法是突发中拍数与传…

Android—统一依赖版本管理

依赖版本管理有多种方式 config.gradle 用于Groovy DSL&#xff0c;新建一个 config.gradle 文件&#xff0c;然后将项目中所有依赖写在里面&#xff0c;更新只需修改 config.gradle文件内容&#xff0c;作用于所有module buildSrc 可用于Kotlin DSL或Groovy DSL&#xff0c;…

MATLAB冒号表示法

MATLAB 冒号表示法 colon(:)是在MATLAB中最有用的运算符之一。它用于创建向量&#xff0c;下标数组和指定迭代。 如果要创建包含1到10的整数的行向量&#xff0c;请编写- 示例 1:10 MATLAB执行该语句并返回包含1到10的整数的行向量- ans 1 2 3 4 5 6 7 8 9 10 如果要指定一…

github Copilot的使用总结

1. 代码建议和补全 GitHub Copilot 的基本使用涉及编写代码时的实时代码建议和补全。一旦你已经安装并配置好 GitHub Copilot 插件&#xff0c;你可以在支持的编辑器&#xff08;如 Visual Studio Code&#xff09;中开始使用 Copilot。以下是一些基本的使用步骤&#xff1a; …

VBA技术资料MF146:发出多次Beep提示声

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

K8S 部署和访问 Kubernetes 仪表板(Dashboard)

文章目录 部署 Dashboard UI浏览器访问登陆系统 Dashboard 是基于网页的 Kubernetes 用户界面。 你可以使用 Dashboard 将容器应用部署到 Kubernetes 集群中&#xff0c;也可以对容器应用排错&#xff0c;还能管理集群资源。 你可以使用 Dashboard 获取运行在集群中的应用的概览…

unit4.web服务的部署及高级优化方案

搭建web服务器要求如下&#xff1a; 1.web服务器的主机ip&#xff1a;172.25.254.100 [rootserver101 桌面]# vmset.sh 100 连接已成功激活&#xff08;D-Bus 活动路径&#xff1a;/org/freedesktop/NetworkManager/ActiveConnection/3&#xff09; [rootserver101 桌面]# ifc…

鑫海移民荣耀呈现:EB5投资移民盛宴落幕,卓越项目引领投资新潮

随着春日的暖阳渐渐铺满大地&#xff0c;我们鑫海移民集团在这个充满希望的季节里&#xff0c;举办了一场意义非凡的EB5投资移民专题活动。于2024年4月27日&#xff08;周六&#xff09;下午13:30&#xff0c;在北京渤海润泽威斯汀酒店隆重举行&#xff0c;我们与众多热情的参与…

基于java的商店积分管理系统的设计与实现

功能需求 从功能上可以划分为个人信息管理、商店管理、平台管理、订单管理和数据分析。后台管理系统主要服务于商户和平台管理员&#xff0c;兑换用户是属于商户平台的自有用户&#xff0c;不会被纳入到后台管理系统中来。商户用户可以对自己的积分进行管理&#xff0c;平台管…

echarts下载图片

toolbox: {show: true,//展示工具栏itemSize:20,//icon的大小iconStyle:{borderColor:"#409eff",borderWidth:"3",color:"#fff"},right:"40px",//偏移位置feature: {saveAsImage: {title: "下载图表", //鼠标滑过之后文案na…

用wps自带工具给图片做标注

在wps中&#xff0c;选中wps中的图片&#xff0c;右键选择【编辑】进入图片编辑器&#xff0c;在选项卡面板右侧选择【标注】工具&#xff0c;再选择【添加文本】工具&#xff0c;即可直接在图片上输入文字&#xff0c;标注完成后选择【覆盖原图】就完成标注任务。

【Canvas与艺术】绘制美国星条旗

注意&#xff1a; 该图位置和大小都是按照网上说明精确绘制的。 【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>使…

FA-128晶振用于医疗设备

血糖仪已成为家庭常用的医疗设备,日本爱普生晶振公司生产的2016封装,32MHz贴片晶振可完美应用于医疗器械血糖仪,此款晶振订货型号为X1E000251005900晶振,型号为FA-128,负载电容分8PF,精度10PPM,其尺寸参数为2.0x1.6x0.5mm,符合ROHS标准且无铅,具有封装尺寸超小,高精度,频率范围…

C++ | Leetcode C++题解之第55题跳跃游戏

题目&#xff1a; 题解&#xff1a; class Solution { public:bool canJump(vector<int>& nums) {int n nums.size();int rightmost 0;for (int i 0; i < n; i) {if (i < rightmost) {rightmost max(rightmost, i nums[i]);if (rightmost > n - 1) {r…

【HarmonyOS4学习笔记】《HarmonyOS4+NEXT星河版入门到企业级实战教程》课程学习笔记(一)

课程地址&#xff1a; 黑马程序员HarmonyOS4NEXT星河版入门到企业级实战教程&#xff0c;一套精通鸿蒙应用开发 &#xff08;本篇笔记对应课程第 1 - 2节&#xff09; P1《课程介绍》 开场白&#xff0c;HarmonyOS 的一个简介&#xff0c;话不多说&#xff0c;直接看图吧&…

第十五届蓝桥杯省赛第二场C/C++B组C题【传送阵】题解(AC)

解题思路 由于 a a a 数组是一个 1 1 1 到 n n n 的一个排列&#xff0c;那么形成的一定是如下形式&#xff1a; 一定会构成几个点的循环&#xff0c;或者是几个单独的点。 从任意点开始&#xff0c;如果能进入一个循环&#xff0c;一定可以将整个循环的宝藏都拿走&#x…

[vant] 图片预览关闭按钮显示不正常

如果这里显示不正常, 那就应该是按照文档引入了 显示有问题, 直接不引入, 声明一下就行

每日论文推荐:我们距离GPT-4V有多远,最接近GPT-4V的开源多模态大模型

&#x1f4cc; 元数据概览&#xff1a; 标题&#xff1a;How Far Are We to GPT-4V? Closing the Gap to Commercial Multimodal Models with Open-Source Suites作者&#xff1a;Zhe Chen, Weiyun Wang, Hao Tian, Shenglong Ye, Zhangwei Gao, Erfei Cui, Wenwen Tong, Kon…

企业计算机服务器中了helper勒索病毒怎么办?Helper勒索病毒解密处理流程

网络技术的不断发展与成熟&#xff0c;为企业的生产运营提供了极大便利&#xff0c;让企业的发展速度大大提升&#xff0c;但网络毕竟是虚拟服务系统&#xff0c;虽然可以为企业提供便利&#xff0c;但也会给企业数据安全带来严重威胁。近日&#xff0c;云天数据恢复中心接到山…

c++中的链表list的模拟实现

拖更了半个月&#xff0c;我终于来填c的坑啦。上次我们说的vetcor不知道小伙伴还记得多少呢&#xff1f;今天我们要讲list的模拟实现。 目录 架构结点list表的结构 构造函数尾插push_back()尾删pop_back()计算个数&#xff1a;size()判断空empty()※迭代器问题普通迭代器迭代器…