一文讲清楚字符串搜索问题【朴素法】和【KMP算法】

news2024/11/25 8:21:42

文章目录

  • 一、引入
  • 二、朴素解法
    • 2.1 朴素法介绍
    • 2.2 图解朴素法
    • 2.3 复杂度分析
  • 三、KMP算法
    • 3.1 `nextArr` 数组介绍
    • 3.2 图解 `KMP` 算法
      • 3.2.1 原理
      • 3.2.2 实现
    • 3.3 `nextArr` 数组求解
    • 3.4 复杂度分析
  • 四、总结
  • 写在最后

一、引入

字符串搜索问题是字符串中重要的一类问题,该问题可以描述为:给定字符串 str1str2 ,找出字符串 str2str1 中的出现位置(返回起始位置索引),如果 str2 不是 str1 中的连续子字符串则返回 -1

我们今天主角—— KMP 算法可以在线性时间复杂内解决字符串搜索问题。在开始讲 KMP 算法之前,我们先来看一下朴素的字符串搜索方法是如何完成搜索任务的。

二、朴素解法

2.1 朴素法介绍

朴素的字符搜索算法从字符 str1[0] 开始与字符串 str2 进行匹配(匹配指的是判断对应字符是否相等),如果遇到无法匹配的字符,则需要从字符 str1[1] 开始与字符串 str2 进行匹配。如此匹配下去,直至找到 str2str1 中出现的位置,如果没有则返回 -1

2.2 图解朴素法

接下来以 str1 = "abbabc"str2 = "abc" 为例,图文并茂进行解释。

①首先从 str1 中的首字符 'a' 开始与 str2 进行匹配,a=ab=bb!=c ,出现了字符不匹配的现象;

朴素1

②从字符串 str1 的第二个字符开始进行匹配,b!=a

朴素2

③从 str1[2] 字符开始进行匹配,b!=a

朴素3

④从 str1[3] 开始匹配,a = ab = bc = c,于是找到了字符串 str2str1 中的位置 3

朴素4

2.3 复杂度分析

时间复杂度: O ( n × m ) O(n \times m) O(n×m),其中 n n n m m m 分别为字符串 str1str2 的长度。

空间复杂度: O ( 1 ) O(1) O(1)

三、KMP算法

Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个字符串S内查找一个词W的出现位置。一个词在不匹配时本身就包含足够的信息来确定下一个匹配可能的开始位置,此算法利用这一特性以避免重新检查先前配对的字符。

为了记录字符不匹配时的一些信息,首先需要了解真前缀、真后缀的定义。

前缀 指的是从字符串首位置开始到某个位置结束的连续子串,字符串 Si 位置处字符结尾的前缀可以表示为 P r e f i x ( S , i ) Prefix(S, i) Prefix(S,i) P r e f i x ( S , i ) = S [ 0... i ] Prefix(S, i)=S[0...i] Prefix(S,i)=S[0...i]

真前缀 指的是除了 S 本身的 S 的前缀。

举例来说,字符串 abbad 的所有前缀为 {a, ab, abb, abba, abbad},它的真前缀为 {a, ab, abb, abba}

后缀 指的是从字符串某个位置 i 开始到整个字符串结束位置的连续子串, 字符串 Si 位置开始的前缀可以表示为 S u f f i x ( S , i ) Suffix(S, i) Suffix(S,i) S u f f i x ( S , i ) = S [ i . . . n − 1 ] Suffix(S, i)=S[i...n-1] Suffix(S,i)=S[i...n1],其中 n n n 为字符串 S 的长度。

真后缀 指的是除了 S 本身的 S 的后缀。

举例来说,字符串 abbad 的所有后缀为 {d, ad, bad, bbad, abbad},它的真后缀为 {d, ad, bad, bbad}

3.1 nextArr 数组介绍

首先介绍 nextArr 数组,至于这个数组的用处,我们稍后再来说明。

字符串 strnextArr 数组这样表示:长度与字符串 str 长度一样,nextArr[i] 表示在 str[i] 字符之前的字符串 str[0...i-1] 中,真后缀与真前缀的最大匹配(相等)长度是多少。

