基础算法--KMP字符串

news2024/12/27 12:03:42

KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:如何快速在「原字符串」中找到「匹配字符串」。
在朴素解法中,不考虑剪枝的话复杂度是 O(m∗n) 的,而 KMP 算法的复杂度为 O(m+n)。

KMP 之所以能够在O(m+n) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。

你可能不太理解,没关系,我们可以通过举个例子来理解 KMP。

1. 匹配过程

在模拟 KMP 匹配过程之前,我们先建立两个概念:

  • 前缀:对于字符串 abcxxxxefg,我们称 abc 属于 abcxxxxefg 的某个前缀。
  • 后缀:对于字符串 abcxxxxefg,我们称 efg 属于 abcxxxxefg 的某个后缀。

然后我们假设原串为 abeababeabf,匹配串为 abeabf
在这里插入图片描述

先看看如果不使用 KMP,会如何进行匹配(不使用 substring 函数的情况下)。
首先在「原串」和「匹配串」分别各自有一个指针指向当前匹配的位置。
首次匹配的「发起点」是第一个字符 a 。显然,后面的 abeab 都是匹配的,两个指针会同时往右移动(黑标)。
在都能匹配上 abeab 的部分,「朴素匹配」和「KMP」并无不同。
在这里插入图片描述

直到出现第一个不同的位置(红标):
接下来,正是「朴素匹配」和「KMP」出现不同的地方:

先看下「朴素匹配」逻辑:

  • 将原串的指针移动至本次「发起点」的下一个位置(b 字符处)(遍历原串);匹配串的指针移动至起始位置。
  • 尝试匹配,发现对不上,原串的指针会一直往后移动,直到能够与匹配串对上位置。

如图:
在这里插入图片描述

也就是说,对于「朴素匹配」而言,一旦匹配失败,将会将原串指针调整至下一个「发起点」,匹配串的指针调整至起始位置,然后重新尝试匹配。
这也就不难理解为什么「朴素匹配」的复杂度是O(m∗n) 了。

然后我们再看看「KMP 匹配」过程:
在这里插入图片描述

首先匹配串会检查之前已经匹配成功的部分中里是否存在相同的「前缀」和「后缀」。如果存在,则跳转到「前缀」的下一个位置继续往下匹配:
在这里插入图片描述
跳转到下一匹配位置后,尝试匹配,发现两个指针的字符对不上,并且此时匹配串指针前面不存在相同的「前缀」和「后缀」,这时候只能回到匹配串的起始位置重新开始。

到这里,你应该清楚 KMP 为什么相比于朴素解法更快:

因为 KMP 利用已匹配部分中相同的「前缀」和「后缀」来加速下一次的匹配。
因为 KMP 的原串指针不会进行回溯(没有朴素匹配中回到下一个「发起点」的过程)。

第一点很直观,也很好理解。
我们可以把重点放在第二点上,原串不回溯至「发起点」意味着什么?

其实是意味着:随着匹配过程的进行,原串指针的不断右移,我们本质上是在不断地在否决一些「不可能」的方案。当我们的原串指针从 i 位置后移到 j 位置,不仅仅代表着「原串」下标范围为 [i,j) 的字符与「匹配串」匹配或者不匹配,更是在否决那些以「原串」下标范围为 [i,j)为「匹配发起点」的子集。

2. 分析实现

我们分析一下复杂度。如果严格按照上述解法的话,最坏情况下我们需要扫描整个原串,复杂度为 O(n)。同时在每一次匹配失败时,去检查已匹配部分的相同「前缀」和「后缀」,跳转到相应的位置,如果不匹配则再检查前面部分是否有相同「前缀」和「后缀」,再跳转到相应的位置 … 这部分的复杂度是 O(m^2),因此整体的复杂度是 O(n * m^2),而我们的朴素解法是 O(m * n)O(m∗n) 的。

说明还有一些性质我们没有利用到。

显然,扫描完整原串操作这一操作是不可避免的,我们可以优化的只能是「检查已匹配部分的相同前缀和后缀」这一过程。

再进一步,我们检查「前缀」和「后缀」的目的其实是「为了确定匹配串中的下一段开始匹配的位置」。

同时我们发现,对于匹配串的任意一个位置而言,由该位置发起的下一个匹配点位置其实与原串无关。

举个 例子,对于匹配串 abcabd 的字符 d 而言,由它发起的下一个匹配点跳转必然是字符 c 的位置。因为字符 d 位置的相同「前缀」和「后缀」字符 ab 的下一位置就是字符 c。

可见从匹配串某个位置跳转下一个匹配位置这一过程是与原串无关的,我们将这一过程称为找 next 点。

显然我们可以预处理出 next 数组,数组中每个位置的值就是该下标应该跳转的目标位置( next 点)。

当我们进行了这一步优化之后,复杂度是多少呢?

预处理 next 数组的复杂度未知,匹配过程最多扫描完整个原串,复杂度为 O(n)。

因此如果我们希望整个 KMP 过程是 O(m + n)的话,那么我们需要在 O(m)的复杂度内预处理出 next数组。

