KMP算法(多种实现方式)

这篇具有很好参考价值的文章主要介绍了KMP算法(多种实现方式)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

KMP算法核心思想

利用已经匹配的数据,去除无效的从头匹配

KMP算法流程

首先我们找到 i=9,j=9时不匹配,如果时暴力算法,此时i应重新来到i=2的位置,j返回j=1的位置,开始新一轮的匹配
KMP算法(多种实现方式)
这样暴力匹配,就白白浪费了已经匹配的串,那么问题来了,我们应该如何利用已经匹配的串呢??

我们看着图片,假设i返回i=2,j返回j=1,i++,j++,i指向b,j指向a,此时就不匹配了,又要重新开始,i来到3,j又回到j=1,双方指向第一个元素就不匹配,kmp算法的核心就是过滤掉这种低效的匹配,我们往下看

KMP算法(多种实现方式)

我们直到i=9和j=9前面的串 a a b a a b a a无论在S串或者J串是完全相等的,这也就意味着,我下面的操作无论在S串操作或者在J串操作,只要不出这个范围,在哪里操作都是一样的,因为他们都是同一串!

我们找到紧挨着i=9,元素b前的一串后缀(aabaa),一定要紧挨着,然后找到 J串从第一个元素开始的,与aabaa相等的前缀! 为什么要这样找呢?? 我们可以发现i从这个前缀的第一个元素开始能够完全匹配,J从第一个元素开始一直到这个缀结束,这样就可以过滤掉,大量的无效匹配,比如刚才的 i=2,i=3,都属于无效匹配
KMP算法(多种实现方式)
此时有个疑问,既然是紧挨着i=9的后缀,和J第一个元素出发的前缀,那么aa不可以吗?? 当然可以啦,从aa开始匹配,也就是i=7开始,新一轮匹配,但是这样会丢解,也就是,i跳的步子大了,跨过了可行解的下标!

通过上面的铺垫,我们引出一个概念

最长相等前后缀

从第一个元素开始,不包括最后一个元素结束,和从最后一个元素开始,不包括第一元素得出的前缀和后缀,前缀和后缀满足,相等且最长

通过上面讲解,我们就是要利用这个最长相等前后缀,达到过滤无效匹配的效果,上面讲过,我的操作完全可以只在J串进行,无需在S串进行,所以我让i=9不动,让J移动到J=5的位置,开始匹配i,和j+1所对应元素是否相等即可! 这也就相当于暴力算法,从i=4开始匹配,匹配到了i=8的位置,故通过寻找最长相等前后缀可以快速达到此效果!!

故:在使用kmp算法时,需要求出J串的每个位置的最长相等前后缀 (有的算法实现时会求出,每个位置 ‘前’ 的最长相等前后缀,这在后面会讲解,不同的求法,实现代码不同,因此会有差异)

求解Next数组

我们依J串为例a a b a a b a a a a求出它的Next数组(next数组存放每个位置的最长前后缀)
KMP算法(多种实现方式)
那么如何用代码实现呢?
KMP算法(多种实现方式)
我们先观察next数组,我们发现只要多进来一个元素,并且满足最长前后缀,那么next数组就会增加1,那么如果不满足呢? 实际上我们观察代码可看到i起始从2开始,j从0开始,每次用j+1的位置进行匹配,我们可以理解为虽然用的是同一条串,但是由于i和j从不同起点出发导致,出现了一个新两个串匹配问题,那么就可以转化为我们刚刚讲的kmp思想

我们看以下匹配模拟过程
KMP算法(多种实现方式)
当i=9时,j=5,j+1元素为b与a并不匹配,也就意味着i=9无法继续发扬光大,无法继承上依次最大前后缀,所以需要寻找新的,也就回到了最初的两个串匹配问题,不同则回退,所以j回退到next[5]的位置(此时next[5]已经求出来了),j落到j=2,用j+1继续匹配i,发现匹配仍然失败b!=a,故j移动到next[j]位置也就是j=1,继续用j+1匹配,匹配成功,退出while循环,i++,j++,接着算出next[10]

