字符串匹配【BF、KMP算法】

文章目录

  • :star:BF算法
    • 代码实现
    • BF的改进思路
  • :star:KMP算法
    • 🚩next数组🚩
    • 代码实现
    • 优化next数组
    • 最终代码

⭐️BF算法

BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将主串S的第一个字符与模式串P的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。

代码实现

int BF(const char* S, const char* P)
{
	assert(S && P);
	int i = 0;
	int j = 0;
	int lenS = strlen(S);
	int lenP = strlen(P);
	while (i < lenS && j < lenP)
	{
		if (S[i] == P[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i - j + 1;
			j = 0;
		}
	}
	if (j >= lenP)
	{
		return i - j;
	}
	return -1;
}
return -1;
}

BF的改进思路

在最坏的情况下,模式串只有最后一个字符和主串不一样
在这里插入图片描述
BF最坏的情况时间复杂度是O(mn),效率太低了
我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。因此,我们考虑降低比较的趟数。如果比较的趟数能降到足够低,那么总的复杂度也将会下降很多。要优化一个算法,首先要回答的问题是“我手上有什么信息?” 我们手上的信息是否足够、是否有效,决定了我们能把算法优化到何种程度。请记住:尽可能利用残余的信息,是KMP算法的思想所在。

⭐️KMP算法

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)

区别:KMP 和 BF 唯一不一样的地方在,主串的 str1 并不会回退,并且 str2 也不会移动到 0 号位置,而是移动到某一个特别位置

上面概念有点抽象,我们举一个具体例子
在这里插入图片描述
请记住:KMP算法和BF算法不同的是BF中发现匹配失败str1会回退到前一个位置的下一个位置,str2回退到模式串起始位置,KMP匹配失败后str1(i)不回退str2(j)回退到某一个位置,这个位置可能是模式串的起始位置,也可能不是起始位置。
如果j想回退到某一个位置,这个位置应该满足这几个条件

  1. j回退的位置一定要尽可能靠近回退前的位置,这样可以保证后续循环的次数更少
  2. j回退的位置前面k个字符一定要和i前面k个字符相等

第二个原因是因为i是不动的,所以接下来想要从移动后的j继续向后匹配的前提就是i前面k个字符和移动后的j前面k个字符完全相同

我们将最终目的是寻找j的回退位置,现在我们已经知道了回退位置所需要满足的条件,根据第2个条件,我们可以推出j回退的位置前面k个字符和j回退前的k个字符相等
这是因为j回退后位置的前k个字符和i前k个字符相等,而i位置的前k个字符右和j回退前的前k个字符相等(因为i和j所指的位置前面全部是匹配的),所以我们能推出j回退后的位置前面k个字符与j回退前的位置前面k个字符相同(非常重要!!!我们就是通过这个条件来找到j的回退位置)
在这里插入图片描述
目前为止,我们将寻找j的回退位置这个问题转化成了一个寻找某个位置,这个位置前面k个字符和j回退前位置的前k个字符相等
在这里插入图片描述
接下来,我们引入next数组,通过next数组我们就能知道j到底要回到到什么位置,请继续往下看

🚩next数组🚩

KMP 的精髓就是 next 数组:也就是用 next[j] = k;来表示,每一个模式串中的j对应一个 K 值K就是j移动后的位置(next数组对应的是模式串,每一个模式串都有自己的next数组,并且next数组的元素个数等于模式串的长度)

如下是对next数组的定义

  • next[0]一定是-1,next[1]一定是0
  • 对于next[j],j>=2时,next[j]的值就是 模式串中下标为j的元素前面的k个元素与模式串中下标为0的元素后面k个元素完全相等的最大k值

举例:
例如对于模式串"abcbabcc"
它的next数组是{-1, 0, 0, 0, 0, 1, 2, 3}
在这里插入图片描述

