首页 > 编程语言 >实现strStr() —— KMP算法(包含next数组的优化)

实现strStr() —— KMP算法(包含next数组的优化)

时间:2024-08-19 15:25:01浏览次数:12  
标签:匹配 前缀 后缀 next int 数组 KMP strStr

目录

KMP算法

KMP算法的应用

前缀表

最长公共前后缀

为什么要使用前缀表

如何计算前缀表

前缀表和next数组

时间复杂度分析

例题28.实现strStr

构造next数组 

使用next数组来做匹配 

前缀表统一减一 C++代码实现

前缀表(不减一)C++代码实现

总结 

拓展:next数组的优化 


KMP算法

 文档讲解:手撕KMP算法

KMP算法是由三维学者发明Knuth,Morris和Pratt,取三位学者名字的首字母,所以叫做KMP算法。

KMP算法的应用

KMP算法主要应用在字符串匹配上。KMP算法的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去匹配。


前缀表

又称为next数组,是KMP算法中最为关键的部分。

前缀表式用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

例如要在文本串 aabaabaafa 中查找是否出现过一个模式串 aabaaf 时:当匹配到第六个字符时,会发现文本串的b和模式串的f不一样。此时如果是暴力匹配,发现不匹配,就要从头开始匹配了;但如果使用前缀表,就不会开始匹配,而是从模式串中的第三个字符b开始继续匹配,因为我们知道前面有两个aa已经匹配了。

由此引出一个问题,前缀表是如何记录的?首先前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,这也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。

由此总结前缀表是:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。


最长公共前后缀

我们首先需要明确,前缀和后缀的概念:

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

接着前缀表要求的就是相同前后缀的长度,也即一个字符串前缀和后缀能够相同的最大长度。例如:字符串a的最长相等前后缀为0,字符串aa的最长相等前后缀为1,字符串aaa的最长相等前后缀为2,等等。


为什么要使用前缀表

前缀表式如何告诉我们上次匹配的位置,并跳过去的,回顾aabaabaafa与aabaaf匹配的例子。在匹配的过程中在下标5的地方遇到不匹配,模式串指向f

然后跳到了下标2,指向b的位置,继续匹配,这里也能够看出最长相等前后缀的应用:

这是因为:下标5之前这部分的字符串(aabaa)的最长相等的前缀和后缀字符串是“aa”,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。

所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。


如何计算前缀表

显然我们需要通过最长相等的前缀和后缀,来计算前缀表:

如图:

长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

紧接着,长度为前2个字符的子串“aa”,最长相等前后缀的长度为1。

长度为前3个字符的子串aab,最长相等前后缀的长度为0。

以此类推:长度为前4个字符的子串aaba,最长相等前后缀的长度为1。长度为前5个字符的子串aabaa,最长相等前后缀的长度为2。长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:

可以看出模式串与前缀表对应位置的数组表示的就是:下标i之前(包括i)的字符串中,有多大长度的相等前缀后缀。

那么当找到不匹配的位置时,我们要看它的前一个字符的前缀表的数值是多少。

为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。所以要看前一位的前缀表的数值。前一个字符的前缀表的数值是2,所以把下标移动到下标2的位置继续匹配。


前缀表和next数组

很多KMP算法的实现都是使用next数组来做回退操作的,那么next数组与前缀表有什么关系呢?实际上next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后欧威next数组的。这主要取决于代码的实现方式,和KMP的原理无关。


时间复杂度分析

其中n是文本串长度,m是模式串长度, 因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),不存在回头路。同时还要单独生成next数组,next数组只与模式串有关,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)。

而暴力解法的时间复杂度是O(n * m),所以KMP在字符串匹配中极大地提高了搜索的效率。


例题28.实现strStr

题目:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

构造next数组 

我们定义一个函数getNext来构建next数组, 函数参数为指向next数组的指针,和一个字符串。代码如下:

void getNext(int* next, const string& s)

构造next数组其实就是计算模式串s前缀表的过程。主要有如下三步:

  1. 初始化
  2. 处理前后缀不相同的情况
  3. 处理前后缀相同的情况 

1.初始化:

定义两个指针 i 和 j ,j 指向前缀末尾位置,i 指向后缀末尾位置。然后对next数组进行初始化赋值,第一个位置设为-1,如下:

int j = -1;
next[0] = j;

这里我们采取的是前缀表统一减一的操作,这只是一种实现方式。后续给出 j 不初始化为-1的实现代码。next[i] 表示 i (包括i) 之前最长相等的前后缀长度(其实就是j)

2.处理前后缀不相同的情况