这里解释以下为什么j从0开始,i从2开始,如果j从0开始,i从2开始,那么每次只需要用j+1去匹配i(“进可攻”),如果不匹配直接j=next[j] (“退可守”),所以我们现在的next数组含义是如果正在匹配的元素不相等,则需要他们之前的next[j],而不是他们自身的next[j],后面会讲解,直接利用自身的next不必需要前一个位置的next回退(一定注意区分)

模式串与主串匹配过程

KMP算法(多种实现方式)

这里的代码和求next函数代码十分相似,只是求next函数用的一个串,这里变为了两个串,注意起始位置,i从1开始j从0开始,之前是i从2开始,因为只有从2开始才能领先一个位置,用j+1去比较,不然j+1和i指向同一个元素无法达到进可攻退可守的效果!

竞赛版本代码

kmp模板题目洛谷P3375

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;

int ne[maxn];
int la,lb; //主串长la,子串长lb
char a[maxn],b[maxn]; //a主串 b子串

int main()
{

    cin>>a+1;
    cin>>b+1;
    la=strlen(a+1);
    lb=strlen(b+1);
    //预处理next函数
    for(int i=2,j=0;i<=lb;i++)
    {
        while(j&&b[i]!=b[j+1])j=ne[j]; //如果不匹配每次向前跳
        if(b[i]==b[j+1])j++;//匹配则j指针前移
        ne[i]=j;
    }

    //根据next数组进行匹配
    for(int i=1,j=0;i<=la;i++)
    {
        while(j&&a[i]!=b[j+1])j=ne[j];//模式串前移动,也就是回跳
        if(a[i]==b[j+1])j++;
        if(j==lb)printf("%d\n",i-lb+1);
    }

    for(int i=1;i<=lb;i++)
    {
        cout<<ne[i]<<" ";
    }


    return 0;
}

上面版本代码复杂,由于求next数组,遍历第一个for循环,里面还有while循环,并且i和j的起始位置,也不大相同,但是next数组的想法特别简单,就是求解到当前位置的,最长前后缀,我么针对next数组进行动手脚,进而简化代码,下面的代码也是考研常见的版本,竞赛中几乎不会使用这个版本,两个版本各有千秋!

对next数组修改

上面版本每次匹配都需要用j+1的位置去匹配,而非j自身,如果j+1不匹配j进行回退,那么反过来一想,能不能让j自身去匹配,如果不相等,则仍然是执行j=next[j]呢? (回退后仍然是j直接匹配)

答案是可以的!

我么将next数组元素值进行向右移动一位,然后+=1就可以达到此效果!
KMP算法(多种实现方式)

我们来手动解释以下,当向右移动一位后,next[j]的值表示,前一位的最长相等前后缀,我们此时移动到该位置发现需要用j+1去和i匹配,所以j还需要+=1,才能直接达到移动后直接和i去匹配的效果! (时刻记得和之前next数组的区别)

前面的next数组手推,是以当前元素为最后一个结点的最长前后缀,现在的next数组,需要考虑以前一个元素为结尾的最长前后缀长度然后在+=1即可,我们来手推以下!

手动匹配next数组方法

在当前元素,前画一个括弧,寻找括弧中最长前后缀,最后+=1

KMP算法(多种实现方式)

考研求解next数组代码

void get_next(String T,int next[])
{
    int i=1,j=0; //i领先一个位置
    next[1]=0;
    while(i<T.length)
    {
        if(j==0||T.ch[i]==T.ch[j]) //如果j=0时,意味着当前i无法和模式串匹配,i需要向后移动
        {
            i++;j++;             //i++相当于next[j]赋值给next[j+1],j++相当于移动到可以直接匹配的位置
            next[i]=j;
        }else{
            j=next[j]; //回退
        }
    }
}

这两个版本的代码差异挺大,主要是由于,next数组右移动一位,然后+=1造成的。初始时i=1,j=0,这样构成字符差,将一串字符划分为两串,然后while循环结束条件为i<T.length,由于最后一个位置的next[j]由next[j-1]向右移动,所以不需要遍历到i=T.length

