KMP算法--子串查找问题

news2024/11/18 18:50:21

目录

一.前言

二.KMP算法简介

三.关键概念1:字符串的前后缀

四. 关键概念2:字符串相等前后缀与最长相等前后缀长度

五.关键概念3:Next数组

六.Next数组在算法中的应用:

七.模式串Next数组的构建


 先膜拜一下三位神仙,KMP算法的三位创始人。

 一.前言

在主字符串中求子字符串(也称为模式字符串)的位置的问题中,最简单的求解方法就是用暴力匹配法,暴力匹配法也是库函数strstr的基本实现思想。

暴力匹配法的基本思路:

用两层循环来实现,外层循环用一个循环变量 i 遍历主字符串str1,每当在主字符串中找到子字符串的首元素就进入第二层循环进行两个字符串的匹配,若匹配失败,指针 i 回溯到匹配的起始位置继续寻找下一个子字符串首字符,同时 j 指针也回到子字符串的首地址,重复上述步骤。

内层循环以两个字符串的终止符或不相等的对应字符为结束标志。

匹配成功的标志是内层循环维护子串str2的指针指向子串str2的终止符。

暴力算法的动画演示:

KMP 算法讲解

代码实现:


char* my_strstr2(const char* str1, const char* str2)
{
	assert(str1 && str2);
	if (!(*str2))                     如果子串为空字符串则返回主串的首地址
	{
		return (char*)str1;
	}
	const char* pstr2 = str2;         pstr1和pstr2用于内层循环进行字符串匹配
	const char* pstr1 = str1;
	while (*str1)
	{
		pstr1 = str1;                 令pstr1和pstr2指向进行字符串匹配的起始位置
		pstr2 = str2; 
		while (*pstr1 && (*pstr1 == *pstr2))
		{                             找到匹配过程中的终止字符或不相等的对应字符后跳出循环
			pstr1++;
			pstr2++;
		    if ('\0' ==*pstr2 )       只有在匹配过程中pstr2指向子字符串终止符才算匹配成功
		    {
			    return (char*)str1;
		    }
		}
		str1++;
	}
	return NULL;
} 
————————————————
版权声明:本文为CSDN博主「摆烂小青菜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_73470348/article/details/128588614

暴力求解的时间复杂度是O(m*n),m和n分别为主字符串和子字符串(模式串)的有效字符个数。

二.KMP算法简介

在暴力匹配算法中,每次主串和模式串匹配失败后, 维护主串和模式串的指针都要回退到匹配的起始位置。

这样的处理导致查找效率十分低下。

KMP算法在主串和模式串匹配失败时,会利用匹配过程中已经成功匹配的部分字符(上图中绿色的部分)的相关信息(字符串最大相等前后缀长度),让维护模式串的指针回退到合适的位置而维护主串的指针不进行回退操作,继续向后匹配。kmp算法中维护主串的指针只递增不回退,从而使查找的时间复杂度降为线性复杂度。

(此后统一称待查找的子串为模式串)

三.关键概念1:字符串的前后缀

字符串的前缀:假设一个字符串的长度为n,从该字符串的第1个字符第i个字符(其中1<=i<n)构成的子串称为该字符串的前缀

字符串的后缀:假设一个字符串的长度为n,从该字符串的第j个字符第n个字符(其中n=>j>1)构成的子串称为该字符串的后缀

若使用数组下标表示字符串前后缀:

字符串的前缀:假设一个字符串的长度为n,从该字符串的第0个字符第i个字符(其中0=<i<n-1)构成的子串称为该字符串的前缀

字符串的后缀:假设一个字符串的长度为n,从该字符串的第j个字符第n-1个字符(其中n-1=>j>0)构成的子串称为该字符串的后缀

关于前后缀定义需要注意一下两点:

1.一个字符串的前后缀不包含该字符串本身 

2.字符串的前缀和后缀都是从左向右读取的

四. 关键概念2:字符串相等前后缀与最长相等前后缀长度

五.关键概念3:Next数组

假设现在有一个模式字符串(待查找的字符串):"ABABA".

则该模式子串有4个子串分别为:

每个子串都有一个自身的最长相等前后缀长度。

(注意由于单个字符没有前后缀因此默认其最长相等前后缀长度为0)

​​​​​​​​​​​​​​​​​​​​​我们将每个子串的最长相等前后缀长度存入一个命名为Next的数组中: 

模式串下标0到0的子串的最长相等前后缀长度存入Next[0];

模式串下标0到1的子串的最长相等前后缀长度存入Next[1];