因为 j 初始化为-1,那么 i 就从1开始,进行s[i] 与 s[j + 1]的比较。

for (int i = 1; i < s.size(); i++) {

如果s[i] 与 s[j + 1]不相同,也就是遇到前后缀末尾不相同的情况,就像向前回退。这个地方有个很关键的部分,怎么进行j的回退:next[j] 就是记录者 j(包括j)之前的子串的相同前后缀的长度,那么s[i] 与 s[j + 1] 不相同,就要找j + 1前一个元素在next数组里的值(就是next[j]),这实际上也蕴含了KMP算法。

所以,处理前后缀不相同的情况代码如下:

while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
    j = next[j]; // 向前回退
}

3.处理前后缀相同的情况

如果s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和 j 说明找到了相同的前后缀,同时还要将j(前缀长度)赋给next[i],因为next[i] 要记录相同前后缀的长度:

if (s[i] == s[j + 1]) { // 找到相同的前后缀
    j++;
}
next[i] = j;

最后整体构建next数组的函数代码如下:

void getNext(int* next, const string& s){
    int j = -1;
    next[0] = j;
    for(int i = 1; i < s.size(); i++) { // 注意i从1开始
        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
            j = next[j]; // 向前回退
        }
        if (s[i] == s[j + 1]) { // 找到相同的前后缀
            j++;
        }
        next[i] = j; // 将j(前缀的长度)赋给next[i]
    }
}

代码构造next数组的逻辑流程动画如下: 图片来源

得到了next数组之后,就可以用这个来进行匹配了

使用next数组来做匹配 

在文本串s里,找是否出现过模式串t 。定义两个下标 j 指向模式串起始位置,i 指向文本串起始位置。j 初始值依然设置为 -1, 因为next数组里记录的起始位置为 -1。

i从0开始,遍历文本串,代码如下:

for (int i = 0; i < s.size(); i++) 

接下来就是s[i] 与 t[j + 1]进行比较。

如果s[i] 与 t[j + 1]不相同,j 就要从next数组里寻找下一个匹配的位置。

while(j >= 0 && s[i] != t[j + 1]) {
    j = next[j];
}

如果s[i] 与 t[j + 1]相同,那么i 和 j同时向后移动,代码如下:

if (s[i] == t[j + 1]) {
    j++; // i的增加在for循环里
}

如果 j 指向了模式串 t 的末尾,那么就说明模式串 t 完全匹配文本串 s 里的某个子串了。由于本题要在文本串字符串中找出模式串出现的第一个位置(从0开始),所以返回当前在文本串匹配模式串的位置 i 减去模式串的长度, 就是文本串字符串中出现模式串的第一个位置。代码如下:

if (j == (t.size() - 1) ) {
    return (i - t.size() + 1);
}

那么使用next数组,用模式串匹配文本串的整体代码如下:

int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
    while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
        j = next[j]; // j 寻找之前匹配的位置
    }
    if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
        j++; // i的增加在for循环里
    }
    if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
        return (i - t.size() + 1);
    }
}

前缀表统一减一 C++代码实现

最后我们得到最终的代码:

class Solution {
public:
    //获得next数组
    void getNext(int* next, const string s) {
        //1.初始化j 和 next[0]
        int j = -1;
        next[0] = j;
        for(int i = 1; i < s.size(); i++) {
            //2.处理s[i] != s[j + 1]的情况
            while(j >= 0 && s[i] != s[j + 1]) {
                j = next[j];
            }
            //3.处理s[i] == s[j + 1]的情况
            if(s[i] == s[j + 1]) {
                j++;
            }
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        if(needle.size() == 0) return 0;
        vector<int> next(needle.size());
        getNext(&next[0], needle);

        //使用next数组进行匹配
        //1.初始化j
        int j = -1;
        for(int i = 0; i < haystack.size(); i++) {
            //2.处理haystack[i] != needle[j + 1]的情况
            while(j >= 0 && haystack[i] != needle[j + 1]) { //不包含和下标0元素比较的情况
                j = next[j];
            }
            //3.处理haystack[i] == needle[j+1]的情况
            if(haystack[i] == needle[j + 1]) { //包含和下标0元素比较的情况
                j++;
            }
            if(j == (needle.size() - 1)) {
                return i - needle.size() + 1;
            }
        }
        return -1;
    }

前缀表(不减一)C++代码实现

事实上,是否减1,只是KMP算法实现上的不同,如果就直接使用前缀表可以换一种回退方式,找j = next[j - 1] 来进行回退。主要是j = next[x] 这一步最为关键。

在前缀表统一减一中,getNext()函数的实现为:

void getNext(int* next, const string& s) {
    int j = -1;
    next[0] = j;
    for(int i = 1; i < s.size(); i++) { // 注意i从1开始
        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
            j = next[j]; // 向前回退
        }
        if (s[i] == s[j + 1]) { // 找到相同的前后缀
            j++;
        }
        next[i] = j; // 将j(前缀的长度)赋给next[i]
    }
}

此时如果输入的模式串为aabaaf,对应的next为-1 0 -1 0 1 -1。

这里的j 和 next[0] 初始化为 -1. 整个next数组是以前缀表减一之后的效果来构建的。

那么前缀表不减一来构建next数组,代码就为:

void getNext(int* next, const string& s) {
    int j = 0;
    next[0] = 0;
    for(int i = 1; i < s.size(); i++) {
        while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
            j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
        }
        if (s[i] == s[j]) {
            j++;
        }
        next[i] = j;
    }
}

