KMP算法及其改进图文详解

news2024/11/27 6:16:24

文章目录

  • KMP算法详解
    • 什么是KMP算法
    • KMP算法的应用场景
    • KMP算法和暴力求解的比较
    • 字符串的前缀、后缀和最长相等前后缀
    • KMP算法实现字符串匹配的具体过程(图解)
      • 从串与主串的下标变化
        • j回退的位置(从串的下标变化)
        • 主串的下标变化
      • Next数组
        • 如何运用代码逻辑计算Next数组
          • arr[j] == arr[k]
          • arr[j] != arr[k]
          • 特殊情况(k == -1)
        • 得到Next数组的函数GetNext
    • 运用Next数组实现KMP算法
    • 对KMP算法的改进
      • 引例:
      • 改善方法
      • 实现代码

KMP算法详解

什么是KMP算法

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

KMP算法的应用场景

  • KMP算法是字符串查找的主流算法(也可以说是字符串匹配)
    • 举个具体的例子:现在有主串“abcdabcdabe”和从串”abe“,现在要求在主串中找出从串的第一个匹配项的下标(下标从 0 开始),简单点来说,就是找到从串在主串第一次出现的位置

KMP算法和暴力求解的比较

  • 暴力求解的方法我已经在实现strStr中讲过,题目如图:

    在这里插入图片描述

  • 具体解析过程我们不再赘述,直接看暴力求解的代码:

    int strStr(char * haystack, char * needle){
        int len_hay = strlen(haystack);
        int len_need = strlen(needle);
        if(len_need > len_hay)
            return -1;
        int i = 0,j = 0;
        int count_j = 0;
        while(1)
        {
            i = 0;	//每一次都要从字符串needle第一个字符开始匹配
            j = count_j;
            if(haystack[j] == '\0')	//如果if条件为真,就说明haystack已经遍历完,且没有出现完全匹配的情况,返回-1
                return -1;
            //将字符串needle和字符串haystack的字符逐一匹配
            while(needle[i] == haystack[j] && needle[i] != '\0' && haystack[j]!= '\0')
            {
                i++;
                j++;
            }
            //如果循环退出后needle[i]为'\0',就说明已经完全匹配,返回第一个匹配项的下标
            if(needle[i] == '\0')
                return count_j;
            //如果未完全匹配,则从haystack的下一个字符开始重新与字符串needle进行匹配
            else
                count_j++;
        }
        return -1;
    }
    
    
  • 可以发现,这种方法最坏的情况就是从串出现在主串的最后(如主串“abcdabe”,从串“abe”这种情况),因此暴力解法的时间复杂度为O(M * N)(M,N分别为主串和从串的长度)

  • 而上面说到KMP算法可以将这一匹配过程的时间复杂度降为O(N + M),极大地提高了效率。

字符串的前缀、后缀和最长相等前后缀

要理解KMP算法,首先要理解字符串前缀、后缀和最长公共前后缀这三个概念

  • 字符串前缀即是指不包含最后一个字符的所有以第一个字符开头的连续子串

    • 字符串“aebcdae”的前缀为{[a] , [ae] , [aeb] , [aebc] , [aebcd] , [aebcda]},如图:

      在这里插入图片描述

  • 字符串后缀:即是指不包含第一个字符的所有以最后一个字符结尾的连续子串

    • 字符串“aebcdae”的前缀为{[e] , [ae] , [dae] , [cdae] , [bcdae] , [ebcdae]}

      在这里插入图片描述

  • 最长相等前后缀:即是前缀和后缀最长相等的连续子串

    • 字符串“abcdae”的最长公共前后缀为[ae]

KMP算法实现字符串匹配的具体过程(图解)

我们以主串"abcababcabc",从串 “abcabc”为例:

  • 第一次匹配:在主串下标为5,从串下标为3时匹配失败,之后主串下标保持不变,从串下标变为为2

    在这里插入图片描述

  • 第二次匹配:在主串下标为5,从串下标为2处匹配失败,之后主串下标不变,从串下标变为0

    在这里插入图片描述

  • 第三次匹配:完全匹配

    在这里插入图片描述

  • 看完上面KMP算法具体的匹配过程,我们可以发现,当出现不匹配的情况时,我们的从串并没有返回到第一个字符重新开始匹配,而是返回到一个特定的位置,同样,主串是停留在原处(即不匹配的字符),而不是和暴力算法一样从第一个字符到第n个字符(遍历整个主串),那么,我们如何求出从串回溯的这一特定位置呢?

