字符串匹配(BF KMP)详解 + 刷题

目录

🌼前言

BF  算法

KMP  算法 

(1)前缀函数  --  O(n^3)

(2)前缀函数  --  O(n^2)

(3)前缀函数  --  O(n) 

(4)辅助理解

🐋P1308 -- 统计单词数

AC  --  s.find()

AC  --  BF暴力

🦁P3375 -- KMP字符串匹配


🌼前言

以下 链接 里的代码,最好自己敲一遍,再去 刷题

BF  算法

字符串基础👇

字符串基础 - OI Wiki (oi-wiki.org)

BF算法👇

字符串匹配 - OI Wiki (oi-wiki.org)

BF  代码

最好 O(n),最坏 O(nm)

/*
s 主串      t 模式串(子串)      n 主串长度      t 子串长度
*/
std::vector<int> match(char *s, char *t, int n, int m) {std::vector<int> ans; // 主串匹配成功的第 1 个下标int i, j;for (i = 0; i < n - m + 1; ++i) { // 主串每个位置for(j = 0; j < m; ++j) if (s[i + j] != t[j]) break;if (j == m) ans.push_back(i); // 某个子串匹配成功}return ans; // 返回值类型是 vector<int>
}

KMP  算法 

解释

字符串匹配的KMP算法 - 阮一峰的网络日志 (ruanyifeng.com)

初始, "A" 的共有元素长度为 0,因为必须是 真前缀 和 真后缀,不能是本身

补充理解👇

KMP 有 2 种解释(原理都是,真前缀 和 真后缀,最大共有长度 ---- 即部分匹配值)

解释(1)

部分匹配值,比如说,2,那么子串,要向后移动 m - 2 个位置

m 是子串长度,6 - 2 == 4,对应上面的例子就是👇

解释(2)

部分匹配值 2,那么 主串下标 i 不变,子串下标 j 从 2开始

(即,主串下标    i   O(n)  线性往后移动,j 每次从 前缀函数 的位置开始,即 pi[i] 开始,不用从头开始)

意思是,还是👆的图:主串 i 位于 C 的位置,子串 就从 "ABCDABD" 中,下标为 2 的位置开始继续比较,即"ABC"的C(等价于上面的 子串右移 4 个位置)

代码

前缀函数与 KMP 算法 - OI Wiki (oi-wiki.org)

👆 代码 是 前缀函数 的代码,即 “解释” 中的 部分匹配值 的代码 

KMP  代码

(1)前缀函数  --  O(n^3)

比如 "ABCDABD",返回的 pi[3] 表示 "ABCD" 中,前后缀 的 最大共有长度(不包括ABCD本身)

注:前缀函数,是 KMP 算法的一部分

s.substr(i ,j) 下标 i 开始截取长度为 j 的子串 

vector<int> prefix_function(string s) {int n = (int)s.length();vector<int> pi(n); // 最大共有长度for (int i = 1; i < n; ++i) // i 是下标,1 ~ n - 1for (int j = i; j >= 0; --j) // 模式串长度if (s.substr(0, j) == s.substr(i - j + 1, j)) { // 前缀 == 后缀pi[i] = j; // 0 ~ i 的最大共有长度break; // 因为 j 递减, 第一个取到的一定最大}return pi;
}

因为 s.substr() 函数,也是线性复杂度,所以 O(n^{^{3}})

i 从 1 开始,因为 最大共有长度,不能是本身

(2)前缀函数  --  O(n^2)

比较难理解的是,s[i + 1] = s[pi[i]]

这个需要结合前面两个例子理解

新增的字符,需要在前面前缀的基础上

才可能实现,最大相同长度 + 1

首先你得搞懂,vecotr<int> pi(n) 存的是什么

pi[i] 保存的是 0 ~ i,真前后缀的最大相同的长度

由于 主串 下标 i 移动到下一位置时,如果 前缀函数 pi[] 的值要增加,

