算法:KMP算法详解

server/2024/10/23 6:44:12/

朴素的BF算法

BF算法即暴力求解字符串匹配的算法
在这里插入图片描述
面对这样两个字符串,

BF算法就是用两个指针,一个i,一个j,分别从s和t的开始位置开始依次匹配
当遇到s[i] == t[0]的时候,此时有可能字符串匹配,需要进行检查
于是从i开始,依次比较i后面的元素和t中的元素,
如果恰好完全相等(次序,字母都相同),则返回i的下标即可
如果遇到了s[i] != t[j],则说明此处不匹配,i需要回退到比较前的i的下一个位置,j需要回退到0,重新开始进行比较

这种算法的时间复杂度是O(m*N) ,N为S串长度,m为T串长度

这种算法的优点是算法思路非常简单,实现起来很容易,库里面的strstr函数就是使用的BF算法
缺点也很明显,时间复杂度稍微有点高


KMP算法

为了提高字符串匹配的效率,D.E.Knuth、J.H.MorTis和V.R.Pratt三位大佬提出了KMP算法

我们发现,限制BF算法的效率最大的障碍,在于回退,回退重复了大量的相同步骤
所以,想要提升字符串匹配的效率核心的问题在于如何避免或者减少下标回退的问题

KMP算法中将这个问题处理成了让i不回退,让j尽量少的回退

样例引入

在这里插入图片描述

我们来看这样一个字符串
主串是 a b c a b a b c a b c
子串是 a b c a b c

当i 走到图中a,j 走到图中c的位置
第一次出现了不匹配,此时注意,
我们可以发现,主串a前面出现的ab在子串c前面也出现过,并且在子串开头也出现过
如果我们把j回退到子串中下标为2的位置,是不是相当于,ab已经匹配好了,开始匹配第三个位置
于是这就减少了回退的距离

在这里插入图片描述

原理分析

此时只是无数例子中的其中一个例子,如何将这种规律普遍化呢?

我们来分析一下规律,是不是有些小伙伴以为j的回退与主串中的ab有关系,实际上是没有关系的,只与子串中的ab有关系
为什么这么说呢?

来,我们这么想,当i走到a位置,c走到j位置的时候,i前面的串和j前面的串一定是匹配的,不然j不可能可能走到这里,所以主串中的ab,一定和j前面最近的ab是相同的,
所以回退到下标为2的位置与主串没有关系
只与子串中开头出现了ab有关系

因此,我们继续研究子串

我们需要寻找的是j前面的子串和开头位置的子串相同
也就是,我们需要寻找这样的串
t[0]……t[x] = t[y]……t[j-1],即以t[0]字符开始,t[j-1]字符结束的相同串
这样,出现匹配失败后,我们j就可以回退到下标为x+1的位置

看完原理后,读者可以尝试手动求一下下面图中的两个题下标练练手(虽然给出了答案,但是还是先手算一下吧)
在这里插入图片描述

原理实现思路

j尽量少回退的原理我们懂了,那如何实现j尽量少回退呢?
首先我们引入一个next数组,用来存放j应该回退到的位置

我们特殊设置next[0] = -1 ,next[1] = 0,这两个位置情况特殊,找不到两个串

问题就转变成了如何求next数组
我们计next[j] = k

当我们跳转到k的时候,我们知道t[0]……t[k-1] == t[j-k]……t[j-1]

我们分成两种情况讨论(sub为子串,即t串,src为主串,即s串)

  1. sub[k] == sub[j]
    此时相等的串继续增加,得到sub[0]……sub[k] == sub[j-k]……sub[j]
    因此next[j+1] = k + 1
  2. sub[k] != sub[j]
    此时k可以继续循环k = next[k],找到一处合适的k,满足条件后走情况一即可。
  3. 还有一种特殊情况,当k 循环到了 next[0] = -1的时候,next数组中-1的下标肯定是不能继续访问的,
    此时,说明在子串中找不到符合要求的两个串,直接下标给0即可,此种情况放到情况一种恰好满足,
    当然也可特殊写一下

开始匹配

next数组求出来之后,我们就已经解决了KMP算法的核心问题,接下来只需要进行简单的匹配就行了
按照前面说的,i不会退,j尽量少回退的原则进行匹配即可。