从串与主串的下标变化

首先我们规定从串进行匹配时,从串字符下标为j,主串字符下标为i

先下结论:当从串在下标为j的字符匹配失败时,要使匹配效率最高,j回退的位置就是 下标为从串j字符前子串的最长相等前后缀长度 的从串元素(j为下标)。而主串下标i保持不变。

j回退的位置(从串的下标变化)

  • 就拿上面的例子来说:

    • 在下标为5的字符(c)处匹配失败,我们将字符c前面的字符串称为子串

    • 又由图我们可以发现,子串后面的两个元素已经和主串一一对应(匹配成功)

    在这里插入图片描述

    • c前面子串"abcab"的最长公共前后缀为ab,其长度为2

      在这里插入图片描述

    • 将后缀ab匹配主串ab调整为前缀ab对应主串ab.

    • 即j回退到的字符就是下标为最长公相等前后缀长度的字符(即最长相等前后缀ab的后一个字符c)

      在这里插入图片描述

主串的下标变化

  • 为什么主串保持不变?

    • 其实道理也是一样的:

    • 主串下标为i前的两个字符已经和从串的前面两个字符一一对应(已经对从串的j进行了调整)

    • 那么我们就只需要继续匹配从串后面的字符和主串字符,而不需要像暴力求解一样依次遍历主串元素,从而降低了时间复杂度。

  • 特殊情况:

    • 有细心的小伙伴可能会提出疑问:如果j已经回退到从串的第一个字符,但仍不能和主串匹配呢

    • 我们来看一个例子:主串"abdefac",从串"ac"

      • 第一次匹配时,在i=1(字符b),j=1(字符c)时匹配失败

        在这里插入图片描述

      • 从串字符c前的子串为a,其最大相等前后缀长度为0,因此j回退到下标为0的字符a,i不变。

      • 第二次匹配时,在i=1(字符b),j=0(字符a)时匹配失败

        在这里插入图片描述

      • 但此时j已经等于0了,已经退到不能再退了,这时我们就要移动i了,我们不断将i右移一个字符,直到主串下标为i的字符等于从串下标为j(j=0)的字符,然后再按上面的方法进行匹配。

        在这里插入图片描述

Next数组

  • 首先规定从串下标为j的字符前的子串的最大相等前后缀长度为k

  • 我们已经知道,当主串与从串不匹配时,从串应该回退到下标为 j字符前子串的最长相等前后缀长度 的字符,而长度为length的从串(不包含结束符"\0")有length个最长公共前后缀长度,我们规定,这length个数据都存在一个叫Next的数组中

  • 例如串“abcababcabc”的Next数组为{-1,0,0,0,1,2,1,2,3,4,5},如图

    在这里插入图片描述

如何运用代码逻辑计算Next数组

  • 显然,用肉眼看出一个字符串的Next数组是十分简单的,但计算机可是十分死板的,那我们怎么用计算机的思维(代码逻辑)来计算Next数组呢?
  • 我们还是以字符串arr“abcababcabc”为例,分以下三种情况:
arr[j] == arr[k]
  • 假设我们要计算Next[9],其中我们已知Next[0]~Next[8] :{-1,0,0,0,1,2,1,2,3}

  • 我们可以发现当j = 8时,arr[j] = a, Next[j] = k = 3,arr[k] = a,即arr[j] = arr[k],又因为我们已知arr[j]前子串的最长公共前后缀长度为k = 3,那么我们就可以分析出,当arr[j] == arr[k]时,Next[j + 1] = k + 1,如图:

    在这里插入图片描述

arr[j] != arr[k]
  • 假设我们要计算Next[6],其中我们知道Next[0]~Next[5] :{-1,0,0,0,1,2}

  • 当j = 5时,arr[j] = a,Next[j] = k = 2,arr[k] = c,即arr[j] != arr[k]

  • 那么我们再令新的k = next[k] = 0(第一个k为新的k,第二个k还是旧的k,即Next[j]),此时arr[k] = arr[0] = a = arr[j],这样又回到了上面arr[j] = arr[k]的条件,所以当arr[j] != arr[k]时,k就要不断回退,并被重新赋值(回退到位置就是arr[k],被赋予的值就是Next[k]),直到出现arr[j] = arr[k]的情况

