字符串匹配算法之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

智能推荐

FTP命令字和返回码_ftp 登录返回230-程序员宅基地

文章浏览阅读3.5k次,点赞2次,收藏13次。为了从FTP服务器下载文件,需要要实现一个简单的FTP客户端。FTP(文件传输协议) 是 TCP/IP 协议组中的应用层协议。FTP协议使用字符串格式命令字,每条命令都是一行字符串,以“\r\n”结尾。客户端发送格式是:命令+空格+参数+"\r\n"的格式服务器返回格式是以:状态码+空格+提示字符串+"\r\n"的格式,代码只要解析状态码就可以了。读写文件需要登陆服务器,特殊用..._ftp 登录返回230

centos7安装rabbitmq3.6.5_centos7 安装rabbitmq3.6.5-程序员宅基地

文章浏览阅读648次。前提:systemctl stop firewalld 关闭防火墙关闭selinux查看getenforce临时关闭setenforce 0永久关闭sed-i'/SELINUX/s/enforcing/disabled/'/etc/selinux/configselinux的三种模式enforcing:强制模式,SELinux 运作中,且已经正确的开始限制..._centos7 安装rabbitmq3.6.5

idea导入android工程,idea怎样导入Android studio 项目?-程序员宅基地

文章浏览阅读5.8k次。满意答案s55f2avsx2017.09.05采纳率:46%等级:12已帮助:5646人新版Android Studio/IntelliJ IDEA可以直接导入eclipse项目,不再推荐使用eclipse导出gradle的方式2启动Android Studio/IntelliJ IDEA,选择 import project3选择eclipse 项目4选择 create project f..._android studio 项目导入idea 看不懂安卓项目

浅谈AI大模型技术:概念、发展和应用_ai大模型应用开发-程序员宅基地

文章浏览阅读860次,点赞2次,收藏6次。AI大模型技术已经在自然语言处理、计算机视觉、多模态交互等领域取得了显著的进展和成果,同时也引发了一系列新的挑战和问题,如数据质量、计算效率、知识可解释性、安全可靠性等。城市运维涉及到多个方面,如交通管理、环境监测、公共安全、社会治理等,它们需要处理和分析大量的多模态数据,如图像、视频、语音、文本等,并根据不同的场景和需求,提供合适的决策和响应。知识搜索有多种形式,如语义搜索、对话搜索、图像搜索、视频搜索等,它们可以根据用户的输入和意图,从海量的数据源中检索出最相关的信息,并以友好的方式呈现给用户。_ai大模型应用开发

非常详细的阻抗测试基础知识_阻抗实部和虚部-程序员宅基地

文章浏览阅读8.2k次,点赞12次,收藏121次。为什么要测量阻抗呢?阻抗能代表什么?阻抗测量的注意事项... ...很多人可能会带着一系列的问题来阅读本文。不管是数字电路工程师还是射频工程师,都在关注各类器件的阻抗,本文非常值得一读。全文13000多字,认真读完大概需要2小时。一、阻抗测试基本概念阻抗定义:阻抗是元器件或电路对周期的交流信号的总的反作用。AC 交流测试信号 (幅度和频率)。包括实部和虚部。​图1 阻抗的定义阻抗是评测电路、元件以及制作元件材料的重要参数。那么什么是阻抗呢?让我们先来看一下阻抗的定义。首先阻抗是一个矢量。通常,阻抗是_阻抗实部和虚部

小学生python游戏编程arcade----基本知识1_arcade语言 like-程序员宅基地

文章浏览阅读955次。前面章节分享试用了pyzero,pygame但随着想增加更丰富的游戏内容,好多还要进行自己编写类,从今天开始解绍一个新的python游戏库arcade模块。通过此次的《连连看》游戏实现,让我对swing的相关知识有了进一步的了解,对java这门语言也有了比以前更深刻的认识。java的一些基本语法,比如数据类型、运算符、程序流程控制和数组等,理解更加透彻。java最核心的核心就是面向对象思想,对于这一个概念,终于悟到了一些。_arcade语言 like

随便推点

【增强版短视频去水印源码】去水印微信小程序+去水印软件源码_去水印机要增强版-程序员宅基地

文章浏览阅读1.1k次。源码简介与安装说明:2021增强版短视频去水印源码 去水印微信小程序源码网站 去水印软件源码安装环境(需要材料):备案域名–服务器安装宝塔-安装 Nginx 或者 Apachephp5.6 以上-安装 sg11 插件小程序已自带解析接口,支持全网主流短视频平台,搭建好了就能用注:接口是公益的,那么多人用解析慢是肯定的,前段和后端源码已经打包,上传服务器之后在配置文件修改数据库密码。然后输入自己的域名,进入后台,创建小程序,输入自己的小程序配置即可安装说明:上传源码,修改data/_去水印机要增强版

verilog进阶语法-触发器原语_fdre #(.init(1'b0) // initial value of register (1-程序员宅基地

文章浏览阅读557次。1. 触发器是FPGA存储数据的基本单元2. 触发器作为时序逻辑的基本元件,官方提供了丰富的配置方式,以适应各种可能的应用场景。_fdre #(.init(1'b0) // initial value of register (1'b0 or 1'b1) ) fdce_osc (

嵌入式面试/笔试C相关总结_嵌入式面试笔试c语言知识点-程序员宅基地

文章浏览阅读560次。本该是不同编译器结果不同,但是尝试了g++ msvc都是先计算c,再计算b,最后得到a+b+c是经过赋值以后的b和c参与计算而不是6。由上表可知,将q复制到p数组可以表示为:*p++=*q++,*优先级高,先取到对应q数组的值,然后两个++都是在后面,该行运算完后执行++。在电脑端编译完后会分为text data bss三种,其中text为可执行程序,data为初始化过的ro+rw变量,bss为未初始化或初始化为0变量。_嵌入式面试笔试c语言知识点

57 Things I've Learned Founding 3 Tech Companies_mature-程序员宅基地

文章浏览阅读2.3k次。57 Things I've Learned Founding 3 Tech CompaniesJason Goldberg, Betashop | Oct. 29, 2010, 1:29 PMI’ve been founding andhelping run techn_mature

一个脚本搞定文件合并去重,大数据处理,可以合并几个G以上的文件_python 超大文本合并-程序员宅基地

文章浏览阅读1.9k次。问题:先讲下需求,有若干个文本文件(txt或者csv文件等),每行代表一条数据,现在希望能合并成 1 个文本文件,且需要去除重复行。分析:一向奉行简单原则,如无必要,绝不复杂。如果数据量不大,那么如下两条命令就可以搞定合并:cat a.txt >> new.txtcat b.txt >> new.txt……去重:cat new...._python 超大文本合并

支付宝小程序iOS端过渡页DFLoadingPageRootController分析_类似支付宝页面过度加载页-程序员宅基地

文章浏览阅读489次。这个过渡页是第一次打开小程序展示的,点击某个小程序前把手机的开发者->network link conditioner->enable & very bad network 就会在停在此页。比如《支付宝运动》这个小程序先看这个类的.h可以看到它继承于DTViewController点击左上角返回的方法- (void)back;#import "DTViewController.h"#import "APBaseLoadingV..._类似支付宝页面过度加载页

推荐文章

热门文章

相关标签