字符串匹配算法之KMP算法_gtg前5-程序员宅基地

技术标签: 算法  c++  java  字符串  数据结构与算法  数据结构  

一、前言

说到字符串匹配算法,可能大家很容易想到的是暴力法,就是每一次移动一个字符,对比模板串和文本串是否相同,这种方法也称之为BF算法(Brute Force)。

下面介绍的是KMP算法,它是由Knuth和Pratt师徒,以及Morris同时发明的,所以联名发表了这个算法,称为KMP算法。

二、KMP算法

1. 算法的整体思路

KMP算法的整体思路是什么样子呢?让我们来看一组例子:

 

KMP算法和BF算法的“开局”是一样的,同样是把主串和模式串的首位对齐,从左到右对逐个字符进行比较。

第一轮,模式串和主串的第一个等长子串比较,发现前5个字符都是匹配的,第6个字符不匹配,是一个“坏字符”:

这时候,如何有效利用已匹配的前缀 “GTGTG” 呢?

我们可以发现,在前缀“GTGTG”当中,后三个字符“GTG”和前三位字符“GTG”是相同的:

在下一轮的比较时,只有把这两个相同的片段对齐,才有可能出现匹配。这两个字符串片段,分别叫做最长可匹配后缀子串和最长可匹配前缀子串。

第二轮,我们直接把模式串向后移动两位,让两个“GTG”对齐,继续从刚才主串的坏字符A开始进行比较:

显然,主串的字符A仍然是坏字符,这时候的匹配前缀缩短成了GTG:

按照第一轮的思路,我们来重新确定最长可匹配后缀子串和最长可匹配前缀子串:

第三轮,我们再次把模式串向后移动两位,让两个“G”对齐,继续从刚才主串的坏字符A开始进行比较:

以上就是KMP算法的整体思路:在已匹配的前缀当中寻找到最长可匹配后缀子串(真后缀)和最长可匹配前缀子串(真前缀),在下一轮直接把两者对齐,从而实现模式串的快速移动。注意,这里我们为了确保不漏过任何可能的匹配,这里我们只能找最长的自匹配真前缀和真后缀(能确保不漏检)。

那我们怎么知道最长可匹配前缀子串是多少呢?也就是更直白的说,当我们第一次移动的时候,我们应该将模板串向前移动多少呢?

这一切都是通过构造next表实现的

2. next表

next数组到底是个什么鬼呢?这是一个一维整型数组,next[i]的下标 i 表示在第一次发生不匹配时的下标(在上面例子中时C的下标5),那next[5]的是什么呢?继续用上面的例子,next[5]首先应该是3。所以next数组代表的是当我模板串中下标为 i 的字符跟主串相对应的字符不匹配时,这个时候我应该拿第下标为next[i]的字符继续跟主串的字符比较。

下面继续说一下next表的构造。也就是next[5]怎么就等于3?

首先我们知道next[0]=0, 即当模板串的下标为0的字符跟主串为的字符不匹配时,只能主串向前走,继续跟pattern[0]比较。(pattern代表模板串)

其次next[1]=0,即当模板串的下标为1的字符跟主串的字符冲突时,下标 i 可以回退到0.

那么对于i >1, 如何求next[i]呢?这里使用了动态规划的思想。

定义 j = next[i-1], 那么我们知道在pattern[0,i-1)当中,自匹配的真前缀和真后缀的最大长度应该为j,比如下面这个例子:

next[i-1]的意思就是当模板串中的G(下标为2)和主串的相应字符不匹配的时候,我们应该回退到哪个字符在此进行比较?这里next[i-1]=0,也就是在G(下标为2)前面的字符,不存在自匹配的真前缀和真后缀,因为G!=T(指下标为0的G)。

现在再求next[i]的值,假设说前三个字符都匹配了,第四个不匹配,我们应该回退到哪个字符?存在一种这样关系,我们已经知道下标为i-1的字符G,它前面的字符序列能够自匹配的真前缀和真后缀的长度为 j =0。假设说这个时候pattern[j]字符(即紧接着真前缀后面的字符)和pattern[i-1]字符(即紧接着真后缀后面的字符)相等,即pattern[j]=pattern[i-1], 那么下标为i的字符T前面的字符序列能够自匹配的真前缀和真后缀长度 等于 下标为i-1的字符前面的字符序列真前缀相长度 + 1。

