字符串匹配(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 - 1
        for (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-1
        for (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;
    else
        cout << 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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习】线性回归模型与梯度下降法

线性回归模型与梯度下降法 线性回归模型与枚举法 线性回归模型定义: w:权重b:偏置#mermaid-svg-ZAxF27Mw5dXNQgw2 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ZAxF27Mw5dXNQgw2 .error-icon{fill:#552222;}…

pyecharts模块的下载方法以及介绍,折线图的创立

目录 1.pyecharts是什么 2.pyecharts下载方法 1.在屏幕左下角搜索这里输入cmd&#xff0c;找到命令提示符并且打开 2.输入pip install pyecharts 然后回车进行下载 3.检查是否下载完成 4.另一个方法 3.pyecharts入门 4.pyecharts的配置选项 set_global_opts全局配置选…

[docker] Docker资源管理

一、docker资源控制 Docker通过Cgroup 来控制容器使用的资源配额&#xff0c;包括CPU、内存、磁盘三大方面&#xff0c;基本覆盖了常见的资源配额和使用量控制。Caroup 是ControlGroups的缩写&#xff0c;是Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如…

OpenKruise :Kubernetes背后的托底

一、 诞生背景 Kubernetes 自身提供的应用部署管理功能&#xff0c;无法满足大规模应用场景的需求&#xff0c;例如应用发布时的原地升级策略&#xff0c;流式扩容&#xff0c;缩容顺序控制等等。所以OpenKruise的出现弥补了 Kubernetes 在应用部署、升级、防护、运维等领域的不…

剪映声音克隆;多位滴滴前中高层加入小红书提速商业化;中国和新加坡互免签证

今日精选 • 剪映推出 AI 音色克隆功能&#xff0c;录制 5 秒声音即可完成克隆• 商业化全面提速&#xff0c;多位滴滴前中高层加入小红书• 2 月 9 日起&#xff0c;中国和新加坡互免签证 科技动态 • 夸克上线大模型新产品“AI PPT”&#xff0c;可一键生成提纲、创作 PPT…

Unity - gamma space下还原linear space效果

文章目录 环境目的环境问题实践结果处理要点处理细节【OnPostProcessTexture 实现 sRGB 2 Linear 编码】 - 预处理【封装个简单的 *.cginc】 - shader runtime【shader需要gamma space下还原记得 #define _RECOVERY_LINEAR_IN_GAMMA】【颜色参数应用前 和 颜色贴图采样后】【灯…

接口自动化测试实践

众所周知&#xff0c;接口自动化测试有着如下特点&#xff1a; 低投入&#xff0c;高产出。 比较容易实现自动化。 和UI自动化测试相比更加稳定。 如何做好一个接口自动化测试项目呢&#xff1f; 我认为&#xff0c;一个“好的”自动化测试项目&#xff0c;需要从“时间”…

【算法练习Day51】柱状图中最大的矩形

​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;练题 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 柱状图中最大的矩形思路动态…

HTML+CSS:飞翔按钮

效果演示 实现了一个按钮的动画效果&#xff0c;当鼠标悬停在按钮上时&#xff0c;按钮的背景颜色和图标会发生变化&#xff0c;并且图标会旋转45度并向右移动1.2em&#xff0c;同时按钮中的文字也会向右移动5em。当鼠标点击按钮时&#xff0c;按钮会变小并向下移动0.1em。整个…

软考复习之软件工程篇

软件生命周期 问题定义&#xff1a;要示系统分析员与用户进行交流&#xff0c;弄清”用户需要计算机解决什么问题”然后提出关于“系统目标与范围的说明”&#xff0c;提交用户审查和确认 可行性研究&#xff1a;一方面在于把待开发的系统的目标以明确的语言描述出来&#xf…

LINUX服务之YUM仓库

1. YUM概述 YUM基于RPM包构建的软件更新机制 可以自动解决依赖关系 所有软件包由集中的YUM软件仓库提供 YUM支持软件源 搭建yum支持的的软件源主要有以下三种&#xff1a; 本地yum&#xff1a;file&#xff1a;//… 网络yum&#xff0c;又分为HTTP服务器&#xff1a;http…

Vue3 watch与watchEffect区别

✨ 专栏介绍 在当今Web开发领域中&#xff0c;构建交互性强、可复用且易于维护的用户界面是至关重要的。而Vue.js作为一款现代化且流行的JavaScript框架&#xff0c;正是为了满足这些需求而诞生。它采用了MVVM架构模式&#xff0c;并通过数据驱动和组件化的方式&#xff0c;使…

从全流程的角度来了解python包的使用,也许你会有不一样的认识

在python中&#xff0c;只要我们一谈到包或模块&#xff0c;基本默认说的就是包的导入和使用。也就是说只要我们知道包的名字&#xff0c;导入后知道怎么使用基本就可以了&#xff0c;但本人认为&#xff0c;我们仅仅了解的是包的一部分&#xff0c;若想对包有个整体的认识&…

376. 摆动序列 - 力扣(LeetCode)

题目描述 如果连续数字之间的差严格地在正数和负数之间交替&#xff0c;则数字序列称为摆动序列。第一个差&#xff08;如果存在的话&#xff09;可能是正数或负数。少于两个元素的序列也是摆动序列。 例如&#xff0c; [1,7,4,9,2,5] 是一个摆动序列&#xff0c;因为差值 (6,…

【机器学习300问】15、什么是逻辑回归模型?

一、逻辑回归模型是为了解决什么问题&#xff1f; 逻辑回归&#xff08;Logistic Regression&#xff09;是一种广义线性回归分析模型&#xff0c;尤其适用于解决二分类问题&#xff08;输出为两个类别&#xff09;。 &#xff08;1&#xff09;二分类举例 邮件过滤&#xff…

详解BLDC和PMSM的特点

文章目录 前言BLDC和PMSM的优点基础架构前言 在电机领域中,有刷电机和无刷电机代表着两种不同的技术路径。有刷电机的绕组通常位于转子,即电机的旋转部分。 而无刷电机则采用一种更为先进的设计,其绕组安置在定子,即电机的静止部分。 这样的设计理念在于将绕组固定在电机的…

深入理解stress/stress-ng

文章目录 一、概述二、安装2.1、源码编译安装2.2、命令行安装2.3、安装确认 三、重要参数详解3.1、查询支持的参数3.2、重要参数说明 四、实例4.1、压测CPU4.2、压测内存4.3、压测IO4.4、压测磁盘及IO4.5、压测磁盘及CPU 团队博客: 汽车电子社区 一、概述 stress是一种工作负载…

【AIGC】Diffusers:AutoPipeline自动化扩散生图管道

前言 &#x1f917; 扩散器能够完成许多不同的任务&#xff0c;并且您通常可以将相同的预训练权重用于多个任务&#xff0c;例如文本到图像、图像到图像和修复。但是&#xff0c;如果您不熟悉库和扩散模型&#xff0c;可能很难知道将哪个管道用于任务。例如&#xff0c;如果您…

新闻界的AI革命:Newspager GPT 全面解析

简介有没有想过一家报社是如何运作的&#xff1f;传统的报社要有策划、采编、编辑、美工、审校等等角色&#xff0c;而现在借助 AI&#xff0c;很多事情可以由 AI 代替了&#xff01;Newspager GPT 就是这样一个由多智能体组成的 AI 系统&#xff0c;你只要输入几个你感兴趣的主…

Javaweb之SpringBootWeb案例之阿里云OSS服务入门的详细解析

2.3.2 入门 阿里云oss 对象存储服务的准备工作我们已经完成了&#xff0c;接下来我们就来完成第二步操作&#xff1a;参照官方所提供的sdk示例来编写入门程序。 首先我们需要来打开阿里云OSS的官方文档&#xff0c;在官方文档中找到 SDK 的示例代码&#xff1a; 参照官方提供…