此时如果输入的模式串为aabaaf,对应的next为0 1 0 1 2 0,(其实这就是前缀表的数值了)

那么用这样的next数组也可以用来做匹配,代码要有所变动:

代码:

class Solution {
public:
    //前后缀不减一的情况
    void getNext(int* next, const string s) {
        //1.初始化j 和 next 数组
        int j = 0;
        next[0] = j;
        
        for(int i = 1; i < s.size(); i++) {
            //2.处理s[i] != s[j]的情况
            while(j > 0 && s[i] != s[j]) { //不包含和s[0]比较的情况
                j = next[j - 1];
            }
            //3.处理s[i] == s[j]的情况
            if(s[j] == s[i]) { //包含和s[0]比较的情况
                j++;
            }
            next[i] = j;
        }
    }
    int strStr(string haystack, string needle) {
        if(needle.size() == 0) return 0;
        vector<int> next(needle.size());
        getNext(&next[0], needle);

        //1.初始化
        int j = 0;
        for(int i = 0; i < haystack.size(); i++) {
            //处理不相等的情况
            while(j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            //处理相等的情况
            if(haystack[i] == needle[j]) {
                j++;
            }
            if (j == needle.size() ) {
                return (i - needle.size() + 1);
            }            
        }
        return -1;
    }
};

总结 

我们需要明确KMP算法的使用场景,能够解决什么问题:字符串匹配的问题

KMP算法中最关键的是next数组,且要知道next数组其实就是前缀表。

我们要知道next数组是如何来实现匹配代码的,如果使得时间复杂度降为O(m + n)


拓展:next数组的优化 

实际上next数组还可以进行优化,以aabaaf为例,我们使用不减1的方式得到的next数组是:0 1 0 1 2 0。

我们在匹配的时候,如果匹配到第四个 a 匹配失败,根据代码我们会跳到下标为1的位置继续匹配。但实际上下标为1的位置,我们知道是 a 肯定也是匹配失败的,这就多出了无效匹配。

因此可以对next数组进行优化,得到nextval数组:0 1 0 0 2 0;

其实得到nextval数组的方法很简单,我们知道next数组是来进行回退的,当我们当前匹配的不正确的时候,就要进行回退,因此如果我们回退后的字母和当前匹配失败的字母是一样的,则更新当前匹配失败字母的next数值,改为回退后的字母的next数值,意味着,跳过匹配,直接找到下一个next数。

对于aabaaf来说,当我们匹配到第四个a失败时,我们跳到下标为1的位置,发现也是 a 则我们需要把next[4]改为next[0](因为我们是跳到next[j - 1]的位置)。这样就直接跳过了无效匹配,找到了下一个应该跳到的位置。

具体代码实现为:

class Solution {
public:
    //前后缀不减一的情况
    void getNext(int* nextval, const string& s) {
        vector<int> next(s.size());
        //1.初始化j 和 next 数组
        int j = 0;
        next[0] = j;
        
        for(int i = 1; i < s.size(); i++) {
            //2.处理s[i] != s[j]的情况
            while(j > 0 && s[i] != s[j]) { //不包含和s[0]比较的情况
                j = next[j - 1];
            }
            //3.处理s[i] == s[j]的情况
            if(s[j] == s[i]) { //包含和s[0]比较的情况
                j++;
            }
            next[i] = j;
        }

        nextval[0] = next[0];
        for(int i = 1; i < s.size() - 1; i++) {
            if(next[i] != 0 && s[next[i]] == s[i + 1]) {
                nextval[i] = next[next[i] - 1];
            }
            else {
                nextval[i] = next[i];
            }
        } 
    }
    int strStr(string haystack, string needle) {
        if(needle.size() == 0) return 0;
        vector<int> next(needle.size());
        getNext(&next[0], needle);
        //1.初始化
        int j = 0;
        for(int i = 0; i < haystack.size(); i++) {
            //处理不相等的情况
            while(j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            //处理相等的情况
            if(haystack[i] == needle[j]) {
                j++;
            }
            if (j == needle.size() ) {
                return (i - needle.size() + 1);
            }            
        }
        return -1;
    }
};

标签:匹配,前缀,后缀,next,int,数组,KMP,strStr
From: https://blog.csdn.net/yachihaoteng/article/details/141310936

相关文章