所以当pattern[j]=pattern[i-1]时,next[i] = next[i-1] + 1 = j+1。

那么当pattern[j] != pattern[i-1]时,next[i]是多少呢?这个时候next[i]的候选者应该是next[j] + 1, next[next[j]] + 1...。这是为什么呢?我们下面这个为例子

这里就j = next[i-1] = 3。 这个时候下标为i的字符跟主串不匹配,我们应该回退到哪个字符呢?这个时候 pattern[j]!=pattern[i-1],即T != C。我们不能说F前面的自匹配的真前缀和真后缀是C的相应值+1。

这个时候我们应该变换j,使得 j =next[j](如下图所示),我们知道 j 之前的GTG是一个能够自匹配的真前缀Prefix1,它和C之前的真后缀GTG匹配(记为Postfix1)。而next[j] = 1之前的G也是一个真前缀Prefix2,它对应一个真后缀Postfix2。

(注:上图为示意图,真实状况下prefix1和postfix可能会重合,为清晰表示,仅展示不重合的情况,不影响理解)

易知Prefix1包含Prefix2和Postfix2, 而Prefix1 = Postfix1

所以Postfix1也包含Prefix2和Postfix2,  且Prefix2 = Postfix2

所以我们可以把Prefix1中的Prefix2部分对应到Postfix1中的Postfix2部分。这时候我们再看pattern[next[j]]是否等于pattern[i-1],如果相等,那么很高兴,我们可以得到跟第一种情况相似的结论,即next[i] = next[j] + 1。

如果pattern[next[j]]不等于pattern[i-1],我们只能按照刚才的思路再执行一次寻找更小的真前缀,这种迭代搜寻直到next[j]为0时才停止。

3. 优化

在构造next表的过程,我们可以做一个优化,就是在第一种情况,

当pattern[j]=pattern[i-1]时,next[i] = next[i-1] + 1 = j+1。

如果这个时候j+1对应的字符和i的字符相同的话(如例子中都是T),我们后退到j+1对应的字符T,再跟主串比较,仍然是不相等的,所以如果我们发现pattern[j+1] == pattern[i]的话,我们可以把next[i]赋值为next[j+1]而不是j+1。

4. 代码实现

#include<string.h>
#include<cstdlib>
#include<iostream>
using namespace std;

int* getNexts(string pattern)
{
    int* next = new int[max(int(pattern.length()),2)];
    int j = 0;
    next[0] = 0;
    next[1] = 0;
    for (int i = 2; i < pattern.length(); i++)
    {
//        j = next[i-1];   在做了优化的情况下不能有这句 
        if (pattern[j] == pattern[i - 1])
        {
            j++;
         // next[i] = j;                             // 非优化实现
            next[i] = pattern[i]!=pattern[j]?j:next[j]; // 优化实现
        }
        else{
            while (j != 0 && pattern[j] != pattern[i - 1])
            { //从next[i+1]的求解回溯到next[j]
                j = next[j];
            }
         // next[i] = j;                             // 非优化实现
            next[i] = pattern[i]!=pattern[j]?j:next[j]; // 优化实现
        }
        
    }
    return next;
}

int kmp(string str, string pattern)
{ 
    //预处理,生成next数组
    int* next = getNexts(pattern);
    int j = 0;
    int i = 0; 
    //主循环,遍历主串字符
    while(i < str.length())
    {
        if (str[i] == pattern[j])
        {
            j++;i++; 
        }
        else if(j!=0)           // 如果不是跟第0个比较,那么可以回退 
        {
        	while (j!=0 && str[i] != pattern[j])
        	{ //遇到坏字符时,查询next数组并改变模式串的起点
            	j = next[j];
        	}
        	
		}else{					// 否则不能回退,直接主串+1 
			i++;
		}
        if (j == pattern.length())
        { //匹配成功,返回下标
            return i - pattern.length();
        }
    }
    return -1;
} 

int main()
{
    string str = "ATGTGAGCTGGTGTGTGCFAA";
    string pattern = "GTGTGCF";
    int index = kmp(str, pattern);
    printf("首次出现位置:%d" , index);
    return 0;
}

 5. 简洁版 

#include<string.h>
#include<cstdlib>
#include<iostream>
using namespace std;