next数组的作用
若i与j的位置不匹配,则j直接返回到next[j]的位置(再次判断i和j的位置是否匹配,仍然不匹配,继续退回到next[j]),因为根据next数组的定义,next[j]的值就是P[j]前k个元素与P[0]后k个元素相等的最大k值,而j返回到k的位置恰好可以保证此时j前面的k个元素和i前面的k个元素完全相同!!!

在这里插入图片描述

🚩next数组练习

练习 1: 举例对于”ababcabcdabcde”, 求其的 next 数组?
-1 0 0 1 2 0 1 2 0 0 1 2 0 0
练习 2: 再对”abcabcabcabcdabcde”,求其的 next 数组? "
-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0

我们可以发现以下规律,如果next数组的值要增加,那么只能在之前的值上增加1,如果next数组的值要减小,可以一次减少多个数

🚩看到这里,我们来理一下我们的思路首先:KMP算法和BF的算法本质就是KMP发现匹配失败后i的值不变,j回退到某一个位置其次:我们分析了回退的位置需要满足一个条件(j回退后的位置前面k个字符和j回退前的k个字符相等)才有可能继续与主串匹配;现在:我们根据回退的位置需要满足的条件引出了next数组(next[j]的值就是模式串与主串补不匹配后j退回的位置),我们接下来要做的就是通过代码求出next数组,一但我们求出了next数组,我们就可以知道j回退的位置具体是哪里了j

🚩next数组的求解🚩

我们手写next数组很容易,因为我们用眼睛可以观察出P[j]前面K个元素和P[0]后面K个元素相同的最大K值,但是计算机无法观察出来,因此我们需要寻找规律

由next数组练习我们可以发现以下规律:如果next数组的值要增加,那么只能在之前的值上增加1,如果next数组的值要减小,可以一次减少多个数

  • 证明如果要next数组的值要增加,只能在前一个的值上增加1
    反证法证明:假设next[i]==k,next[j]==k+2(j>i并且中间没有元素等于k+1)
    由next数组定义可知P[0]~P[k-1]==P[j-k]~P[j-1],P[0]~P[k+1]==P[j-k-2]~P[j+1],
    则一定有P[k]==P[j],于是有P[0]~P[k]==P[j-k]~P[j]这说明了i和j中间一定有元素P[m]==k+1
    与假设矛盾,因此推出如果next数组的值要增加,那么只能在之前的值上增加1
  • 如果要next数组的值要减少,可以减少多个数这点毋庸置疑,无需证明

求解next数组还差最后一步,什么时候next数组的值要增加呢?
由前面的例子可知:在这里插入图片描述
假设next[j]==k,可以推出P[0]~P[k-1]==P[j-k]~P[j-1],
如果想让next[j+1]==k,则需要满足P[0]~P[k]==P[j+1-k]~P[j],只需要在已知条件加上P[k]==P[j]
所以当P[j]==P[k]时,next[j+1]==k+1
如果P[j]!=P[k],我们就让new_k=next[k],再来判断P[j]是否等于P[new_k],如果相等P[j+1]==new_k+1

至此,我们就已经求出next数组中所有的值了
实现next数组

void GetNext(int* next, const char* P, size_t lenP)
{
	assert(next && P);
	if (lenP == 0)
		return;
	next[0] = -1;
	next[1] = 0;
	int j = 2;//下一项
	int k = 0;//前一项的k
	while (j < lenP)//next数组为遍历完
	{
		//k有可能回退到起始位置
		if (k == -1 || P[j - 1] == P[k])
		{
			next[j] = k + 1;
			j++;
			k++;
		}
		else
		{
			k = next[k];
		}
	}
}