if中的条件中j==0时,由于i和j一直不匹配导致,j=next[j]一直进行,所以最后移动到j=0,即i元素对应的子串,没有最长前后缀,所以需要next[i+1]应该跳到1号位置,注意:每次都是求前一个元素的最长前后缀,在赋值给当前元素,在代码中体现就是,满足if语句,然后i++,这个i++就是手动求next数组,向右移动一位,然后j++,对应如果发生回跳,应该跳到能够直接去匹配的位置,而不是最长前后缀的最后一位。

if语句中T.ch[i]==T.ch[j],注意此时i宏观上是i-1,也就是以前一个结点为最后结点的最长前后缀,需要注意next[i-1] 并不代表以i-1为最后一个结点的最长前后缀,所以第i-1对应元素作为需要和j匹配的元素,代码视觉上是i去匹配,实际上是i-1去匹配,如果匹配成功证明 i-1可以发扬光大,不过匹配失败,那么最长前后缀只好缩短!

来看下面图解

KMP算法(多种实现方式)
如果匹配不成功
KMP算法(多种实现方式)

考研版本代码

#include <bits/stdc++.h>
using namespace std;

#define MAXLEN 225

typedef struct
{
    char ch[MAXLEN];
    int length;
} String;

void get_next(String T,int next[])
{
    int i=1,j=0; //i领先一个位置
    next[1]=0;
    while(i<T.length)
    {
        if(j==0||T.ch[i]==T.ch[j]) //如果j=0时,意味着当前i无法和模式串匹配,i需要向后移动
        {
            i++;
            j++;             //i++相当于next[j]赋值给next[j+1],j++相当于移动到可以直接匹配的位置
            next[i]=j;
        }
        else
        {
            j=next[j]; //回退
        }
    }
}

int  Index_KMP(String S,String T,int next[])
{
    int i=1,j=1;
    while(i<=S.length&&j<=T.length)
    {
        if(j==0||S.ch[i]==T.ch[j])
        {
            i++,j++;//匹配成功,i和j向后移动
        }
        else
        {
            j=next[j];//j回调可以直接匹配的位置

        }
    }
    if(j>T.length) return i-T.length; //不需要+1,因为while1循环退出之前i++,用来while循环判断了
    else return 0;
}



int main()
{
    String s1,s2; //s1母串,s2子串
    char a[MAXLEN],b[MAXLEN];
    cout<<"输入母串:"<<endl; //aabaabaabaabaabaaaa
    cin>>s1.ch+1;
    s1.length=strlen(s1.ch)-1;
    cout<<"输入子串:"<<endl;//aabaabaaaa
    cin>>s2.ch+1;
    s2.length=strlen(s2.ch)-1;
    int next[MAXLEN];
    get_next(s2,next);
    cout<<"next数组如下"<<endl;
    for(int i=1;i<=s2.length;i++)
    {
        cout<<next[i]<<" ";
    }
    cout<<endl;
    printf("匹配位置:%d\n",Index_KMP(s1,s2,next));



    return 0;
}

运行结果
KMP算法(多种实现方式)

对KMP算法求解next数组优化(nextval)

由考研版本可知,next数组是回跳到可以直接匹配的位置,那么问题来了,当前i和j所对应元素发生不匹配,j进行回跳,如果回跳后的元素和回跳前的元素相当,那么岂不是白跳了,跳了也是不匹配!

KMP算法(多种实现方式)

我们看到i=4,j=4发生不匹配,子串前4个元素相同,那么按照之前的回跳方法需要先跳到j=3,仍然不匹配,在跳j=2,j=1,j=0,最后i++,元素b匹配失败,这样需要回跳好多步,导致性能退化。

解决方法很简单,只需要在求next数组过程中,判断以下当前元素和回跳后的元素是否相等即可,如果不相等,则next[i]=j,如果相等,则next[i]=next[j]即可有点递归感觉,只需要一次就可以,因为是从前向后找,所以前面的next[j]一定保证了不会跳到相同元素对应位置! (有动态规划的感觉了)