int* getNexts(string pattern)
{
    int* next = new int[max(int(pattern.length()),2)];
    int j = 0;
    next[0] = 0;
    next[1] = 1;
    for (int i = 2; i < pattern.length(); i++)
    {
        while (j != 0 && pattern[j] != pattern[i - 1])
        { //从next[i+1]的求解回溯到next[j] 
            j = next[j];
        }
        if (pattern[j] == pattern[i - 1])
        {
            j++;
        }
        // next[i] = j;                             // 非优化实现
        next[i] = pattern[i]!=pattern[j]?j:next[j]; // 优化实现
    }
    return next;
}

int kmp(string str, string pattern)
{ 
    //预处理,生成next数组
    int* next = getNexts(pattern);
    int j = 0;
    //主循环,遍历主串字符
    for (int i = 0; i < str.length(); i++)
    {
        while (j > 0 && str[i] != pattern[j])
        { //遇到坏字符时,查询next数组并改变模式串的起点
            j = next[j];
        }
        if (str[i] == pattern[j])
        {
            j++;
        }
        if (j == pattern.length())
        { //匹配成功,返回下标
            return i - pattern.length() + 1;
        }
    }
    return -1;
} 

int main()
{
    string str = "ATGTGAGCTGGTGTGTGCFAA";
    string pattern = "GTGTGCF";
    int index = kmp(str, pattern);
    printf("首次出现位置:%d" , index);
    return 0;
}

 

三、参考资料

1. 数据结构(C++语言版)

2. https://baijiahao.baidu.com/s?id=1659735837100760934&wfr=spider&for=pc

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40765537/article/details/108038855

智能推荐

【软件基础】面向对象分析与设计思想总结-程序员宅基地

文章浏览阅读1.3k次。面向对象的本质:通过对象之间的协作完成功能。面向对象的特点:采用封装、继承、多态和抽象等设计方法。面向过程和面向对象开发中,在分析问题面向过程:拿到问题分析问题如何解决的步骤;面向对象:拿到问题分析问题中有哪些类,类的属性与方法,类与类之间的关系。面向对象遵循:合适的方法应该出现在合适的类中。_面向对象分析

电脑应用工具背景变为护眼绿?_电脑被护眼助手弄得背景是绿色的-程序员宅基地

文章浏览阅读424次。背景变为护眼绿?1、打开注册表--利用doc命令,即“Win+R”组合快捷键打开“运行”,输入regedit命令。2、修改路径下的值 路径:HKEY_CURRENT_USER-&gt;Control Panel-&gt;Colors-&gt;双击window 将其值改为:204 232 207 3、重启系统即可!4.若对你有所帮助请点个赞。..._电脑被护眼助手弄得背景是绿色的

双系统linux分多少内存,win+linux双系统的用户,你们的linux分了多少分区?-程序员宅基地

文章浏览阅读943次。hgywww 于 2010-02-03 22:19:15发表:挺好的调查kantiede 于 2010-01-31 14:39:50发表:学习爱唯一 于 2010-01-31 12:13:32发表:分给linux十个G,然后给他分了四个区sagawf 于 2009-11-17 21:03:54发表:win10Gfedora11 15Gyanchao1988 于 2009-11-17 11..._win+lin双系统lin配置多少储存

神经网络硕士就业前景,计算神经科学就业前景_神经网络就业-程序员宅基地

文章浏览阅读7.8k次。一、算法工程师简介(通常是月薪15k以上,年薪18万以上,只是一个概数,具体薪资可以到招聘网站如拉钩,猎聘网上看看)算法工程师目前是一个高端也是相对紧缺的职位;算法工程师包括音/视频算法工程师(通常统称为语音/视频/图形开发工程师)、图像处理算法工程师、计算机视觉算法工程师、通信基带算法工程师、信号算法工程师、射频/通信算法工程师、自然语言算法工程师、数据挖掘算法工程师、搜索算法工程师、控制算法工程师(云台算法工程师,飞控算法工程师,机器人控制算法)、导航算法工程师(@之介感谢补充)、其他【其他一切需要复杂_神经网络就业

Cocos Creator中使用对象池(官方文档摘录)_cocos creator网络请求时如何获取当前的对象-程序员宅基地