🚩理清思路

  1. 在发现主串与模式串匹配失败后,我们将j回退到某一个位置,这个位置由next[j]的值决定,因为next[j]存放的就是P[0]后面k个元素和P[j]前面k个元素完全相同的最大k值,回到下标为k的位置可以保证此时j的前面刚好有k个元素和i前面的k个元素相匹配,一步退到失配后最有可能匹配成功的地方(KMP算法只退到可能匹配成功的地方),不需要像BF算法在一次失配后接着试剩下的所有位置会不会成功
  2. 通过分析,我们得知了next数组得求值方法,当P[j]==P[k] (k==next[j]),next[j+1]==k+1
    当P[j]!=P[k]时,另k==next[k]直到P[j]==P[k]

代码实现

void GetNext(int* next, const char* P, size_t lenP)
{
	assert(next && P);
	next[0] = -1;
	next[1] = 0;
	size_t j = 2;//下一项
	size_t k = 0;//前一项的k
	while (j < lenP)//next数组为遍历完
	{
		//k有可能回退到起始位置
		if (k == -1 || P[j - 1] == P[k])
		{
			next[j] = k + 1;
			j++;
			k++;
		}
		else
		{
			k = next[k];
		}
	}
}
int KMP(const char* S, const char* P)
{
	assert(S && P);
	int lenS = strlen(S);
	int lenP = strlen(P);
	int* next = (int*)malloc(sizeof(int) * lenP);
	assert(next);
	GetNext(next, P, lenP);
	int i = 0;
	int j = 0;
	while (i < lenS && j < lenP)
	{
		if ((j == -1) || (S[i] == P[j]))
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}
	free(next);
	if (j >= lenP)
		return i - j;
	return -1;
}

优化next数组

将next数组优化为nextval数组
在这里插入图片描述

练习:

模式串 t=‘abcaabbcabcaabdab’ ,该模式串的 next 数组的值为( D ) , nextval 数组的值为 (F)

A. 0 1 1 1 2 2 1 1 1 2 3 4 5 6 7 1 2 B. 0 1 1 1 2 1 2 1 1 2 3 4 5 6 1 1 2
C. 0 1 1 1 0 0 1 3 1 0 1 1 0 0 7 0 1 D. 0 1 1 1 2 2 3 1 1 2 3 4 5 6 7 1 2
E. 0 1 1 0 0 1 1 1 0 1 1 0 0 1 7 0 1 F. 0 1 1 0 2 1 3 1 0 1 1 0 2 1 7 0 1

最终代码

void GetNext(int* next, const char* P, size_t lenP)
{
	assert(next && P);
	next[0] = -1;
	next[1] = 0;
	size_t j = 2;//下一项
	size_t k = 0;//前一项的k
	while (j < lenP)//next数组为遍历完
	{
		//k有可能回退到起始位置
		if (k == -1 || P[j - 1] == P[k])
		{
			next[j] = k + 1;
			j++;
			k++;
		}
		else
		{
			k = next[k];
		}
	}
}
//优化next数组
void GetNextVal(int* nextVal, const int* next, const char* P)
{
	assert(nextVal && next);
	nextVal[0] = -1;
	for (size_t i = 1; i < strlen(P); i++)
	{
		//回退位置的字符等于当前字符,回退到那个位置的nextVal值处
		if (P[i] == P[next[i]])
		{
			nextVal[i] = nextVal[next[i]];
		}
		//回退的位置的字符不等于当前字符,回退到当前位置的next值处
		else
		{
			nextVal[i] = next[i];
		}
	}
}
int KMP(const char* S, const char* P)
{
	assert(S && P);
	int lenS = strlen(S);
	int lenP = strlen(P);
	int* next = (int*)malloc(sizeof(int) * lenP);
	int* nextVal = (int*)malloc(sizeof(int) * lenP);
	assert(next);
	GetNext(next, P, lenP);
	GetNextVal(nextVal, next, P);
	int i = 0;
	int j = 0;
	while (i < lenS && j < lenP)
	{
		if ((j == -1) || (S[i] == P[j]))
		{
			i++;
			j++;
		}
		else
		{
			j = nextVal[j];
		}
	}
	free(next);
	if (j >= lenP)
		return i - j;
	return -1;
}

