kmp算法的使用和相关例题

KMP算法,也就是我们常说的“烤馍片”,是字符串类型题里面很重要的算法之一,主要用于字符串匹配。当出现字符串不匹配时,使用KMP算法就可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。学习KMP,一方面是为了利用这个算法做题,另一方面也是通过对kmp的运用来更加深入的理解字符串这个数据类型的本质。

而KMP算法中的精髓就是前缀表,整个算法都是基于前缀表而存在的。

关于前缀表,你需要了解这些:

  1. 前缀和后缀是什么?最长相等前后缀是什么?
  2. 前缀表是什么?前缀表怎么算?
  3. 前缀表有什么用?前缀表怎么用?

以上问题我们来一一解答。

一、前缀&后缀&最长相等前后缀

给你一个字符串,aabaaf,它的前缀和后缀都分别有五个,一一列举下来:

1

大家也许能看出来一些前缀和后缀的特点了。

前缀,就是字符串里不包含最后一个字符的、以第一个字符开头的所有子串;

后缀,就是字符串里不包含第一个字符的、以最后一个字符结尾的所有子串。

2

最长相等前后缀又是什么呢?我们还是以aabaaf为例。我们在上面已经把它的所有前后缀列出来了,我们可以看出,它根本没有相等的前后缀,所以aabaaf的最长相等前后缀的长度为0。

在这里插入图片描述

我们再以aabaa为例,我们会发现,它有两对相等前后缀,分别是a和a,aa和aa。那么aabaa的最长相等前后缀的长度就是2。

二、前缀表是什么&怎么算

前缀表的直接解释就是,在字符串里,长度为前n个字符的子串,它们最长相等前后缀的长度,所组成的数组。在题目里我们经常给它起名叫next数组。可能有点难理解,我们还是以aabaaf为例,它的前缀表是[0,1,0,1,2,0]。我们接下来演示一下它的前缀表是怎么算的,同时去理解前缀表的含义。

  1. 在字符串aabaaf中,长度为前1个字符的子串a,它的最长相等前后缀的长度为0

    与之对应的,next数组里的第1位成员的值就为0

  2. 在字符串aabaaf中,长度为前2个字符的子串aa,它的最长相等前后缀的长度为1

    与之对应的,next数组里的第2位成员的值就为1

  3. 在字符串aabaaf中,长度为前3个字符的子串aab,它的最长相等前后缀的长度为0

    与之对应的,next数组里的第3位成员的值就为0

  4. 在字符串aabaaf中,长度为前4个字符的子串aaba,它的最长相等前后缀的长度为1

    与之对应的,next数组里的第4位成员的值就为1

  5. 在字符串aabaaf中,长度为前5个字符的子串aabaa,它的最长相等前后缀的长度为2

    与之对应的,next数组里的第5位成员的值就为2

  6. 在字符串aabaaf中,长度为前6个字符的子串aabaaf,它的最长相等前后缀的长度为0

    与之对应的,next数组里的第6位成员的值就为0

好的,然后我们把next数组里六位成员值都写出来,就能得到前缀表[0,1,0,1,2,0]了。

三、前缀表有什么用&怎么用

了解了前缀表是什么,那我们就要问前缀表有什么用了。前缀表的作用就是回退,意思是当我们在匹配字符串的时候,遇到了不匹配的字符,我们就可以通过查询前缀表,回退到上一个匹配过的地方继续匹配,而不用重头匹配,从而减少匹配的次数。那前缀表是怎么实现这个作用的?

我们举一个例子,假设我们要在文本串(s):aabaabaaf 中查找是否出现过一个模式串(l):aabaaf。那么首先我们需要得到模式串的前缀表,[0,1,0,1,2,0]。接着我们开始匹配这两个字符串。

前面的匹配都没有问题,到这里出问题了:

在这里插入图片描述

匹配不上了。我们就要使用前缀表来进行回退。我们读取当前模式串字符前一位的前缀表,也就是next[4]=2。将指针退至前缀表里的值,所对应的模式串下标的字符位置,也就是 l[2]=b。如图:

在这里插入图片描述

这样,我们就避免了再匹配一次aa的情况,节省了时间。这就是前缀表的作用和用法,也就是KMP的原理。

例题里对KMP的实际应用:

下面展示两道比较基础的匹配字符串的题目,旨在展示KMP的代码实现过程及思路。

(模版题)力扣28.找出字符串中第一个匹配项的下标

28. 找出字符串中第一个匹配项的下标

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

示例 1:

输入:haystack = “sadbutsad”, needle = “sad”
输出:0
解释:“sad” 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

示例 2:

输入:haystack = “leetcode”, needle = “leeto”
输出:-1
解释:“leeto” 没有在 “leetcode” 中出现,所以返回 -1 。

提示:

  • 1 <= haystack.length, needle.length <= 104
  • haystackneedle 仅由小写英文字符组成

题目分析

使用KMP匹配字符串,haystack 是文本串,needle 是模式串,最后返回第一个匹配项的下标。

解题思路

我们现在来进行KMP的代码实现。为了更清晰直观,我们把求next数组的过程单拎出来写成函数的形式。求next数组我们可以分为四个步骤:

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

首先进行初始化。函数命名为getNext,传入函数的参数为一个next数组和模式串needle。我们先用函数strlen获得needle的长度并赋值给m,定义前缀末尾j=0(j在后面用于匹配寻找最长相等前后缀),并把0赋值给next[0](因为前缀表的第一位肯定是0,单个字符没有相等前后缀)。然后设置for循环,用来移动后缀末尾i。这个时候我们的i就要从1开始遍历了,而不是从0。因为我们要比较needle[j]needle[i],也就是比较前缀末尾和后缀末尾,i从1开始才能比较。条件是i<m,最后执行i++。初始化结束。