所以我们的重点在于如何在 O(m)复杂度内处理处 next 数组。

3. next 数组的构建

接下来,我们看看 next 数组是如何在 O(m)的复杂度内被预处理出来的。

假设有匹配串 aaabbab,我们来看看对应的 next 是如何被构建出来的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这就是整个 next 数组的构建过程,时空复杂度均为 O(m)。

至此整个 KMP 匹配过程复杂度是 O(m + n)的。

4.算法实现

python代码

def build_next(patt):
    """
    计算 Next 数组
    """

    next = [0]  # next 数组(初值元素一个0)
    prefix_len = 0  # 当前共同前后缀的长度
    i = 1
    while i < len(patt):
        if patt[prefix_len] == patt[i]:
            prefix_len += 1
            next.append(prefix_len)
            i += 1
        else:
            if prefix_len == 0:
                next.append(0)
                i += 1
            else:
                prefix_len = next[prefix_len - 1]

    return next


def kmp_search(string, patt):
    next = build_next(patt)

    i = 0  # 主串中的指针
    j = 0  # 子串中的指针
    while i < len(string):
        if string[i] == patt[j]:  # 字符匹配, 指针后移
            i += 1
            j += 1
        elif j > 0:  # 字符失配,根据 next 跳过字串前面的一些字符
            j = next[j - 1]
        else:  # 子串第一个字符就失配
            i += 1

        if j == len(patt): # 匹配成功
            return i - j


string = "ABABABCAA"
patt = "ABABC"
ind = kmp_search(string, patt)
print(ind)

acwing 831 KMP字符串

#include <iostream>
#include <string>

using namespace std;

const int N = 10010;
const int M = 100010;
int ne[N];//把每一个点为终点的最长相同前缀和后缀的长度存在里面
char s[M], p[N];

int main() {
	int n, m;

	cin >> n >> p + 1 >> m >> s + 1;

	ne[1] = 0;//这里从1开始
	for (int i = 2, j = 0; i <= n; ++i) {
		while (j && p[j + 1] != p[i]) j = ne[j];
		
		if (p[j + 1] == p[i]) j++;

		ne[i] = j;
	}

	for (int i = 1, j = 0; i <= m; ++i) {
		while (j && s[i] != p[j + 1]) j = ne[j];

		if (s[i] == p[j + 1]) j++;
		if (j == n) {
		    j = ne[j];
			cout << i - n << ' ';
		}
	}

	return 0;
}

5.总结

KMP 算法的应用范围要比 Manacher 算法广,Manacher 算法只能应用于「回文串」问题,相对比较局限,而「子串匹配」问题还是十分常见的。

背过这样的算法的意义在于:相当于大脑里有了一个时间复杂度为 O(n) 的 api 可以使用,这个 api 传入一个原串和匹配串,返回匹配串在原串的位置。

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

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

相关文章

成都优优聚公司是靠谱的吗?

成都优优聚公司专业美团代运营团队&#xff0c;以高效、专业、全面的服务赢得了众多客户的青睐。作为一家在美团代运营行业具备丰富经验和优质资源的公司&#xff0c;我们始终以客户的需求为导向&#xff0c;致力于为客户打造出色的美团线上运营方案。 我们公司拥有一支经验丰富…

设计模式探索:从理论到实践的编码示例 (软件设计师笔记)

&#x1f600;前言 设计模式&#xff0c;作为软件工程领域的核心概念之一&#xff0c;向我们展示了开发过程中面对的典型问题的经典解决方案。这些模式不仅帮助开发者创建更加结构化、模块化和可维护的代码&#xff0c;而且也促进了代码的复用性。通过这篇文章&#xff0c;我们…

处理不平衡数据的十大Python库

数据不平衡是机器学习中一个常见的挑战&#xff0c;其中一个类的数量明显超过其他类&#xff0c;这可能导致有偏见的模型和较差的泛化。有各种Python库来帮助有效地处理不平衡数据。在本文中&#xff0c;我们将介绍用于处理机器学习中不平衡数据的十大Python库&#xff0c;并为…

面向对象之旅:核心理念、设计方法与UML详解(软件设计师笔记)

&#x1f600;前言 面向对象技术是现代软件工程的核心&#xff0c;为软件设计和开发带来了一种强大且有序的方法。通过将现实世界的实体和概念映射到可操作的代码结构&#xff0c;该技术使我们能够更高效、清晰和可靠地创建复杂的软件系统。在本章中&#xff0c;我们将详细介绍…

我的国庆回家之路

文章目录 回家的计划假期的礼物学习新技术与家人团聚遇到的趣事总结 &#x1f389;欢迎来到IT陈寒的博客空间~我的国庆回家之路 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a;IT陈寒的博客&#x1f388;该系列文章专栏&#xff1a;Java学习路线&#x1…

【运维日常】华为云专线实现idc通过nat出网

本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》&#xff1a;python零基础入门学习 《python运维脚本》&#xff1a; python运维脚本实践 《shell》&#xff1a;shell学习 《terraform》持续更新中&#xff1a;terraform_Aws学习零基础入门到最佳实战 《k8…