举例来说,str= "aaaab",则nextArr[4]=3str2[4]='b',所以它之前的字符串为 "aaaa",该字符串的真前缀和真后缀的最大匹配字符串为 "aaa",长度为 3,所以 nextArr[4]=3

我们先明白 nextArr 数组的概念即可,至于求解方法我们将在 3.3 节进行介绍。

3.2 图解 KMP 算法

3.2.1 原理

在理解了 nextArr 数组的含义之后,KMP 算法就很好理解了,为了方便表示,我们用图示的方式对字符搜索匹配进行解释。

①假设我们从 str1[i] 字符出发,匹配到 j 位置时发现字符不一致,如下图所示,str1[j] != str2[j-i]

②我们计算字符串 str2nextArr 数组,nextArr[j-i] 表示str2[0...j-i-1] 字符串真前缀和真后缀的最大匹配长度。假设真前缀为下图中 a 区域所示,真后缀为下图中 b 区域区域所示,a 区域后的第一个字符记为 str2[k]

③在下一次检查中,可以直接从 str1[j]str2[k] 处开始,不需要向朴素解法那样从 str1[i+1]str2[0] 处开始。 因为直到 jj-i 位置才有 str1[j] != str2[j-i],所以 c 区域等于 b 区域,而 b 区域又和 a 区域是一样的,所以下一次匹配可以直接从 str1[j]str2[k] 处开始(其实是从 c 区域开始位置与 str2 进行匹配,而 b 区域又和 a 区域是一样的,所以下一次匹配可以直接从 str1[j]str2[k] 处开始)。

④现在有一个疑问,str1[i] 与 c 之间的区域为什么不用与字符串 str2 进行匹配,因为在这个区域中,从任何一个字符出发都匹配不出 str2。现在用反证法简单证明一下。

假设 str1[i] 与 c 之间的区域能与字符串 str2 匹配,那么 str1 中的 d 区域应该和以字符 str2[0] 开始的区域 e 匹配。现在假设 d 区域对应到 str2 中的区域为 d’,那么有 d = e = d',因为 d' > be > a,所以我们现在找到了字符 str2[j-i] 之前字符串的更大的真前缀与真后缀匹配长度,这与 a、b 区域是最大的真前缀、真后缀匹配长度矛盾,所以原始的假设不成立,即str1[i] 与 c 之间的区域为什么不用与字符串 str2 进行匹配。

3.2.2 实现

经过以上的分析,我们知道了在进行字符搜索匹配的时候,遇到了 str2[j-i] 字符无法与 str1[j] 匹配时是利用 nextArr[j] 来加速匹配的。现在假设我们已经实现了求 nextArr 数组的函数 getNextArray(),利用该函数求字符串 str2nextArr 数组并实现 KMP 算法。

先贴出代码,再进行分析:

// 判断字符串s2是否是字符串s1的子串,若是返回起始位置索引,否则返回 -1 
int kmp(string s1, string s2) {
  if(s1.size() == 0 || s2.size() == 0 || s1.size() < s2.size()) { // (0)
      return -1;
  }
  
  int i1 = 0, i2 = 0;
  vector<int> next = getNextArray(s2);	// (1)
  
  while (i1 < s1.size() && i2 < s2.size()) {
      if (s1[i1] == s2[i2]) {			    // (2)
          ++i1;
          ++i2;
      }
      else if (next[i2] == -1) {		    // (3) 字符串s2中的位置已经无法再往前跳了
          ++i1;
      }
      else {
          i2 = next[i2];                  // (4)
      }
  }
  return i2 == s2.size() ? i1 - i2 : -1;  // (5)
}

(0)一些特判的情况;

(1)通过 getNextArray(s2) 得到字符串 s2nextArr 数组;

(2)利用双指针进行字符匹配,i1 指向字符 s1i2 指向字符 s2,匹配上了就同时向右移动指针;

