复杂的字符串算法——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,一经查实,立即删除!

相关文章

UML 的工厂方法设计模式 策略设计模式 抽象工厂设计模式 观察者设计模式

UML 的工厂方法设计模式 UML 的工厂方法设计模式是一种创建型设计模式&#xff0c;它通过定义一个创建对象的接口&#xff0c;但将具体的对象创建延迟到子类中。这样可以让子类决定实例化哪个类。该模式提供了一种创建对象的灵活方式&#xff0c;同时也隐藏了对象的具体实现细…

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的系统讲解&#…

Pandas 2.2 中文官方教程和指南(十七)

原文&#xff1a;pandas.pydata.org/docs/ 重复标签 原文&#xff1a;pandas.pydata.org/docs/user_guide/duplicates.html Index对象不需要是唯一的&#xff1b;你可以有重复的行或列标签。这一点可能一开始会有点困惑。如果你熟悉 SQL&#xff0c;你会知道行标签类似于表上的…

TCP/IP协议族中的TCP(三):解析其关键特性与机制

⭐小白苦学IT的博客主页⭐ ⭐初学者必看&#xff1a;Linux操作系统入门⭐ ⭐代码仓库&#xff1a;Linux代码仓库⭐ ❤关注我一起讨论和学习Linux系统 前言 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是互联网协议族中至关重要的组成部…

08.OSPF的特殊区域及其特点

OSPF特殊区域 Stub 末梢区域&#xff0c;处在AS的边缘&#xff0c;只有连接其他区域的ABR&#xff0c;没有ASBR&#xff0c;没有虚连接穿越的非骨干区域 只能接收LSA1和LSA2与 LSA3&#xff0c;不能接收LSA4和LSA5区域内部路由与外部AS路由通信&#xff0c;由本区域的ABR&am…

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…

Swift中TableView的编辑模式

Swift中TableView的编辑模式可以通过UITableView的属性isEditing来控制。 要将TableView设置为编辑模式&#xff0c;可以使用以下代码&#xff1a; tableView.isEditing true要退出编辑模式&#xff0c;可以使用以下代码&#xff1a; tableView.isEditing false当TableVie…

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

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

数据结构-前缀树

前缀树 前缀树定义 前缀树&#xff08;Trie树&#xff09;&#xff0c;又称字典树、单词查找树或键树&#xff0c;是一种专门设计用于高效存储和检索字符串集合中词项的树形数据结构。其核心特性在于能够快速实现字符串的前缀匹配&#xff0c;极大减少了无谓的字符比较&#xf…

微信小程序详解

微信小程序是一种无需下载安装即可使用的应用&#xff0c;它实现了应用“触手可及”的梦想&#xff0c;用户只需扫一扫或搜索一下即可打开应用。微信小程序全面开放申请后&#xff0c;企业、政府、媒体、其他组织或个人开发者均可申请注册。 微信小程序的特点包括&#xff1a;…

基于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>使…

uniapp 阿里云点播 其他功能

详细记录 阿里云播放器 基础功能 官方文档 继 根据业务开发了其他功能 大家可以结合 上一篇 基础阿里云播放器使用 使用