接下来我们处理前后缀不相同的情况。判断条件肯定有needle[i]!=needle[j],判断了以后,对前后缀匹配不上的位置进行回退的操作。我们要读取当前j的前一位的next数组的值,也就是j=next[j-1]。“遇见冲突看前一位”,这是KMP实现的一个不变量(如果是以这种格式写的next数组的话)。也因为存在这个不变量,我们的判断条件必须加上一个j>0。当数组中出现回退的操作,j-1,我们就不得不考虑它的越界访问问题,在判断的时候我们就要规避掉。

关于此处的判断是使用if还是while,也是容易出错的一个地方。我们回退的操作会进行几步?一定是一步吗?这个问题需要仔细地全面的思考。如果想不来,就举例子模拟。假设字符串aaaf,我们遍历到i=3,j=2的时候,就会发现此时就匹配不上了。如果我们用if,就代表j只会回退一步。回退一步后的j=next[j-1]=next[1]=1,接着运行程序的话,就会直接把j的值赋给next[i],也就是赋给next[3],但是很明显next[3]应该等于0,而不是1。这就代表这个时候的j回退不到位,我们得到了错误的前缀表,题也就不可能做对了。所以此处的判断必须使用while

接着我们处理前后缀相等的情况。相等的情况就使用if判断就行,不存在需要多次的情况。如果判断出来此时前后缀相等,我们就让j++。因为j作为字符串下标,会比正常的计数都要小上一个1,我们需要把它加回来才能得到正确的前缀表。

最后更新next,就直接把此时j的值赋给next[i]就可以了。至此,我们求next数组的函数就写完了,下面开始写主函数。

在主函数里,我们的操作几乎和上面的一致,只是把上面对前缀末尾j和后缀末尾i的匹配,变成了对文本串haystack和模式串needle的匹配,并且最后不需要更新next,只用做前三步即可。需要注意的就是,我们要对模式串needle长度为0的情况进行特判,因为在这种情况下是肯定不需要匹配的,直接return 0。最后加一个if(j==m),判断是否匹配到了模式串的末尾,如果满足条件就返回i-m+1,即第一个匹配项的下标。最后没有匹配到的情况,返回-1。

代码实现

void getNext(int* next,char* needle){
    int m=strlen(needle);
    next[0]=0;
    int i=0,j=0;
    for(i=1;i<m;i++){
        while(needle[i]!=needle[j]&&j>0){
            j=next[j-1];
        }
        if(needle[i]==needle[j]) j++;
        next[i]=j;
    }
}

int strStr(char* haystack, char* needle) {
    int n=strlen(haystack);
    int m=strlen(needle);
    if (m == 0) return 0;
    int* next = malloc(sizeof(int) * m);
    getNext(next, needle);
    int j=0;
    for(int i=0;i<n;i++){
        while(haystack[i]!=needle[j]&&j>0){
            j=next[j-1];
        }
        if(haystack[i]==needle[j]) j++;
        if(j==m) return (i-m+1);
    }
    return -1;
}

力扣1392.最长快乐前缀

1392. 最长快乐前缀

「快乐前缀」 是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。

给你一个字符串 s,请你返回它的 最长快乐前缀。如果不存在满足题意的前缀,则返回一个空字符串 ""

示例 1:

输入:s = “level”
输出:“l”
解释:不包括 s 自己,一共有 4 个前缀(“l”, “le”, “lev”, “leve”)和 4 个后缀(“l”, “el”, “vel”, “evel”)。最长的既是前缀也是后缀的字符串是 “l” 。

示例 2:

输入:s = “ababab”
输出:“abab”
解释:“abab” 是最长的既是前缀也是后缀的字符串。题目允许前后缀在原字符串中重叠。

提示:

  • 1 <= s.length <= 105
  • s 只含有小写英文字母

题目分析

求next数组,得到next数组的最后一位,创建一个新字符串放答案。

解题思路

这道题如果你会KMP了就很好办,因为从题目的描述里完全就可以看出来,他这个所谓的最长快乐前缀就是最长相等前后缀。我们只需要得到next数组,并且我们只需要它的最后一位元素的值,也就是next[n-1]。因为最后一位元素对应的是原字符串的最长相等前后缀,我们不需要其他子串的最长相等前后缀。

接着依旧是上一道题求next数组的过程,没有任何变化,理解了就很容易写出来。

得到以后,定义一个max变量用来保存next[n-1]的值,再+1是为了预留添加终止符‘\0’的空间。我们创建一个新字符串,给它分配一块大小为max的内存。然后用for循环把字符串s的前缀录入字符串l里,最后添加终止符,输出l,这道题就做完了。

代码实现

char* longestPrefix(char* s) {
    int i,j=0,max=0;
    int n=strlen(s);
    int*next=(int*)malloc(sizeof(int)*n);
    next[0]=0;
    for(i=1;i<n;i++){
        while(j>0&&s[i]!=s[j]){
            j=next[j-1];
        }
        if(s[i]==s[j]) j++;
        next[i]=j;
    }
    max=next[n-1]+1;
    char*l=(char*)malloc(sizeof(char)*max);
    for(i=0;i<max;i++){
        l[i]=s[i];
    }
    l[max-1]='\0';
    return l;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值