遍历主串,
当src[i] == sub[j] 的时候,++i, ++j
当src[i[ != sub[j] 的时候,j = next[j]重新比较即可
当j到了-1的时候,也就是子串的第一个字符都不匹配,那主串这个字符不可能匹配,直接++i,++j即可,可归类到情况一种

时间复杂度分析

在KMP算法中,i并未回退,j近似未回退,也就是相当于遍历了两个串
时间复杂度为O(m + N)

为什么BF算法沿用至今

但在一般情况下暴力匹配算法执行时间也近似为O(m+n),O(M*N)是最坏情况,但是最坏情况出现的概率比较小,毕竟我们寻找的子串大概率是一组无序的字符组成的,而不是有很多重复。

代码

vector<int> get_next(string sub)
{int n = sub.size();vector<int> next(n);next[0] = -1, next[1] = 0;for (int i = 2; i < n; ++i){int k = next[i - 1];while (1){if (k == -1 || sub[i - 1] == sub[k]){next[i] = k + 1;break;}else{k = next[k];}}}return next;
}int KMP(string src, string sub, int pos)
{int n = sub.size();vector<int> next(n);//存放next数组next = get_next(sub);//求next数组int i = 0, j = 0;while (i < (int)src.size() && j < (int)sub.size()){if (j == -1 || src[i] == sub[j]){++i;++j;}else{j = next[j];}}if (j == sub.size())//找到了{return i - j;}if (i == src.size())//找不到return -1;
}

http://www.ppmy.cn/server/134102.html

相关文章

线性可分支持向量机的原理推导 9-32线性分类超平面的位置 公式解析

本文是将文章《线性可分支持向量机的原理推导》中的公式单独拿出来做一个详细的解析&#xff0c;便于初学者更好的理解。 公式 9-32 是线性可分支持向量机&#xff08;SVM&#xff09;中的一个关键公式&#xff0c;用于表达线性分类超平面的位置。通过这个公式&#xff0c;我们…

Scrapy | Scrapy框架中管道的使用

管道的使用 基本使用如何在管道中区分不同的爬虫 在Scrapy中&#xff0c;爬虫管道&#xff08;Item Pipeline&#xff09;是用于处理Spider提取的数据的一系列组件。它们的主要职责是清洗、验证和存储爬取的数据。每个管道组件是一个Python类&#xff0c;这些类必须定义一个pro…

黑盒测试和白盒测试的具体方法(附加实际应用中的技巧和注意事项)

黑盒测试的具体方法 黑盒测试有多种具体的方法&#xff0c;以下是几种常见的黑盒测试技术&#xff1a; 等价类划分 定义&#xff1a;将输入数据划分为若干等价类&#xff0c;每个等价类中的数据被认为是等效的。目的&#xff1a;减少测试用例数量&#xff0c;同时覆盖所有可…

QT的文件操作类 QFile

QFile 是 Qt 框架中用于文件处理的一个类。它提供了读取和写入文件的功能&#xff0c;支持文本和二进制文 件。 QFile 继承自 QIODevice &#xff0c;因此它可以像其他IO设备一样使用。 主要功能 文件读写&#xff1a; QFile 支持打开文件进行读取或写入操作文件信息&#x…

Unity性能优化

前言 当游戏开发使用传统的OPP&#xff08;面向对象编程&#xff09;面对大量的Game object时FPS会显著降低&#xff0c;而使用Dots&#xff08;面向数据编程&#xff09;性能依旧很好 计算机内存基础 CPU自身有三级高速缓存&#xff0c;L1,L2,L3,其中CPU访问&#xff08;L1…

Django学习-f对象和

F对象&#xff1a; Q对象&#xff1a;

HW支持-定时扫描局域网内所有设备MAC不在白名单则邮件提醒

需求背景 护网行动&#xff0c;是公安部组织的安全攻防演练活动。 曾经有被新安装的校园卡刷卡机黑到内网的经历&#xff0c;所以尽可能在护网期间能关就关&#xff0c;不新增设备。发现异常接入内网的设备即时进行提醒和处理。 实现步骤 MAC地址白名单放在一个txt文件中&…

搭建自己的Docker(容器)镜像加速器

容器镜像加速服务器 本Github项目可快速部署容器镜像加速服务器。 由于配置格式及docker客户端配置限制&#xff0c; 本项目仅适用于使用containerd runtime的容器镜像加速。 本项目两个分支&#xff1a; main: 使用nginx作为反向代理traefik: 使用traefik进行流量路由 前置…