文章浏览阅读3k次。在运行时进行节点的创建(cc.instantiate)和销毁(node.destroy)操作是非常耗费性能的,因此我们在比较复杂的场景中,通常只有在场景初始化逻辑(onLoad)中才会进行节点的创建,在切换场景时才会进行节点的销毁。如果制作有大量敌人或子弹需要反复生成和被消灭的动作类游戏,我们要如何在游戏进行过程中随时创建和销毁节点呢?这里就需要对象池的帮助了。对象池的概念对象池就是一组可回收的节_cocos creator网络请求时如何获取当前的对象

网管软件——Acronis True Image Enterprise Server 9-程序员宅基地

文章浏览阅读206次。服务器的系统备份一直困饶着企业网管,ghost也是从版本8系列开始支持ntfs格式,虽说也可以达到了备份服务器系统的要求,但效果无法满足实时备份(系统运行过程中实行备份),现在介绍的是由acronis出品的Acronis True Image Enterprise Server可以实现一健(F11)进行备份及恢复,而且可以在增量备份,不影响系统正常运行的情况下。这是安装好后..._acronis true image 9

随便推点

vue element远程搜索下拉框出tooltip el-autocomplete下拉框出省略号时鼠标移上去出提示_el-autocomplete 下拉太长省略-程序员宅基地

文章浏览阅读2.3k次,点赞8次,收藏13次。vue element远程搜索下拉框出tooltip el-autocomplete下拉框出省略号时鼠标移上去出提示 需求如下效果图如下对el-tooltip进行了二次封装组件使用需求如下1.element远程搜索框下 下拉框文字超出宽度后会出省略号 要求鼠标移上去能够出文字效果图如下对el-tooltip进行了二次封装<template> <el-tooltip ref="tlp" :content="text" effect="dark" _el-autocomplete 下拉太长省略

Java多线程知识点总结(思维导图+源码笔记,Java架构师成长路线-程序员宅基地

文章浏览阅读713次,点赞5次,收藏10次。又是一年求职季,在这里,我为各位准备了一套Java程序员精选高频面试笔试真题,来帮助大家攻下BAT的offer,题目范围从初级的Java基础到高级的分布式架构等等一系列的面试题和答案,用于给大家作为参考以下是部分内容截图最后又是一年求职季,在这里,我为各位准备了一套Java程序员精选高频面试笔试真题,来帮助大家攻下BAT的offer,题目范围从初级的Java基础到高级的分布式架构等等一系列的面试题和答案,用于给大家作为参考以下是部分内容截图。

Sci-Hub的URL使用_scihub url-程序员宅基地

文章浏览阅读2.7k次。目录标题@[TOC](目录标题)借鉴文章出处URL就是出版商的文章页面路径,要把文章的全部网址复制到Sci-Hub主页的搜索框进行搜索。_scihub url

如何从零将vue+springboot项目打包部署到云服务器(亲测,图文教程超详细!!)_spring boot vue 部署 图解-程序员宅基地

文章浏览阅读5.2k次,点赞30次,收藏115次。手把手教如何将个人项目部署到云服务器(超详细!!)步骤目录手把手教如何将个人项目部署到云服务器(超详细!!)前言一、云服务器设置1.1 首先去购买一个云服务器,阿里或腾讯,具体步骤就不讲了1.2 拿到服务器后先修改密码1.3 修改服务器安全组策略1.4 远程连接云服务器二、远程服务器环境配置2.1 安装jdk(1) 将Linux系统下自带JDK(如果原先安装过,无则忽略)的删除(2) JDK11的安装(3) 设置JAVA_HOME2.2 安装配置MySQL(1)下载mysql(2)卸载Maria DB_spring boot vue 部署 图解

Docker容器—Windows下的安装与使用_docker windows容器-程序员宅基地

文章浏览阅读5.8k次,点赞3次,收藏12次。Docker容器—Windows下的安装与使用_docker windows容器

云原生数据库性能对比(阿里云、百度智能云、腾讯云)-程序员宅基地

文章浏览阅读1.7k次,点赞27次,收藏27次。SysBench 是一个跨平台且支持多线程的模块化基准测试工具,用于评估系统在运行高负载的数据库时相关核心参数的性能表现。可绕过复杂的数据库基准设置,甚至在没有安装数据库的前提下,快速了解数据库系统的性能。

推荐文章

热门文章

相关标签