在这里插入图片描述

特殊情况(k == -1)
  • 出现了k == -1这种情况,就意味着k走到字符串的第一个元素也没有遇到arr[j] == arr[k]的情况,那么此时k就不能继续回退了,也就是说下标为j + 1元素前子串的最长公共前后缀长度为0,即arr[j + 1] = k + 1 = -1 + 1 = 0。

得到Next数组的函数GetNext

void GetNext(int *Next, char *str)
{
    int len = strlen(str);	//从串长度
    int i = 1;		//第一个待求项Next[i]
    int k = -1;		//待求项前一个的k值
    Next[0] = -1;	//默认第一个值为-1
    while(i < len)
    {
        if(k == -1 || str[i - 1] == str[k])	//arr[j] == arr[k]和k == -1
        {
            Next[i] = k + 1;
            i++;	//待求项右移
            k++;	//待求项前一个的k值加一
            
		}
        else	//arr[j] != arr[k]
            k = Next[k];
	}
}

运用Next数组实现KMP算法

  • 我们来看一道具体的题目实现strStr

    在这里插入图片描述

  • 直接上代码

    //得到Next数组的函数
    void GetNext(int *Next, char *str)
    {
        int len = strlen(str);	//从串长度
        int i = 1;		//第一个待求项Next[i]
        int k = -1;		//待求项前一个的k值
        Next[0] = -1;	//默认第一个值为-1
        while(i < len)
        {
            if(k == -1 || str[i - 1] == str[k])	//arr[j] == arr[k]和k == -1
            {
                Next[i] = k + 1;
                i++;	//待求项右移
                k++;	//待求项前一个的k值加一
    
    		}
            else	//arr[j] != arr[k]
                k = Next[k];
    	}
    }
    //实现字符串查找
    int strStr(char * haystack, char * needle){
        int len_hay = strlen(haystack);
        int len_need = strlen(needle);
        int i = 0, j = 0;
        //如果从串长度大于主串长度,直接返回-1
        if(len_need > len_hay)
            return -1;
        //如果主串从串长度都为0,直接返回0
        if(len_hay == 0 && len_need == 0)
            return 0;
        int *Next = (int *)malloc(sizeof(int) * len_need);	//为Next数组申请内存
        GetNext(Next,needle);	//得到Next数组
        while(i < len_hay && j < len_need)
        {
            if(haystack[i] == needle[j])
            {
                i++;
                j++;
            }
            //当j还未回溯到第一个字符
            //且从串与主串开始不匹配时,j开始回溯
            else if(j != 0)
                j = Next[j];
            //如果j已经回溯到第一个字符,那么就让主串i向右走一个字符,继续匹配
            else
                i++;
        }
        //如果j大于等于从串长度,说明j已经走到了从串为,说明匹配完成,返回主串开始匹配的位置
        if(j >= len_need)
            return i - j;
        return -1;
    }
    

对KMP算法的改进

引例:

  • 我们先来看一个例子:

    • 主串为“aaaabcab”,从串为“aaaac”,匹配过程如图:

      在这里插入图片描述

    • 我们可以发现在这一个匹配过程中,第二步,第三步,第四步其实是多余的,为什么呢?我们可以看到第一步中字符b和字符c不匹配时,字符c回溯到字符a,显然字符a仍然不和字符b匹配,但字符a回溯后的字符还是a,自然不能和字符b匹配,这样就造成了许多重复比较的情况,因此我们就是要减少这种重复比较来改善KMP算法。

改善方法

  • 通过上述例子,我们知道了,出现重复比较的原因是当主串字符和从串字符出现不匹配时,从串字符的回溯字符仍等于原来的字符(arr[j] = arr[k])
  • 因此我们就要阻止这种情况的出现,若从串字符的回溯字符仍等于从串字符,那么就要继续回溯(next[i] = next[k]),直到出现不相等的情况或回溯到了从串头
  • 自然,我们对next数组的求法也要做出改变。
    • 例如字符串“ababaaab”的next数组为{-1,0,-1,0,-1,3,1,0}
      在这里插入图片描述

实现代码