实现优化next数组代码

#include <bits/stdc++.h>
using namespace std;

#define MAXLEN 225

typedef struct
{
    char ch[MAXLEN];
    int length;
} String;



int  Index_KMP(String S,String T,int next[])
{
    int i=1,j=1;
    while(i<=S.length&&j<=T.length)
    {
        if(j==0||S.ch[i]==T.ch[j])
        {
            i++,j++;//匹配成功,i和j向后移动
        }
        else
        {
            j=next[j];//j回调可以直接匹配的位置

        }
    }
    if(j>T.length) return i-T.length; //不需要+1,因为while1循环退出之前i++,用来while循环判断了
    else return 0;
}

void get_nextval(String T,int nextval[])
{
    int i=1,j=0;
    while(i<T.length)
    {
        if(j==0||T.ch[i]==T.ch[j])
        {
            i++,j++;
            if(T.ch[i]!=T.ch[j])
            {
                nextval[i]=j;  //如果不相等正常赋值
            }
            else
            {
                nextval[i]=nextval[j]; //相等时
            }
        }
        else
        {
            j=nextval[j];
        }
    }
}


int main()
{
    String s1,s2; //s1母串,s2子串
    char a[MAXLEN],b[MAXLEN];
    cout<<"输入母串:"<<endl; //aaabaaaab
    cin>>s1.ch+1;
    s1.length=strlen(s1.ch)-1;
    cout<<"输入子串:"<<endl;//aaaab
    cin>>s2.ch+1;
    s2.length=strlen(s2.ch)-1;
    int next[MAXLEN];
    get_nextval(s2,next);
    cout<<"nextval数组如下"<<endl;
    for(int i=1; i<=s2.length; i++)
    {
        cout<<next[i]<<" ";
    }
    cout<<endl;
    printf("匹配位置:%d\n",Index_KMP(s1,s2,next));



    return 0;
}

KMP算法(多种实现方式)

KMP算法复杂度分析

母串长n,模式串(子串)长m

首先我们分析一下暴力版本复杂度,i需要从1回退,2回退,3回退,一共回退n次,j每一次回退后都需要遍历自身长度,那么就需要n乘以m次操作,O(nm)

kmp算法i并不会回退,所以i从1到n一共执行n次操作故复杂度O(n),不要忘记还要求next数组的复杂度,求next数组,和kmp一样都i都不会回溯,所以为O(m),故总时间复杂度O(n+m),这个复杂度在代码上很好理解,尤其是考研版本的代码,只需要一个while循环!

总结

整体来说kmp算法,初学复杂,尤其是对于next数组,不同求解,不同含义,容易让人混淆,笔者在这里对next数组来龙去脉进行贯穿讲解,分成了竞赛版,和考研版本,大家各取所需!竞赛版本,next数组想法十分简单,实现起来比考研版本复杂,但考研版本next数组含义复杂,代码实现简单,不过对i位置的理解容易懵掉,大家加油!!!

感谢大家观看,感谢B站董晓算法,十分感谢!
董晓算法讲解kmp文章来源地址https://www.toymoban.com/news/detail-480334.html

