lc28. 找出字符串中第一个匹配项的下标(中等)-kmp算法详解

本文详细介绍了KMP(Knuth-Morris-Pratt)算法的工作原理,通过实例展示了如何利用最长相等前后缀构造next数组以提高字符串匹配效率。讲解了动态规划思想在KMP算法中的应用,并提供了Java代码实现。KMP算法通过避免不必要的回溯,将时间复杂度降低到O(n),是解决字符串匹配问题的有效方法。
摘要由CSDN通过智能技术生成

给你两个字符串 haystack 和 needle ,请你在 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 。

题解:暴力解法就不讲了。讲讲kmp算法。

先看kmp为什么能减小时间复杂度及其正确性证明,设文本串haystack为aabaaac,模块串needle为aac。n = haystack.length,m = needle.length

令指针i = 0指向haystack,j = 0 指向needle。假如是暴力解法,当i = 2,j = 2时字符不相等,指针i会回溯到1 的位置,j回溯到0重新开始。 

为了减少时间复杂度,我们就会想,假如j回溯至0而i不回溯呢。显然时间复杂度会从o(mn)减小到o(n)。但是这样会导致一个问题,当i = 5,j = 2时a!=c,不匹配,此时若是回溯j = 0而i不变,得到haystack子串ac不匹配needle,return -1。显然与答案不符。

这样做的根本问题是在回溯j的时候会错过已经匹配了的haystack字串(本例中是一个a)。

显然,我们如果将j不回溯到0,而是回溯到1,就会匹配成功了。

同理,在每一个不匹配的状态下,j都会回到一个特定的位置。而这位置与haystack和i无关,只与needle有关。

这种无后效性的递推方式,其实就是动态规划的思想。每一个不匹配时的j,都是一个状态。而kmp的next[i]其实就是dp[i]。

那么怎么得到next[i]呢?

这里要引入前后缀和最长相等前后缀的知识。对于字符串aac来说,不看最后一个字符,并且以第一个字符开始的子串及aac的前缀,即a,aa。同理后缀为c,ac。此时我们知道aac没有相等的前后缀,next[2]=0。同理对于aa字串,其最长相等前后缀为a,所以next[1]=1。而a字串没有前后缀,next[0]=0。

j012
子串aaaaac
next010

为什么求最长相等前后缀就可以得到next数组呢?回到前面的例子。回溯j=0不行的原因其实是由于aac串子串aa中存在相等的前后缀a,回溯j=0会使我们漏掉文本串中已经匹配的a,此时我们将移动至1(即最长相等前后缀中前缀的下一个位置)就能使已经匹配的信息继续保持匹配。

接下来就是怎么实现求next[i]的问题了。这里结合代码进行讲解。

public void get_next(int[] next, String needle){
        int j = 0;
        for(int i = 1; i < needle.length(); ++i){
            while(j > 0 && needle.charAt(i) != needle.charAt(j)){
                j = next[j - 1];//2.这里是最难理解的
            }
            if(needle.charAt(i) == needle.charAt(j)){
                j++;//1.
            }
            next[i] = j;
        }
    }

这里的i相当于上方表格中的j,needle[0-i]也就代表了模块串的每一个子串。而这里的j代表当前字符串最长相等前后缀长度(也是其前缀下标+1)。

从i=1开始,当needle[i]==needle[j]时我们就让j++,并且i++。显然,若是一直相等下去,那么两个指针一直同时移动,随着i++,next也就++,j则一直标记着最长相等前后缀长度(也是其前缀下标+1)的位置。也就是1.处代码的含义。

那么当needle[i]==needle[j],显然我们要将j回退到一个地方,那么回退到哪里呢。这里有一点动态规划的思想,利用先前求好的状态来求当前状态。也是实现部分最难理解最抽象的地方。即j = next[j-1]这行代码。

设模式串

j1j0i
acaab...acaac

当i,j处于此处时模式串失配。令j回溯至某个位置并且i不变。听起来是不是有点耳熟。是的,这其实就是之前文本串和模式串匹配时我们的思路。那时的文本串就是此时的模式串,而模式串就是字串abaab。按照之前的想法,我们应该让j=next[j-1]来避免错过已匹配字符。不断执行这条语句直到needle[i]==needle[j],此时的j1就是相等前缀的位置了。

当我们得到了next[i]之后,接下来的事就简单了。在此不赘述。

代码:

class Solution {
    public int strStr(String haystack, String needle) {
        int[] next = new int[needle.length()];
        get_next(next, needle);
        for(int i = 0, j = 0; i < haystack.length(); ++i){
            while(j > 0 && haystack.charAt(i) != needle.charAt(j)){
                j = next[j - 1];
            }
            if(haystack.charAt(i) == needle.charAt(j)){
                j++;
            }
            if(j == needle.length()){
                return i - needle.length() + 1;
            }
        }
        
        return -1;
    }

    public void get_next(int[] next, String needle){
        int j = 0;
        for(int i = 1; i < needle.length(); ++i){
            while(j > 0 && needle.charAt(i) != needle.charAt(j)){
                j = next[j - 1];
            }
            if(needle.charAt(i) == needle.charAt(j)){
                j++;
            }
            next[i] = j;
        }
    }
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值