//得到新的Next数组Nextval
void GetNextVal(int* nextval, char *str)
{
	int len = strlen(str);
	int k = -1;
	int i = 0;
	nextval[0] = -1;	//第一个k值默认为-1
    //由于操作是先++后赋值,因此为了不会数组越界,i < len - 1
	while (i < len - 1)
	{
		if (k == -1 || str[i] == str[k])
		{
			i++;
			k++;
            //相比于最开始的KMP算法,多出来的就是这个if判断
            //如果回溯字符等于原字符,那么就要继续回溯,避免重复比较
			if (str[i] == str[k])
				nextval[i] = nextval[k];
            //如果回溯字符不等于原字符,那么就和原来的操作一样
			else
				nextval[i] = k;
		}
		else
			k = nextval[k];
	}
}

int strStr(char* hayStack, char* needle)
{
	int len_h = strlen(hayStack);
	int len_n = strlen(needle);
	int i = 0, j = 0;
	int *NextVal = (int*)malloc(sizeof(int) * len_n);
	GetNextVal(NextVal, needle);
	while (i < len_h && j < len_n)
	{
		if (hayStack[i] == needle[j])
		{
			i++;
			j++;
		}
		else if (j != 0)
		{
			j = NextVal[j];
            //相比于原来的KMP算法,多出了这一句if判断
            //这是由于新的NextVal数组由于k的多次回溯,会出现不止第一个字符的k为-1的情况,因此为防止数组越界,当j为-1时要将其置为零
			if (j == -1)
				j = 0;
		}
		else
			i++;
	}
	if (j >= len_n)
		return i - j;
	else
		return -1;
}

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

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

相关文章

[CTF/网络安全] 攻防世界 xff_referer 解题详析

[CTF/网络安全] 攻防世界 xff_referer 解题详析 XFF及refererXFF格式referer格式姿势总结 题目描述&#xff1a;X老师告诉小宁其实xff和referer是可以伪造的。 XFF及referer X-Forwarded-For&#xff08;简称 XFF&#xff09;是一个 HTTP 请求头部字段&#xff0c;它用于表示 …

深入理解计算机系统第七章知识点总结

文章目录 详解ELF文件-> main.o前十六个字节的含义推测elf的大小查看节头部表推断每个section在elf中的具体位置查看.text的内容查看.data的内容关于.bss查看.rodata的内容关于其他的节表示的信息 详解符号表符号编译器如何解析多重定义的全局符号静态库与静态链接构造和使用…

seata的部署和集成

seata的部署和集成 一、部署Seata的tc-server 1.下载 首先我们要下载seata-server包&#xff0c;地址在http://seata.io/zh-cn/blog/download.html 2.解压 在非中文目录解压缩这个zip包&#xff0c;其目录结构如下&#xff1a; 3.修改配置 修改conf目录下的registry.conf文…

开源大模型资料总结

基本只关注开源大模型资料&#xff0c;非开源就不关注了&#xff0c;意义也不大。 基座大模型&#xff1a; LLaMA&#xff1a;7/13/33/65B&#xff0c;1.4T token LLaMA及其子孙模型概述 - 知乎 GLM&#xff1a;6/130B&#xff0c; ChatGLM基座&#xff1a;GLM&#xff08…

【网络】- TCP/IP四层(五层)协议 - 网际层(网络层) - 网际协议IP

目录 一、概述 二、初步了解网际协议 IP  &#x1f449;2.1 与数据链路层的区别  &#x1f449;2.2 网际协议 IP 概览  &#x1f449;2.3 分层的意义 三、IP协议基础知识  &#x1f449;3.1 IP地址属于网络层地址  &#x1f449;3.2 路由控制  &#x1f449;3.3 IP分包与…

solr快速上手:核心概念及solr-admin界面介绍(二)

0. 引言 上一节&#xff0c;我们简单介绍了solr并演示了单节点solr的安装流程&#xff0c;本章&#xff0c;我们继续讲解solr的核心概念 solr快速上手&#xff1a;solr简介及安装&#xff08;一&#xff09; 1. 核心概念 核心&#xff08;索引/表&#xff09; 在es中有索引…

【软件测试】5年测试老鸟总结,自动化测试成功实施,你应该知道的...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 自动化测试 Pytho…