int main()
{
	char* str = "ababcabcdabcde";
	char* sub = "abcd";
	printf("%d\n", KMP(str, sub));
	return 0;
}

至此整个KMP算法我们已经实现了,不得不承认确实很复杂,我花了2个下午才弄明白😭,如果还有哪里不懂得地方,可以看看这个视频【完整版】终于有人讲清楚了KMP算法,Java语言C语言实现,一定要静下心来看!!!

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

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

相关文章

三、Python 操作 MongoDB ----非 ODM

文章目录一、连接器的安装和配置二、新增文档三、查询文档四、更新文档五、删除文档一、连接器的安装和配置 pymongo&#xff1a; MongoDB 官方提供的 Python 工具包。官方文档&#xff1a; https://pymongo.readthedocs.io/en/stable/ pip安装&#xff0c;命令如下&#xff1…

JVM调优,调的是什么?目的是什么?

文章目录前言一、jvm是如何运行代码的&#xff1f;二、jvm的内存模型1 整体内存模型结构图2 堆中的年代区域划分3 对象在内存模型中是如何流转的?4 什么是FULL GC,STW? 为什么会发生FULL GC?5 要调优,首先要知道有哪些垃圾收集器及哪些算法6 调优不是盲目的,要有依据,几款内…

HttpRunner3.x(1)-框架介绍

HttpRunner 是一款面向 HTTP(S) 协议的通用测试框架&#xff0c;只需编写维护一份 YAML/JSON 脚本&#xff0c;即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。主要特征继承的所有强大功能requests &#xff0c;只需以人工方式获得乐趣即可处理HTTP&#xf…

【每日反刍】——指针运算

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日反刍 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日一题 &#x1f7e2; 读书笔记 &#x1f7e1; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

【Java进阶篇】—— File类与IO流

一、File类的使用 1.1 概述 File 类以及本章中的各种流都定义在 java.io 包下 一个File对象代表硬盘或网络中可能存在的一个文件或文件夹&#xff08;文件目录&#xff09; File 能新建、删除、重命名 文件和目录&#xff0c;但 File不能访问文件内容本身。如果我们想要访问…

【LeetCode】二叉树基础练习 5 道题

第一题&#xff1a;单值二叉树 题目介绍&#xff1a; 如果二叉树每个节点都具有相同的值&#xff0c;那么该二叉树就是单值二叉树。 只有给定的树是单值二叉树时&#xff0c;才返回true&#xff1b;否则返回false。 //题目框架 bool isUnivalTree(struct TreeNode* root){ }…

【24】Verilog进阶 - 序列检测2