模式串下标0到2的子串的最长相等前后缀长度存入Next[2];

模式串下标0到3的子串的最长相等前后缀长度存入Next[3];

模式串下标0到4的子串的最长相等前后缀长度存入Next[4];

 

类似的实例还有:

六.Next数组在算法中的应用:

在查找主串中模式串(子串)的过程中常会有这样的情况:

主串和模式串的一部分字符已经完成匹配了,但是并没有全部匹配成功。

这时如果是暴力匹配法,维护主串的指针会回到匹配的起始位置的下一个位置接着查找匹配入口点,维护子串的指针则回到子串的起点。

kmp算法则会根据已完成匹配的子串(比如图中模式串的0到3的子串)的最长相等前后缀长度(记录在Next数组中的对应位置),保持维护主串的指针位置不变去调整维护子串的指针位置。

 

算法动画:

KMP 算法讲解

KMP 算法讲解

用Next数组来查找主串中模式串的代码:

prefix是遍历模式串的变量。

i是遍历主串的变量。

mainstr是主串数组名。

modstr是模式串数组名。

	kmp算法中遍历主串的指针会一直递增不回退
	遍历模式串的指针在匹配失败时查Next表回退。
	设计代码时保证每次循环只进行一个字符的匹配或一次prefix指针的查表回退
	循环变量i每次循环最多自增一次避免越界访问的情况
	prefix = 0;
	i = 0;   
	while (mainstr[i])
	{
		if (modstr[prefix] && mainstr[i] == modstr[prefix])
		{
			prefix++;
			i++;
		}
		else if (!modstr[prefix])     匹配到模式串的终止符函数返回
		{
			return (char*)(mainstr + i - prefix);
		}
		else if(prefix)               字符不匹配且prefix不为0时,prefix指针查表回退
		{                              
			prefix = Next[prefix - 1];
		}
		else
		{
			i++;                      prefix查表回退到0后依然无法匹配,则i自增继续遍历主串
		}
	}


	if (!modstr[prefix])
	{
		return (char*)(mainstr + i - prefix);
	}
	else
	{
		return NULL;
	}

七.模式串Next数组的构建

其实Next数组的构建思路和用Next数组来查找主串中模式串的思路非常相似。

构建Next数组的核心是子串最长相等前后缀递推思路。

	                      //子串最长相等前后缀递推思路构建Next表
	int Next[NextSize] = { 0 };  
	int i = 1;            //构建next数组时,变量i用于遍历模式串(同时也作为Next数组的赋值下标)
	int prefix = 0;       //prefix变量用于记录,当前子串(下标0-i构成的子串)的最长相等前后缀长 
                          //度。
	                      //注意prefix变量同时也作为维护当前子串的前缀的下标
	while (modstr[i])
	{
		while (prefix && modstr[i] != modstr[prefix])
		{
			prefix = Next[prefix - 1];  
			               //字符不匹配,当前子串最长相等前后缀长度停止递增,prefix指针查表回退
			               //查表回退的过程一直到前后缀可以继续匹配或prefix为0(回到模式串0前缀 
                           //位置)为止
		}
		if (modstr[prefix] == modstr[i])
		{
			               //字符匹配, 当前子串最长相等前后缀长度递增,匹配继续。
			prefix++;
		}
		Next[i] = prefix;  //每一次循环就给Next数组的一个元素赋值,即确定下标0-i字符构成的子串 
                           //的最长相等前后缀长度
		i++;               //i下标自增,准备为下一个Next表元素赋值
	}     

modstr表示模式字符串数组名。 

Next[0]默认等于0。

构建过程中从最短子串开始以递增的方式计算每一个子串的最长相等前后缀长度并为相应的Next数组元素赋值,若子串中的前后缀能够一直逐个字符匹配下去,则后一个子串最长相等前后缀长度就是前一个子串最长相等前后缀长度加一若子串前后缀遇到某个字符匹配失败则prefix指针查表回退(查表回退的思路与用Next数组来查找主串中模式串的思路非常相似,利用子串的最长相等前后缀子串的最长相等前后缀长度来确定prefix回退的位置),而i指针始终递增不回退。

Next数组的生成动画演示:

KMP 算法讲解

kmp算法模拟实现字符串库函数strstr代码:

#include <stdio.h>
#include <assert.h>
#include <string.h>
#define NextSize 100