  • 易优CMS网站prenext 获取上一篇、下一篇内容
    【基础用法】名称:prenext功能:获取当前文档上一篇、下一篇内容。语法:{eyou:prenextget='pre'}   上一篇:{$field.title}{eyou:else/}  上一篇:暂无{/eyou:prenext}{eyou:prenextget='next'}   下一篇:{$field.title}{eyou:else/}  下一篇:暂无{/eyou:prenex......
  • Abp vNext+SignalR
    我的项目基于ABPvNext(版本8.2.0)+Blazor+SQLServer。从官网下载的模板默认不包含SignalR,因此这里记录了如何在ABPvNext中集成SignalR实现实时通信的过程。以下是一个详细的集成步骤指南:1.安装所需的NuGet包官方文档:https://abp.io/docs/latest/framework/rea......
  • Abp vNext+SqlServer+MySqlServer
    目前项目使用的是ABPvNext版本8.2.0加上SQLServer。由于业务需求,我们需要与MySQL数据库进行对接,这意味着项目的主要功能将继续使用SQLServer,而部分特定功能将需要与MySQL数据库交互。步骤1.在项目“XXX.XXX.EntityFrameworkCore”中安装MySql包dotnetaddpackag......
  • LeetCode 556. 下一个更大元素 III(next_permutation())
    题目:556.下一个更大元素III思路:用到next_permutation(),细节看注释。next_permutation、prev_permutationclassSolution{public:intnextGreaterElement(intn){ //转变为string类型,便于调用next_permutation()strings=to_string(n);......
  • 代码随想录算法训练营day09|151.翻转字符串里的单词,卡码网:55.右旋转字符串,28.实现 str
    151.翻转字符串里的单词题目链接:https://leetcode.cn/problems/reverse-words-in-a-string/description/暴力removeExtraSpaces:voidremoveExtraSpaces(string&s){for(inti=s.size()-1;i>0;i--){if(s[i]==''&&s[i]=......
  • KMP算法
    KMP算法介绍KMP(Knuth-Morris-Pratt)算法是一种高效的字符串匹配算法,用于在主串(text)中查找模式串(pattern)。KMP通过预处理模式串,避免了在匹配失败时回溯主串,提高了匹配效率。其时间复杂度为O(n+m),其中n是主串的长度,m是模式串的长度。KMP算法的关键思想KMP算法的核心思想......
  • next_permutation
    使用next_permutation函数非常简单,以下是具体的步骤和注意事项:步骤:包含头文件:确保包含<algorithm>头文件,因为next_permutation函数位于这个头文件中。#include<algorithm>准备容器:next_permutation可以用于处理任何支持随机访问迭代器的容器,比如vector或strin......
  • VoxelNeXt 模型优化(实测提点)
     VoxelNeXt原理这里不多介绍,官方代码地址:https://github.com/dvlab-research/VoxelNeXt总结一下VoxelNeXt的特点:没有采用增大卷积核尺寸的方式来增大感受野,而是利用稀疏卷积做了两次下采样在做BEV处理时,pointpillar是batch_spatial_features.view(batch_size,self.num_......
  • KMP算法——理解 next 数组
    !注意!本文与《王道》,《严书》有所不同,字符串均从第0位开始,next数组没有添加常数1。博客为梳理思路所用,难免纰漏,希望不吝赐教。在字符串匹配中,设m为待匹配的主串S长度,n为找寻的模式串T长度。如:在主串S='ababc'中寻找模式串T='abc'则字符串匹配算法返回S中第......
  • 【HarmonyOS Next】Unity(团结引擎)对接HarmonyOS
    一、环境UnityTuanjieV1.2.3DevEcoStudioNEXTDeveloperBeta1-BuildVersion:5.0.3.402二、官方资料参考Unity鸿蒙:https://docs.unity.cn/cn/tuanjiemanual/Manual/openharmony.htmlC#调用鸿蒙ArkTS:https://docs.unity.cn/cn/tuanjiemanual/Manual/openharmon......