最多只能 +1(因为新增的字符,一定是在前面前缀的基础上的

所以 长度 j 只需要从 pi[i - 1] + 1 开始遍历,从上一个位置,最大相同长度 + 1 开始

又 ∵ j--,长度 j 是递减的,pi[i - 1] + 1,是长度 j 可能的最大值

所以只需要修改 j = i  -->  j = pi[i - 1] + 1 即可

相当于对 p[i - 1] + 1 到 i 这段的 剪枝,这一部分是不必要的

vector<int> prefix_function(string s) {int n = (int)s.length();vector<int> pi(n);for (int i = 1; i < n; ++i) // 下标从 1 到 n-1for (int j = pi[i - 1] + 1; j >= 0; --j) // 剪枝if (s.substr(0, j) == s.substr(i - j + 1, j)) {pi[i] = j;break;}return pi;
}

考虑到 j = pi[i - 1] + 1,对长度的限制,以及 长度 j 是递减的

每次只有在最好的情况 pi[i - 1] + 1,比较次数上限才会 +1

而每次超过 1 次的比较,都会消耗之后的比较次数

所以计算前缀函数,只需要 O(n) 次比较,总的时间复杂度 O(n^{2})

(3)前缀函数  --  O(n) 

s[0 ... i] ,下标从 0 到 i 的子串

s[0 ... j - 1] = s[i - j + 1 ... i] ,前缀 = 后缀,长度都为 j                             

划线部分不是很理解,其他都理解了,,最后得到 状态转移方程,来降低复杂度

下面是代码部分👇

注意这里的 s 是子串 

   vector<int> prefix_function(string s) {int n = (int)s.length();vector<int> pi(n);for (int i = 1; i < n; ++i) {// 下标 1 ~ n-1// j 表示 长度int j = pi[i - 1]; // 上一个位置的前缀函数值(最大长度)// 状态转移,退出循环时://(1)一直失配,直到 j = 0//(2)匹配成功while (j > 0 && s[i] != s[j]) j = pi[j - 1]; // 一直回溯if (s[i] == s[j]) j++; pi[i] = j; // 当前前缀函数值}
}

解释下第 12 行

if (s[i] == s[j]) j++; 

👇这里的 i 即 上一个 j  

例如,假设 s[0...i-1] 的前缀函数值为 j,即 pi[i-1] = j。当 s[i]s[j] 相等时,我们可以将前缀函数长度增加 1,即 j++。然后将新的前缀函数长度 j 赋值给 pi[i],表示 s[0...i] 的前缀函数值为 j

(4)辅助理解

关于,为什么 KMP 的复杂度是 O(n + m),详细解释👇

即,为什么 子串 中,下标 j 不是要回溯吗,为什么复杂度只是本身的长度 m 呢

字符串处理 - KMP算法的时间复杂度是如何计算的? - SegmentFault 思否

🐋P1308 -- 统计单词数

一般代码中的 next[] 数组,即上面的 pi[] 数组

P1308 [NOIP2011 普及组] 统计单词数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

AC  --  s.find()

前置

(1)std::string::npos

#include<iostream>
#include<cstring>
using namespace std;int main()
{string s1 = "babajiaoni";string s2 = "bajiao";string s3 = "babb";if(s1.find(s3) == string::npos) //找不到子串cout<<"找不到子串"<<endl;if(s1.find(s2) != string::npos) //能找到子串cout<<"能找到子串";return 0;
}

(2)

(3)cppreference

pos -- 查找的起始位置

字符串 s 中,下标从 5 开始查找 "is"

返回

Position of the first character of the found substring or npos if no such substring is found

返回子串中第一个字符的位置(主串中),找不到则返回 npos

AC  代码

#include<iostream>
#include<cstring>
#include<string>
using namespace std;int main()
{string t, s;getline(cin, t);getline(cin, s); // 读入一行(包括空格)// 匹配独立的单词,加上空格,便于 find() 匹配t = ' ' + t + ' '; s = ' ' + s + ' ';// 统一变小写int k = 'a' - 'A';// 三目   ? :for (string::size_type i = 0; i < s.length(); ++i)s[i] = (s[i] < 'a' && s[i] != ' ') ? (s[i] + k) : s[i];for (string::size_type i = 0; i < t.length(); ++i)t[i] = (t[i] < 'a' && t[i] != ' ') ? (t[i] + k) : t[i];// 或者用tolower(t[i])    大写 toupper(t[i])if (s.find(t) == string::npos) { // 匹配失败cout << -1;return 0;}int ans = 0, s_pos = s.find(t); // s 中 t 第一次出现的位置int temp = s_pos; // 初始位置while (s_pos != string::npos) {ans++;s_pos = s.find(t, s_pos + 1); // 下一位置开始查找}    cout << ans << " " << temp;// npos 是它能容纳的最大正值,常表示字符串结束return 0;
}

AC  --  BF暴力

#include<iostream>
#include<vector>
using namespace std;int main()
{string t, s;getline(cin, t);getline(cin, s); // 读入一行(包括空格)// 统一变小写int k = 'a' - 'A';// 三目   ? :for (string::size_type i = 0; i < s.length(); ++i)s[i] = (s[i] < 'a' && s[i] != ' ') ? (s[i] + k) : s[i];for (string::size_type i = 0; i < t.length(); ++i)t[i] = (t[i] < 'a' && t[i] != ' ') ? (t[i] + k) : t[i];// 或者用tolower(t[i])    大写 toupper(t[i])vector<int> ans; // 保存答案int n = s.length(), m = t.length(), i, j;for (i = 0; i < n - m + 1; ++i) {for (j = 0; j < m; ++j) {if (i > 0 && s[i - 1] != ' ') break; // 空格开头if (s[i + j] != t[j]) break; // 匹配失败}// 空格结尾if (j == m && (s[i+j] == ' ' || i+j == n) )ans.push_back(i);}if (ans.empty()) cout << -1;elsecout << ans.size() << " " << ans[0];return 0;
}

🦁P3375 -- KMP字符串匹配

P3375 【模板】KMP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

 

坑😫

注意,OI -wiki 的代码没问题,不要因为看了大佬 阮一峰 的文字解释,就误认为,可以 j = pi[j]

熟记 状态转移方程,j = pi[j - 1] 即可

else j = pi[j - 1];和j = pi[j - 1]; // 回溯到最大匹配的位置

AC  代码

#include<iostream>
#include<vector>
using namespace std;int cnt = 1; // 测试string s, t;
int pi[1000010]; // 部分匹配数组// 预处理 next数组 复杂度 O(m)
void prefix(string s) // 前缀函数, 子串 -- 最大共有长度
{int m = s.size();for (int i = 1; i < m; ++i) {int j = pi[i - 1]; // 上一位置的部分匹配值// 一直回溯  直到 j == 0 或 匹配成功while (j > 0 && s[i] != s[j]) j = pi[j - 1]; // 状态转移if (s[i] == s[j]) j++;pi[i] = j; // 更新当前匹配值}
}int main()
{cin >> s >> t;int n = s.size(), m = t.size();int i = 0, j = 0;prefix(t);// 主串 与 子串 比较while (i < n) {if (j == 0 && s[i] != t[j]) {i++;continue;}if (s[i] == t[j]) i++, j++;else j = pi[j - 1]; if (j == m) { // 匹配到子串最后一个字母cout << i - j + 1 << endl; // 下标 1 开始j = pi[j - 1]; // 回溯到最大匹配的位置}// cout << "(" << cnt++ << ")" <<i << " " << j << endl;}// 输出 pi[]for (i = 0; i < m; ++i)cout << pi[i] << " ";return 0;
}

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

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

相关文章

文件包含技术总结

开发人员一般会把重复使用的函数写到单个文件中&#xff0c;需要使用某个函数时直接调用此文件&#xff0c;而无需再次编写&#xff0c;这中文件调用的过程一般被称为文件包含。 allow_url_fopen On&#xff08;是否允许打开远程文件&#xff09; allow_url_include On&…

机器学习算法(一)

一、线性回归 线性回归&#xff08;Linear Regression&#xff09;可能是最流行的机器学习算法。线性回归就是要找一条直线&#xff0c;并且让这条直线尽可能地拟合散点图中的数据点。它试图通过将直线方程与该数据拟合来表示自变量&#xff08;x 值&#xff09;和数值结果&am…

uniapp page宽度设置为750rpx,子元素宽度100%,大小不一致

uniapp page宽度设置为750rpx&#xff0c;子元素宽度100%&#xff0c;大小不一致。 原因是我在page加了margin: 0 auto;去掉就正常了&#xff08;但是如果在超大屏幕还是会出现&#xff0c;我猜是使用rpx导致的&#xff0c;rpx渲染成页面时会转成精确到一个小数点几位数的rem&a…

[实战]加密传输数据解密

前言 下面将分享一些实际的渗透测试经验&#xff0c;帮助你应对在测试中遇到的数据包内容加密的情况。我们将以实战为主&#xff0c;技巧为辅&#xff0c;进入逆向的大门。 技巧 开局先讲一下技巧&#xff0c;掌握好了技巧&#xff0c;方便逆向的时候可以更加快速的找到关键函数…

arcgis实现截图/截屏功能

arcgis实现截图/截屏功能 文章目录 arcgis实现截图/截屏功能前言效果展示相关代码 前言 本篇将使用arcgis实现截图/截屏功能&#xff0c;类似于qq截图 效果展示 相关代码 <!DOCTYPE html> <html> <head><meta charset"utf-8"><meta nam…

突发:Do Kwon申请破产!

作者&#xff1a;秦晋 1月22日&#xff0c;据《彭博社》报道&#xff0c; 由Do Kwon联合创立的Terraform Labs Pte.数字资产公司在美国特拉华州申请破产保护。 根据周日在特拉华州提交的法庭文件显示&#xff0c;该公司的资产和负债估计均在1亿至5亿美元之间&#xff0c;债权人…

【Linux编译器-gcc/g++使用】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 设计样例&#xff0c;先见一下 方案一&#xff1a; 方案二&#xff1a; 在企业里面一般维护软件的源代码的话&#xff0c;要维护几份&#xff1f; 方案一&…

mysql数据库事务(事务设置、隔离级别、实现原理)

目录 事务 数据库事务 事务特性 事务设置 事务隔离级别 1.读未提交 2.读已提交 3.可重复读 4.串行化 事务实现原理 原子性&#xff1a;undolog 持久性&#xff1a;redolog 隔离性: 如果隔离级别是读已提交&#xff1a; 如果隔离级别是可重复读&#xff1a; 事务…

【Linux】 开始使用 gcc 吧!!!

Linux 1 认识gcc2 背景知识3 gcc 怎样完成 &#xff1f;3.1 预处理预处理^条件编译 3.2 编译3.3 汇编3.4 链接 4 函数库5 gcc 基本选项Thanks♪(&#xff65;ω&#xff65;)&#xff89;谢谢阅读下一篇文章见&#xff01;&#xff01;&#xff01; 1 认识gcc 我们在windows环…

系统架构设计师教程(十六)嵌入式系统架构设计理论与实践

嵌入式系统架构设计理论与实践 16.1 嵌入式系统概述16.1.1 嵌入式系统发展历程16.1.2 嵌人式系统硬件体系结构16.2 嵌入式系统软件架构原理与特征16.2.1 两种典型的嵌入式系统架构模式16.2.2 嵌入式操作系统16.2.3 嵌入式数据库16.2.4 嵌入式中间件16.2.5 嵌入式系统软件开发环…

智能风控体系之divergence评分卡简介

评分卡模型的出现据说最早是在20世纪40年代&#xff0c;Household Finance and Spiegel和芝加哥邮购公司第一次尝试在贷款决策过程中使用信用评分.但是这两家公司都终止了这项业务。后来&#xff0c;在20世纪50年代末&#xff0c;伊利诺伊州的美国投资公司&#xff08;AIC&…

《WebKit 技术内幕》学习之十四(1):调式机制

第14章 调试机制 支持调试HTML、CSS和JavaScript代码是浏览器或者渲染引擎需要提供的一项非常重要的功能&#xff0c;这里包括两种调试类型&#xff1a;其一是功能&#xff0c;其二是性能。功能调试能够帮助HTML开发者使用单步调试等技术来查找代码中的问题&#xff0c;性能调…

Spring Boot 模块工程(通过 Maven Archetype)建立

前言 看到我身边的朋友反馈说&#xff0c;IDEA 新建项目时&#xff0c;如果通过 Spring Initializr 来创建 Spring Boot , 已经无法选择 Java 8 版本&#xff0c;通过上小节的教程&#xff0c;不知道该如何创建 Spring Boot 模块工程。如下图所示&#xff1a; 一.IDEA 搭建 …

Kafka(八)使用Kafka构建数据管道

目录 1 使用场景2 构建数据管道时需要考虑的问题2.1 及时性2.2 可靠性高可用可靠性数据传递 2.3 高吞吐量2.4 数据格式2.5 转换ETLELT 2.6 安全性2.7 故障处理2.8 耦合性和灵活性临时数据管道元数据丢失末端处理 3 使用Connect API3.1 Connect的数据处理流程sourcesinkconnecto…

IP组播地址

目录 1.硬件组播 2.因特网范围内的组播 IP组播地址让源设备能够将分组发送给一组设备。属于多播组的设备将被分配一个组播组IP地址 组播地址范围为224.0.0.0~239.255.255.255(D类地址)&#xff0c;一个D类地址表示一个组播组。只能用作分组的目标地址。源地址总是为单播地址…

丝路昆仑文物展:启用网关,文物预防性保护设备数据无缝对接平台

一、多功能网关数据无缝流转 近日&#xff0c;“丝路昆仑——新疆文物精品展”在天津博物馆开展。展览分为三部分&#xff1a;“丝路前奏”、“丝路华响”和“丝路梵音”&#xff0c;前两部分是以张骞凿通西域前后的中原西域两地文化交流&#xff0c;第三部分则讲述了佛教沿西…

【并发】什么是 Future?

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;JAVA ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 关键特性和操作包括&#xff1a; 提交任务&#xff1a; 查询完成状态&#xff1a; 等待结果&#xff1a; 取消任务&#xff1a…

golang整合rabbitmq,创建交换机并绑定队列

1,如果要开发消息队列,需要创建交换机和队列,通常有2中方式创建,1种是在面板直接创建 2,第二种就是在代码中创建,这里 展示的是go语言代码中创建rabbitmq package mainimport ("fmt""log""github.com/streadway/amqp" )func main() {// 连接R…

年销180万辆的特斯拉,护城河却在崩塌

文&#xff5c;刘俊宏 2023年率先开启汽车价格战的马斯克&#xff0c;伤敌一百自损八千&#xff1f; 在1月25日的特斯拉2023Q4财报电话会上&#xff0c;特斯拉CEO马斯克对中国公司的竞争力如此感叹道&#xff0c;“要是没有贸易壁垒&#xff0c;他们将摧毁&#xff08;destroy…