(3)next[i2] == -1 可以使用 i2 == 0 代替表示的含义都一样。此时指针 i2 指向了字符 s[0],并且 s1[i1] != s2[0],所以向 右移动 i1 指针,开始新的一轮匹配;

(4)指针 i2 跳到 s2[0...i2-1] 的真前缀的下一个字符开始匹配。

(5)最后,如果 i2 指针走完了字符串 s2,则返回 s2s1 中首先出现的位置,否则表示 s2s1 的连续子串。

3.3 nextArr 数组求解

我们已经明白了 nextArr 数组的概念及其存在的重要意义,接下来就来看看该数组到底应该怎么求。

字符串 snextArr 数组:表示的当前字符前面字符串的真前缀和真后缀的最大匹配长度。字符串 snextArr[0] = -1,因为字符 s[0] 之前没有任何字符,我们初始为 -1nextArr[1] = 0,因为 s[1] 前面只有一个字符,一个字符没有真前缀和真后缀,所以为 0

先贴出代码,再进行分析:

// nextArr数组计算
vector<int> getNextArray(string s) {
    if (s.size() == 1) {			// (0)
        return {-1};
    }
    
    int n = s.size();
    vector<int> next(n);
    next[0] = -1;
    next[1] = 0;
    int i = 2; 	// next数组的位置
    int cn = 0;	// 两层含义:1.最大匹配长度 2.真前缀后面第一个字符的位置(即将与s[i-1]字符比较的字符)
    
    while (i < n) {
        if (s[i-1] == s[cn]) {		// (1)
            // next[i++] = ++cn;
            next[i] = cnt + 1;
            ++i;
            ++cnt;
        }
        else if (cn > 0) {			// (2)
            cn = next[cn];
        }
        else {
            next[i++] = 0;			// (3)
        }
    }
    
    return next;
}

(0)字符串 s 长度为 1 时,nextArr = {-1};否则长度 >= 2next[1] = 0,接下来分析 n >= 3 时,nextArr 数组是如何更新的;

(1)i 表示当前要求 nextArr[i] 的位置,cn 有两层含义:一是指字符串真前缀与真后缀的最大匹配长度,二是指真前缀后面第一个字符位置。当在计算 nextArr[i] 时表明 nextArr[i-1] 已经计算得到,可以直接使用。

nextArr[i-1] = nextArr[cn] 时,表示此时 s[0...i-1] 中真前缀与真后缀的最大匹配字符串为 a 区域字符串加上 s[cn] 字符(b 区域字符串加上 s[i-1] 字符),最大匹配长度为 cn + 1,接下里需要将 i++cn++ 为下一个位置的 nextArr 数组计算做准备。该情况下的执行代码可以等价替换为 nextArr[i++] = ++cnt;

为什么要对 cn ++ 呢?因为 cn 始终表示的是真前缀后面第一个字符的位置。在计算下一个位置 nextArr[i+1] 时需要使用 nextArr[i]nextArr[i] 的真前缀这时更新为 a 区域加上 s[cn] 字符了,所以即将与 s[i] 字符比较的字符就是 cn+1 位置的字符。