基于html+css的图展示82

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

chatgpt赋能Python-pythonage

Pythonage - 一款优秀的Python SEO工具 无论是个人博客还是商业网站&#xff0c;SEO&#xff08;搜索引擎优化&#xff09;都是最重要的。Pythonage是一款优秀的Python SEO工具&#xff0c;可以帮助你优化你的网站并提高搜索引擎排名。在这篇文章中&#xff0c;我们将详细介绍…

ChatGPT 使用 拓展资料:开始构建你的优质Prompt

ChatGPT 使用 拓展资料:开始构建你的优质Prompt

【JavaEE】阻塞队列、定时器和线程池

目录 1、阻塞队列 1.1、概念 1.2、生产者消费者模型 1.3、阻塞队列的模拟实现 2、定时器 2.1、使用标准库中的定时器 2.2、模拟实现定时器 3、线程池 3.1、标准库中的线程池 3.1.1、ThreadPoolExecutor类的构造方法 3.1.2、Java标准库的4种拒绝策略【经典面试题】…

Canal内存队列的设计

1、背景 笔者的公司内部使用了开源的Canal数据库中间件来接受binlog数据&#xff0c;并基于此进行数据的订阅和同步到各种同构和异构的数据源上&#xff0c;本文将对Canal内部使用的store模块进行分析。 2、Store模块概览 Canal的store模块用于存储binlog中的每一个event&am…

MySQL- 多表查询(上)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a;小刘主页 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️树高千尺&#xff0c;落叶归根人生不易&…

安卓基础巩固(一):布局、组件、动画、Activity、Fragment

文章目录 布局LinearLayoutRelativeLayoutTableLayoutFrameLayoutConstraintLayoutListView基于ArrayAdapter自定义Adaper提升ListView的运行效率 RecyclerView基本属性使用案例布局&#xff08;显示方式&#xff09;监听事件利用View.onClickListener 和 onLongClickListener …

日志收集机制和日志处理流程规范

本博客地址&#xff1a;https://security.blog.csdn.net/article/details/130792958 一、日志收集与处理流程 云原生平台中对日志提取收集以及分析处理的流程与传统日志处理模式大致是一样的&#xff0c;包括收集、ETL、索引、存储、检索、关联、可视化、分析、报告这9个步骤…

Leetcode 二叉树详解

二叉树 树的概念及基本术语见树与二叉树的基础知识 定义&#xff1a;一棵二叉树是结点的一个有限集合&#xff0c;该集合或者为空&#xff0c;或者是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。 特点&#xff1a;每个结点至多只有两棵子树&#xff…

Vivado综合属性系列之八 DIRECT_ENABLE DIRECT_RESET

目录 一、前言 二、DIRECT_ENABLE、DIRECT_RESET ​ ​2.1 属性说明 ​ ​2.2 工程代码 ​ ​2.3 综合结果 一、前言 在Vivado 2019之前的版本中&#xff0c;对于设计中触发器的使能端口和复位端口是会自动接地&#xff0c;如果需要接设计端口&#xff0c;如果要直连…

GitHub Copilot开发者酷游网址训练营

目标读者 已使用且【酷游网K͜W͜98典neт娜娜宝宝提供】想发挥GitHub Copilot所有潜能的使用者想知道GitHub Copilot未来展望的使用者想了解GitHub Copilot能力的开发者 简介 最近Open AI带起的新世代&#xff0c;热潮汹涌&#xff0c;一堆AI工具蜂拥而至(如:chatGPT和Midjo…

近期关于Transformer结构有潜力的改进方法总结

目录 0 引言1 Gated Linear Unit (GLU)1.1 思路 2 Gated Attention Unit (GAU)2.1 思路2.2 实验结论2.3 混合注意力 3 FlashAttention3.1 标准Attention的实现3.2 FlashAttention的实现针对目标1针对目标2 4 总结5 参考资料 0 引言 标准Transformer在最新的实际大模型中并没有…

C++STL算法篇之集合算法

CSTL算法篇之集合算法 集合算法set_union(并集)set_difference(差集)set_intersection(交集)set_symmetric_difference(对称差集) 集合算法 当然最好还是要包含 functional algorithm 这2个头文件 集合算法有4个函数 1.set_union 交集 2.set_difference 差集 3.set_intersectio…