每日两道leetcode(这题其实应该是困难题才对)

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

题目

给你两个字符串 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 。

提示:

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

官方题解

  • 如果直接调内置函数一行就写完了,但是感觉没啥意义,看了官解发现考察的是KMP算法的实现(这玩意儿我当年就没听懂,这题是简单题???),那么来看看思路吧。
  • 官解提供了两种方法,每次匹配到第一个首字符都先做一次内部遍历看看是不是,这个方法时间复杂度大概是O(n*m)的(n为),空间复杂度的为O(1)。——事实证明不要看不起暴力,合理剪枝也可以时间复杂度也可以超过100%。
  • 复现:
  • class Solution {
    public:
        int strStr(string haystack, string needle) {
            for(int i = 0; i < haystack.size(); ++i) {
                if(haystack[i] == needle[0]) {
                    int j = 1;
                    for(j; j < needle.size(); ++j) {
                        if(haystack[i+j] != needle[j]) break;
                    }
                    if(j == needle.size()) return i;
                }
            }
            return -1;
        }
    };

KMP算法学习

思路

  1. 参考:最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibili
  2. 前面考虑了暴力算法,但是每次只挪动一步显然效率是非常慢的,KMP算法的精髓在于遍历过程中也记录子串的最长前后缀匹配情况(不妨设为move数组),那么一旦发生失配,我们通过查上一个move的情况就知道到上一个子串的最长前后缀匹配关系(不妨设上一个子串的最长前后缀长度为想),那么我们重置模式串的时候就可以x长度的比较,然后从模式串x+1位置开始和待匹配串当前下标比较;反之,如果模式串当前下标和待匹配串当前下标匹配上了的话,那么就直接继续往后匹配即可。如果模式串的下标达到了模式串的长度,那么待匹配串当前下标减去模式串长度+1就是匹配的串的开始下标了。
  3. 那么问题就归结为怎么去求这个move数组:
    1. move数组存储的就是最长相同前后缀长度,比如:ABAB,其前缀集为除最后一个字符外的前缀集合:A、AB、ABA;其后缀集为除第一个字符外的后缀集合:B、AB、BAB;那么最长的相同前后缀为AB,其长度为2。
    2. 显然当下标为0时move[0] = 0,那么如果新加入的字符和已经匹配的字符串后一个字符匹配,那么move[i+1] = move[i]+1。如果失配,那么就找move[i]的记录的前缀开始匹配,如果匹配,那么就是一个新的匹配,相当于是利用最大为i-1长度的前缀更新i+1的最大前后缀(如果不匹配就回继续根据前缀的前缀缩小)——动态规划的思想(一开始没看出来)。然后不断类推直到move数组更新完成。
  4. 算出move数组后就不断地做匹配然后移动即可。

代码实现

class Solution {
public:
    int strStr(string haystack, string needle) {
        int n = haystack.size(), m = needle.size();
        if(n == 0 || m == 0) return -1;
        // 计算move数组
        vector<int> move(m);
        move[0] = 0;
        // 这里i从1开始是因为模式串长度从2开始才有前后缀的关系。
        // 这里是做模式串的最长相同前后缀长度的计算的——规模为m。
        for(int i = 1, j = 0; i < m; ++i) {
            // 不能匹配就缩前缀,但不需要一步一步缩,上一子串已经记录了能匹配前缀的情况,若不匹配再搜它的前缀即可
            while(j > 0 && needle[i] != needle[j]) {
                j = move[j-1];
            }
            if(needle[i] == needle[j]) ++j;
            move[i] = j;
        }
        // KMP
        // 这里i从0开始是因为是两个串的匹配
        for(int i = 0, j = 0; i < n; ++i) {
            while(j > 0 && haystack[i] != needle[j]) {
                j = move[j-1];
            }
            if(haystack[i] == needle[j]) ++j;
            if(j == m) return i-j+1;
        }
        return -1;
    }
};

复杂度分析

  • 时间复杂度:move数组的计算的循环是O(m)的(循环m次,最差情况是匹配到最后才发现不匹配,然后又用m次循环回溯,所以时间开销是介于[m,2m)的),匹配的循环是O(n)的(同理),所以总的时间复杂度是O(m+n)的。
  • 空间复杂度:主要是move数组的开销——O(m)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值