(2)当 nextArr[i-1] != nextArr[cn] 时,s[i-1] 将与新的 cn(图中的 cn')进行比较。

下图中的 n、m 区域分别为 a 区域字符的真前缀和真后缀,于是有 m = n;图中 a 区域和 b 区域分别为字符串 s[0...i-1] 的真前缀和真后缀,于是有 m = m',因此 n = m';此时如果 s[i-1] = s[cn'],就有 nextArr[i] = cn' + 1,似乎与 (1) 中的判断类似,只是 cn 变成了 cn',而恰好 cn' = nextArr[cn]cn 的定义使然)。于是情况 (2) 只需要更新 cn 即可,判断让 (1) 处来做。

(3)此时若是 cn = 0 了,表示 s[0...i-1] 没有匹配的真前缀和真后缀,于是 nextArr[i] == 0,更新 i ++ 为计算下一个位置的 nextArr 数组做准备。

3.4 复杂度分析

求解字符串 snextArr 数组的时间复杂度为 O ( m ) O(m) O(m) m m m 是字符串的长度,空间复杂度为 O ( n ) O(n) O(n)

KMP 算法的时间复杂度为 O ( n ) O(n) O(n) n n n 是搜索的原字符串的长度,空间复杂度为 O ( 1 ) O(1) O(1)

四、总结

本篇博文主要讲述了字符串搜索算法中的朴素法和具有线性时间复杂度的 KMP 法。

KMP 算法的核心之处在于借助 nextArr 数组进行加速。

以上求解 nextArr 数组以及 KMP 算法的代码,可以作为模板进行记忆。对于此类经典的算法,一次看不懂不怕,就怕不坚持。

写在最后

以上就是本篇文章的内容了,感谢您的阅读。🍗🍗🍗

如果感到有所收获的话可以给博主点一个 👍 哦。

如果文章内容有任何错误或者您对文章有任何疑问,欢迎私信博主或者在评论区指出。💬💬💬

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

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

相关文章

C#通过ModbusTcp协议读写西门子PLC中的浮点数

一、Modbus TCP通信概述 MODBUS/TCP是简单的、中立厂商的用于管理和控制自动化设备的MODBUS系列通讯协议的派生产品&#xff0c;显而易见&#xff0c;它覆盖了使用TCP/IP协议的“Intranet”和“Internet”环境中MODBUS报文的用途。协议的最通用用途是为诸如PLC&#xff0c;I/…

什么是rem单位和em单位?它们有什么区别?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ rem 和 em 单位⭐ rem 单位&#xff08;Root Em&#xff09;⭐ em 单位⭐ 区别总结⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入…

.net core 上传文件大小限制

微软官网文档中给的解释是.net core 默认上传文件大小限制是30M&#xff0c;所以即便你项目里没有限制&#xff0c;这里也有个默认限制。 官网链接地址 总结了一下解决办法&#xff1a; 1.首先项目里添加一个web.config自定义配置文件 在配置文件中加上这段配置 <!--//…

2024年java面试--多线程(3)

系列文章目录 2024年java面试&#xff08;一&#xff09;–spring篇2024年java面试&#xff08;二&#xff09;–spring篇2024年java面试&#xff08;三&#xff09;–spring篇2024年java面试&#xff08;四&#xff09;–spring篇2024年java面试–集合篇2024年java面试–redi…

Echarts使用

Echarts使用 1.ECharts简介 ECharts是一个使用JavaScript实现的开源可视化库&#xff0c;可以流畅的运行在PC和移动设备上&#xff0c;兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等)&#xff0c;底层依赖矢量图形库ZRender,.提供直观&#xff0c;交互丰富&am…

Spring Cloud--从零开始搭建微服务基础环境【四】

&#x1f600;前言 本篇博文是关于Spring Cloud–从零开始搭建微服务基础环境【四】&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;…

自然语言处理: 第十一章BERT(Bidirectional Encoder Representation from Transformers)

论文地址:[1810.04805] BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding (arxiv.org) 理论基础 之前介绍GPT的搭建的方式的时候&#xff0c;将BERT与GPT进行了对比&#xff0c;我们可以知道BERT和GPT都是基于transformer架构下的分支&…

Android 1.2.1 使用Eclipse + ADT + SDK开发Android APP

1.2.1 使用Eclipse ADT SDK开发Android APP 1.前言 这里我们有两条路可以选&#xff0c;直接使用封装好的用于开发Android的ADT Bundle&#xff0c;或者自己进行配置 因为谷歌已经放弃了ADT的更新&#xff0c;官网上也取消的下载链接&#xff0c;这里提供谷歌放弃更新前最新…

PHP8数组的类型-PHP8知识详解

php 8 引入了对数组的类型提示&#xff0c;以帮助开发者更准确地定义和验证数组的结构。以下是 PHP 8 中支持的数组类型&#xff1a;索引数组、关联数组、混合类型数组。 1、索引数组 (Indexed arrays): PHP索引数组一般表示数组元素在数组中的位置&#xff0c;它由数字组成&a…

centos7关闭防火墙和selinux(内核防火墙)

centos7关闭防火墙和selinux&#xff08;内核机制或叫内核防火墙&#xff09; 小白教程&#xff0c;一看就会&#xff0c;一做就成。 1.关闭防火墙&#xff0c;centos7默认是firewalld #关闭 systemctl stop firewalld.service #关闭开机自启 systemctl disable firewalld.ser…

Opencv 图像金字塔----高斯和拉普拉斯

原文&#xff1a;图像金字塔----高斯和拉普拉斯 图像金字塔是图像中多尺度表达的一种&#xff0c;最初用于机器视觉和图像压缩&#xff0c;最主要用于图像的分割、融合。 高斯金字塔 ( Gaussian pyramid): 高斯金字塔是由底部的最大分辨率图像逐次向下采样得到的一系列图像…

Vue.js 报错:Cannot read property ‘validate‘ of undefined“

错误解决 起因&#xff0c;是我将elemnt-ui登录&#xff0c;默认放在mounted()函数里面&#xff0c;导致vue初始化就调用这个函数。 找了网上&#xff0c;有以下错误原因&#xff1a; 1.一个是你ref写错了&#xff0c;导致获取不了这个表单dom&#xff0c;我这显然不是。 2.…

基于改进莱维飞行和混沌映射的粒子群优化算法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

机器人中的数值优化(十)——线性共轭梯度法

本系列文章主要是我在学习《数值优化》过程中的一些笔记和相关思考&#xff0c;主要的学习资料是深蓝学院的课程《机器人中的数值优化》和高立编著的《数值最优化方法》等&#xff0c;本系列文章篇数较多&#xff0c;不定期更新&#xff0c;上半部分介绍无约束优化&#xff0c;…

【Linux】如何手动挂载和卸载文件系统?

按块设备名称挂载按文件系统UUID挂载卸载文件系统感谢 &#x1f496; 我们必须了解&#xff0c;只有root用户可以手动挂载和卸载文件系统。 当我们切换到root用户后&#xff0c;可以使用mount命令将存储设备上的文件系统挂载到文件系统层次结构中用作挂载点的目录。 mount 命令…

Beego项目实战

Beego项目实战 beego博客项目-创建项目beego博客项目-集成gormbeego博客项目-集成Bootstrap创建用户表单beego项目实现-添加用户controller和routerBeego博客项目-设计静态页面beego博客项目-用户注册beego博客项目-用户登录beego博客项目-集成markdown编辑器beego博客项目-创建…

CCF是什么?

CCF是计算机学会&#xff08;China Computer Federation&#xff09;的英文缩写&#xff0c;是一个在中国从事计算机领域学术研究和技术发展的国家性非营利学术团体。其宗旨是促进和推动计算机科学技术的发展和应用&#xff0c;发挥学术团体在学术研究、学术交流、学术评价、学…

伺服阀放大器使用手册

控制通用型不带反馈信号输入的伺服阀放大器&#xff0c;对射流管式电液伺服阀、喷嘴挡板式电液伺服阀及国外各类电液伺服阀进行控制。 通过系统参数有10V和4~20mA输入指令信号选择&#xff1b; 供电电源: 24VDC&#xff08;标准&#xff09; 输出电流&#xff1a;最大可达10…

PHP8的多维数组-PHP8知识详解

今天分享的是php8的数组中的多维数组&#xff0c;主要内容有&#xff1a;多维数组的概念、创建和输出二维数组、创建和输出三维数组。 1、多维数组的概念 多维数组是包含一个或多个数组的数组。在多维数组中&#xff0c;主数组中的每一个元素也可以是一个数组&#xff0c;子数…

DHTMLX Gantt 8.0.5 Crack -甘特图

8.0.5 2023 年 9 月 1 日。错误修复版本 修复 修复通过gantt.getGanttInstance配置启用扩展而触发的错误警告修复启用skip_off_time配置时gantt.exportToExcel()的不正确工作示例查看器的改进 8.0.4 2023 年 7 月 31 日。错误修复版本 修复 修复数据处理器不跟踪资源数据…