char* kmpstrstr(const char* mainstr, const char* modstr)
{
	if (!modstr[0])       
	{
		return (char*)mainstr;
	}   
    //构建Next数组                 
	int Next[NextSize] = { 0 };  
	int i = 1;            
	int prefix = 0;       
	while (modstr[i])
	{
		while (prefix && modstr[i] != modstr[prefix])
		{
			prefix = Next[prefix - 1];  
		}
		if (modstr[prefix] == modstr[i])
		{	               
			prefix++;
		}
		Next[i] = prefix;  
		i++;              
	}                     
    //利用Next数组完成模式串查找
	prefix = 0;
	i = 0;   
	while (mainstr[i])
	{
		if (modstr[prefix] && mainstr[i] == modstr[prefix])
		{
			prefix++;
			i++;
		}
		else if (!modstr[prefix])
		{
			return (char*)(mainstr + i - prefix);
		}
		else if(prefix)                
		{                              
			prefix = Next[prefix - 1];
		}
		else
		{
			i++;                      
		}
	}

	if (!modstr[prefix])
	{
		return (char*)(mainstr + i - prefix);
	}
	else
	{
		return NULL;
	}
}

测试代码:

int main()
{
	char arr1[] = "ABCABCBBCABBCBABBCBBABBCBBABBCAA";
	char arr2[] = "CBBA";
	char* retlib = strstr(arr1, arr2);
	char* retmy =  kmpstrstr(arr1, arr2);
	if (retlib || retmy)
	{
		printf("%s\n", retlib);
		printf("\n");
		printf("%s\n", retmy);
	}
	return 0;
}

感谢观看,多多支持小青菜。  

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

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

相关文章

面试前端数组去重,我会问这3个小问题

关于数组去重&#xff0c;已经是一个老生常谈的问题了&#xff0c;网络上已经有N篇关于数组去重的讲解了&#xff0c;所以&#xff0c;凡是能看见这篇博客的&#xff0c;我们都是有缘人&#xff0c;希望2023年你可以乘风破浪&#xff0c;职击沧海。而一般面试的时候&#xff0c…

MySQL调优-高性能业务表结构设计

目录 前言记录&#xff1a; 数据库表设计 范式设计 什么是范式&#xff1f; 数据库设计的第一范式 数据库设计的第二范式 数据库设计的第三范式 范式说明 反范式设计 什么叫反范式化设计&#xff1f; 反范式设计-商品信息 范式化和反范式总结 实际工作中的反范式实…

C++ stack和queue

1. stack的介绍和使用1.1 stack的介绍1. stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行元素的插入与提取操作。2. stack是作为容器适配器被实现的&#xff0c;容器适配器即是对特定类封装作为其底层的容器&…

基于深度学习的自然语言处理

1、什么是自然语言处理&#xff1f; 自然语言处理&#xff08;Natural Language Processing, NLP&#xff09;是计算机科学领域与人工智能领域中的一个重要方向。它研究能实现人与计算机之间用自然语言进行有效通信的各种理论和方法。自然语言处理是一门融语言学、计算机科学、…

【信号与系统】预习笔记(每日更新ing)

2023.1.8已打卡 信号与系统&#xff08;一&#xff09;信号与系统概述1.0 常见三角公式1.1 信号与系统1.2 信号的表述、分类1.3 信号的运算&#xff08;二&#xff09;连续系统的时域分析&#xff08;三&#xff09;离散系统的时域分析&#xff08;四&#xff09;傅里叶变换与频…

软件质量保证与软件测试复习文档

目录 引言&#xff1a; 内容&#xff1a; 一、Ron patton《软件测试》中谈到的软件缺陷的定义被业界广泛认可&#xff0c;主要包括哪五条&#xff1f; 二、软件测试人员的主要工作职责是什么&#xff0c;一般围绕哪几个重要文档开展工作&#xff1f; 三、什么是软件测试模…

差分算法介绍

一、基本概念 差分算法是前缀和算法的逆运算&#xff0c;可以快速的对数组的某一区间进行计算操作。 例如&#xff0c;有一数列 a[1],a[2],.…a[n]&#xff0c;且令 b[i] a[i]-a[i-1],b[1]a[1]&#xff0c;那么就有 a[i] b[1]b[2].…b[i] a[1]a[2]-a[1]a[3]-a[2].…a[i]-a[i…

电脑开机密码忘记了怎么办?

相信很多朋友为了保护自己的隐私&#xff0c;都会在自己的电脑设置开机密码&#xff0c;但有时候电脑太久没用&#xff0c;就有可能忘记开机密码了&#xff0c;这可怎么办&#xff1f;别着急&#xff0c;今天就跟大家分享两种苹果电脑忘记开机密码解决方式&#xff0c;适用于Ma…