到了这里,关于KMP算法(多种实现方式)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C语言实现快排核心思想(双指针法)

     核心代码: 这就是每一趟快排的实现代码,由上面的动图,我们能知道前后指针法的核心是玩好cur和prev这两个指针,具体的逻辑是cur找比key小的值,找到就prev++,然后prev和cur的值就进行交换,但是总不能自己跟自己交换吧,这就是多此一举了,所以我们在代码中的if语句里

    2024年02月02日
    浏览(57)
  • KMP算法 Java实现

    Problem: 28. 找出字符串中第一个匹配项的下标 目录 解题方法 思路 构建next数组 回溯查找 复杂度 Code 构建next串 回溯查找next串,最后下标 通过最大前缀后缀能找到下一次未查找到后要回溯的位置 无论如何第一个数的下一次回溯位置肯定是0,因此 next[0]=0 这里的 j 表示前缀起始位

    2024年04月17日
    浏览(34)
  • KMP算法【C++实现】

    BF算法 字符串匹配,我们一般思路是被对比的串作为主串,对主串的的每一个字符串作为子串的开头,与要匹配的字符串进行匹配,匹配不成功则子串开头+1,要匹配的字符串回溯到1,进行匹配,直到匹配成功或者主串全部遍历完成,这就是BF算法。 分析时间复杂度,假设主

    2023年04月27日
    浏览(59)
  • KMP算法(C语言实现)

    博主的博客主页 :CSND博客 Gitee主页 :博主的Gitee 博主的稀土掘金 :稀土掘金主页 博主的b站账号: 程序员乐 公众号——《 小白技术圈 》,回复: ml学习资料 。1T学习资料免费分享给你。 在经典的字符串匹配中,如果字符匹配失败i会返回到开始匹配时的后一个字符

    2023年04月22日
    浏览(33)
  • 简单有趣的轻量级网络 Shufflenet v1 、Shufflenet v2(网络结构详解+详细注释代码+核心思想讲解)——pytorch实现

         这期博客咱们来学习一下Shufflenet系列轻量级卷积神经网络,Shufflenet v1 、Shufflenet v2。 本博客代码可以直接生成训练集和测试集的损失和准确率的折线图,便于写论文使用。 论文下载链接: Shufflene系列轻量级卷积神经网络由旷世提出,也是非常有趣的轻量级卷积神经网

    2024年02月01日
    浏览(47)
  • 防抖和节流及多种实现方式

    当用户在网页中进行操作时,如点击、滚动、输入等,往往会频繁地触发事件。如果每个事件都立即执行相应的函数,可能会导致性能问题和用户体验不佳,因为这些函数可能需要执行复杂的操作,如计算、网络请求等。 为了优化这种情况,我们可以使用防抖和节流来限制函

    2023年04月24日
    浏览(49)
  • 【数据结构】模式匹配之KMP算法与Bug日志—C/C++实现

    ​ 🌈 个人主页: Sarapines Programmer 🔥  系列专栏: 《数据结构奇遇记》 🔖墨香寄清辞: 墨痕寄壮志,星辰梦未满。 通幽径心凝意,剑指苍穹势如山。 目录 🌞1. 模式匹配的基本概念 🌞2. 模式匹配的解决办法 🎈2.1 暴力匹配(BF)算法 🎈2.2 KMP算法 🤖2.3 BUG记录_KMP算法 1

    2024年02月04日
    浏览(54)
  • 【排序算法】 归并排序详解!深入理解!思想+实现!

    🎥 屿小夏 : 个人主页 🔥个人专栏 : 算法—排序篇 🌄 莫道桑榆晚,为霞尚满天! ​ 什么是归并?通过归并排序就能让数据有序?分治法是怎么在归并排序上应用的?本文将对归并排序进行细致入微的讲解,庖丁解牛般让你彻底明白归并排序! 归并排序(MERGE-SORT)是建

    2024年02月08日
    浏览(44)
  • 双指针算法,基础算法实践,基本的算法的思想,双指针算法的实现

    双指针算法是一种常用于解决数组和链表问题的算法技巧。它的核心思想是使用两个指针在数据结构中按照一定的规则移动,从而达到快速搜索或处理数据的目的。这个技巧通常用于 优化算法 , 降低时间复杂度 ,提高程序的执行效率。双指针算法有多种应用场景,以下是其

    2024年02月11日
    浏览(40)
  • Linux实现查看文件内容的多种方式

    目录 1、more:分屏显示文件内容。 2、less:文本内容查看器 3、head -n:显示文件前n行到终端 4、tail -n:显示文件后n行到终端 5、实现实时查看文件内容(追踪文件)         除了使用vi/vim 编辑器查看文件内容和使用cat命令将文件所有内容展示到终端上以外,还有多种方式。

    2024年02月12日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包