VL35 状态机-非重叠的序列检测 1 思路 状态机嘛,也是比较熟悉的朋友啦, 我就火速写出了STG。如下黑色所示: 2 初版代码 `timescale 1ns/1nsmodule sequence_test1(input wire clk ,input wire rst ,input wire data ,output reg flag ); //*************code**********…

系统架构:经典三层架构

引言 经典三层架构是分层架构中最原始最典型的分层模式&#xff0c;其他分层架构都是其变种或扩展&#xff0c;例如阿里的四层架构模式和DDD领域驱动模型。阿里的 四层架构模型在三层基础上增加了 Manager 层&#xff0c;从而形成变种四层模型&#xff1b;DDD架构则在顶层用户…

Canvas百战成神-圆(1)

Canvas百战成神-圆 初始化容器 <canvas id"canvas"></canvas>canvas{border: 1px solid black; }让页面占满屏幕 *{margin: 0;padding: 0; } html,body{width: 100%;height: 100%;overflow: hidden; } ::-webkit-scrollbar{display: none; }初始化画笔…

JavaEE--Thread 类的基本用法(不看你会后悔的嘿嘿)

Thread类是JVM用来管理线程的一个类,换句话说,每个线程都唯一对应着一个Thread对象. 因此,认识和掌握Thread类弥足重要. 本文将从 线程创建线程中断线程等待线程休眠获取线程实例 等方面来进行具体说明. 1)线程创建 方法1:通过创建Thread类的子类并重写run () 方法 class M…

UDS 14229 -1 刷写34,36,37服务,标准加Trace讲解,没理由搞不明白

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

Windows环境下实现设计模式——状态模式(JAVA版)

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天总结一下Windows环境下如何编程实现状态模式&#xff08;设计模式&#xff09;。不知道大家有没有这样的感觉&#xff0c;看了一大堆编程和设计模式的书&#xff0c;却还是很难理解设计模式&#xff0c;无…

【完整代码】用HTML/CSS制作一个美观的个人简介网页

【完整代码】用HTML/CSS制作一个美观的个人简介网页整体结构完整代码用HTML/CSS制作一个美观的个人简介网页——学习周记1HELLO&#xff01;大家好&#xff0c;由于《用HTML/CSS制作一个美观的个人简介网页》这篇笔记有幸被很多伙伴关注&#xff0c;于是特意去找了之前写的完整…

【高阶数据结构】红黑树

文章目录1. 使用场景2. 性质3. 结点定义4. 结点旋转5. 结点插入1. 使用场景 Linux进程调度CFSNginx Timer事件管理Epoll事件块的管理 2. 性质 每一个节点是红色或者黑色根节点一定是黑色每个叶子节点是黑色如果一个节点是红色&#xff0c;那么它的两个儿子节点都是黑色从任意…

【基础算法】单链表的OJ练习(6) # 复制带随机指针的链表 #

文章目录&#x1f347;前言&#x1f34e;复制带随机指针的链表&#x1f351;写在最后&#x1f347;前言 本章的链表OJ练习&#xff0c;是最后的也是最难的。对于本题&#xff0c;我们不仅要学会解题的思路&#xff0c;还要能够通过这个思路正确的写出代码&#xff0c;也就是思路…

20230314整理

1.JVM内存区域 程序计数器&#xff1a;字节码解释器通过改变程序计数器来依次读取指令&#xff0c;在多线程的情况下&#xff0c;程序计数器用于记录当前线程执行的位置&#xff0c;从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。它的生命周期随着线程的创建而创…

基于Java+SpringBoot+vue的学生成绩管理系统设计和实现【源码+论文+演示视频+包运行成功】

博主介绍&#xff1a;专注于Java技术领域和毕业项目实战 &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不然下次找不到哟 Java项目精品实战案例&#xff08;200套&#xff09; 目录 一、效果演示 二、…

扯什么 try-catch 性能问题?

“yes&#xff0c;你看着这鬼代码&#xff0c;竟然在 for 循环里面搞了个 try-catch&#xff0c;不知道try-catch有性能损耗吗&#xff1f;”老陈煞有其事地指着屏幕里的代码&#xff1a; for (int i 0; i < 5000; i) {try {dosth} catch (Exception e) {e.printStackTrace…

如何测试一个AI系统?

最近AI大火&#xff0c;chatgpt、GPT-4、文心一言不断的在轰炸着我们的生活、工作&#xff0c;很多时候我们都在感叹这智能化来的太快了。对于一个测试工程师&#xff0c;如何开始测试一个AI系统呢&#xff0c;今天我们就一起来聊聊相关的内容。 智能系统对测试工程师提出的新问…

2023年网络安全比赛--网络安全事件响应中职组(超详细)

一、竞赛时间 180分钟 共计3小时 二、竞赛阶段 1.找出黑客植入到系统中的二进制木马程序,并将木马程序的名称作为Flag值(若存在多个提交时使用英文逗号隔开,例如bin,sbin,…)提交; 2.找出被黑客修改的系统默认指令,并将被修改的指令里最后一个单词作为Flag值提交; 3.找出…