使用Junit进行单元测试的简单例子

首先新建一个工程&#xff0c;选择合适的路径和JDK版本&#xff0c;其它默认就行。 把Main.java内容改为如下。 后面就是对add方法增加单元测试 public class Main {public static void main(String[] args) {System.out.println("Hello world!");}public static i…

计算机网络——应用层协议原理

目录 1. 网络应用体系结构 1.1 客户机/服务器结构 1.2 P2P结构 1.3 混合结构 2. 进程通信 2.1 标识进程通信 2.2 套接字(socket) 3. 网络应用的服务需求 3.1 可靠数据传输 3.2 吞吐量 3.3 定时 3.4 安全性 3.5 常见网络应用的要求 4. 因特网提供的传输服务…

ArcGIS基础实验操作100例--实验69布局中添加报表和Excel图表

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验69 布局中添加报表和Excel图表 目录 一、实验背景 二、实验数据 三、实验步骤 &…

最快的表格:Dapfor Wpf GridControl

Dapfor Wpf GridControl 特性Wpf GridControl 是我们网格的第三个版本&#xff0c;它基于 WPF 技术。前两个产品是基于Microsoft WinForms 技术的MFC Grid 和.Net Grid。在网格的第三次迭代中&#xff0c;Dapfor 的专家采用了以前产品的最佳功能&#xff0c;从而产生了比其他供…

(4)go-micro微服务proto开发

文章目录一 Protobuf介绍二 安装Protobuf三 Protobuf语法1.1 基本规范1.2 字段规则1.3 service如何定义1.4 Message如何定义四 proto代码编写五 生成.go文件六 最后一 Protobuf介绍 Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准&#xff0c;…

微信小程序开发笔记 基础篇③——自定义数据dataset,事件触发携带额外信息

文章目录一、前文二、视频演示三、原理和流程四、注意事项五、全部源码六、参考一、前文 想要实现一个电费充值界面。多个不同金额的充值按钮&#xff0c;每个按钮都携带自定义数据&#xff08;金额&#xff09;点击不同金额的充值按钮&#xff0c;就会上传对应的数据&#xf…

ssh无法登录Centos9解决方法

环境&#xff1a;Centos Stream release 9 情况&#xff1a;通过ssh方式&#xff0c;不管本地登录localhost还是远程登录&#xff0c;均失败。 尝试关闭firewalld和selinux&#xff0c;也不起作用。经搜索和尝试&#xff0c;需要修改/etc/ssh/sshd_config的PermitRootLogin的参…

Cpp20入门0:使用模块输出HelloWorld (import module)

时间&#xff1a;2023.1.8 视频地址&#xff1a;C20要不要学&#xff1f;&#xff1f;&#xff1f;_哔哩哔哩_bilibili 目录 一、Cpp20_HelloWorld ​编辑 头文件 Module.ixx 源文件 main函数 0.Cpp20_HelloWorld.cpp 二、Cpp20 main直接import 三、visual studio 快捷…

C语言银行管理系统

程序示例精选 C语言银行管理系统 如需安装运行环境或远程调试&#xff0c;见文章底部微信名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<C语言银行管理系统>>编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。 学习与应…

指针进阶版☞(超easy~)

回顾初级指针&#xff1a;http://t.csdn.cn/5tCSr &#xff08;其中包含指针和指针数组&#xff09; 接下来的内容是进阶新知识点哟 (&#xff3e;&#xff35;&#xff3e;)ノ~&#xff39;&#xff2f;一.字符指针o(*&#xffe3;▽&#xffe3;*)ブ1.常量字符的指针。对于常…

STL-vector容器和string容器

目录 一、STL的基本概念 二、vector容器 1.遍历 2.vector存放自定义数据类型 3.容器嵌套容器 4.构造函数 5.容量和大小 6.插入和删除 7.容器互换 三、string容器 1.string和char的区别 2.string的构造函数 3.赋值操作 4.字符串拼接 5.查找和替换 6.比较 7.字符串的存取和单个字…

Linux应用编程---5.多线程的创建以及线程间数据共享

Linux应用编程—5.多线程的创建以及线程间数据共享 5.1 多线程的创建 ​ 创建多线程&#xff0c;则多次调用pthread_create()函数。创建两个线程&#xff0c;线程1每隔一秒打印字符串&#xff1a;Hello world&#xff01;&#xff0c;线程2每隔一秒打印字符串&#xff1a;Goo…