【VUE复习·9】v-for 基础用法(循环渲染也叫列表渲染)

总览 1.v-for 都能循环什么 2.用法 一、v-for 都能遍历什么 能循环的东西包括&#xff1a;数组、对象、字符串&#xff08;和java里面的3个引用数据类型一样&#xff09;、纯粹循环数量&#xff08;少用&#xff09; 二、用法 1.用法1&#xff1a;简单循环&#xff08;遍历…

开源协作开发者内容平台Vrite

什么是 Vrite &#xff1f; Vrite 是一个开源协作空间&#xff0c;用于创建、管理和部署产品文档、技术博客和知识库。它旨在提供高质量、集成的用户和开发人员体验。 Vrite 具有以下功能&#xff1a; 内置管理仪表板&#xff0c;用于使用看板或列表视图管理内容生产和交付&am…

亘古难题:前端开发 or 后端开发

目录 一、引言二、两者的对比分析1. 技能要求和专业知识前端开发后端开发 2. 职责和工作内容前端开发后端开发 3. 项目类型和应用领域前端开发后端开发 4. 就业前景和市场需求前端开发后端开发 三、技能转换和跨领域工作四、介绍全栈开发五、结语附、开源项目微服务商城项目前后…

Java获取给定月份的前N个月份和前N个季度

描述&#xff1a; 在项目开发过程中&#xff0c;遇到这样一个需求&#xff0c;即&#xff1a;给定某一月份&#xff0c;得到该月份前面的几个月份以及前面的几个季度。例如&#xff1a;给定2023-09&#xff0c;获取该月份前面的前3个月&#xff0c;即2023-08、2023-07、2023-0…

2023/9/30 -- ARM

今日任务&#xff1a;消息队列实现进程之间通信方式代码&#xff0c;现象 msgW.c: #include <myhead.h> //消息结构体 typedef struct {long msgtype; //消息类型char data[1024]; //消息正文 }Msg_ds;#define SIZE sizeof(Msg_ds)-sizeof(long) //…

cesium源码无法更新的解决方案

一、环境&#xff1a; 中国移动的宽带 win10操作系统 二、问题复现步骤&#xff1a; 1、开了VPN&#xff0c;设置为全局代理 2、在vscode中执行git pull命令 3、结果显示无法更新 三、解决方案&#xff1a; 1、安装Github官方开发的软件Github Desktop 下载地址&#xf…

(二)Python编程环境搭建

本章重点介绍 Python 编程环境的搭建&#xff0c;包括各个平台下 Python 的下载和安装&#xff0c;常见 IDE 的使用&#xff0c;如何运行 Python 程序等。还会亲自带着大家编写并运行第一个 Python 程序&#xff0c;让大家认识一下最简单的 Python 代码。 本章的后半部分还介绍…

【C++】C++多态——实现、重写、抽象类、原理

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;C学习 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【C】C继…

百度面试题:为什么使用接口而不是直接使用具体类?

大家好&#xff0c;我是小米&#xff01;今天&#xff0c;我要和大家聊聊一个在 Java 编程中非常重要的话题&#xff1a;“百度面试题&#xff1a;为什么要使用接口而不是直接使用具体类&#xff1f;”这个问题在很多 Java 面试中都会被问到&#xff0c;因为它涉及到了面向对象…

基于Java的学校运动会信息管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

mysql的mvcc详解

一 MVCC的作用 1.1 mvcc的作用 1.MVCC&#xff08;Multiversion Concurrency Control&#xff09;多版本并发控制。即通过数据行的多个版本管理来实现数据库的并发控制&#xff0c;使得在InnoDB事务隔离级别下执行一致性读操作有了保障。 2.mysql中的InnoDB中实现了MVCC主要…

力扣146|LRU缓存淘汰算法

LRU缓存淘汰算法 leet code146: https://leetcode.cn/problems/lru-cache 一、基本思想 1.1 基本思想 LRU全名Last Recently Used&#xff0c;即当缓存空间满时&#xff0c;优先淘汰最不常使用&#xff08;访问&#xff09;的缓存。 1.2 抽象接口 1、 init() 初始化大小为…

ITSM和ITIL有什么区别?

ITIL是最广泛接受的ITSM方法&#xff0c;是用于管理组织IT运营和服务的最佳实践和建议的框架。它是由英国政府的中央计算机和电信局&#xff08;CCTA&#xff09;在1980年代中期委托创建的。基于ITIL框架构建的ITSM流程为更好的IT服务和改善业务铺平了道路。总而言之&#xff0…

【Java】关于我Debug的一些技巧

文章目录 条件断点断点回退表达式的执行直接返回 条件断点 IDEA中右击断点的时候可以看到如下的状态&#xff0c;在这里的Condition中我们可以选择进入当前断点的一个条件&#xff0c;比如我希望只有keyuser2的时候才进入断点&#xff0c;那么我就可以按照如下的方式去编写条件…