⭐每天一道leetcode:28.找出字符串中第一个匹配项的下标(简单;暴力解;KMP算法,有难度)

⭐今日份题目

给你两个字符串 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 仅由小写英文字符组成

⭐题目思路

还是先提取一下题目特征点:

  • 完全匹配(与needle相等的部分)

  • 第一个匹配项

  • 返回下标

😭先说说我刚开始的错误思路:

我想的是从头到尾遍历,然后找到最长匹配子串长度,如果和needle的长度相等,那么就是找到了,其中,只要有一个位置不匹配,那么就将匹配子串长度设为0。但我忽略了一个问题,如haystack=mississippi和needle=issip,从第二个字母开始匹配,匹配到issis时发现不是,长度置为0,但是其中包含了needle的is部分,那么长度应该为2且下一个字母应该与needle的第三个字母判断是否相等,所以就出错了。

🍟暴力求解

接下来说说暴力求解的方法:

暴力求解的思路就是生成haystack中所有长度与needle相同的子串,然后找到第一个匹配项,返回其下标。

⭐暴力求解代码
class Solution 
{
public:
    int strStr(string haystack, string needle) 
    {
        int n=haystack.size(), m=needle.size();
        for (int i=0;i+m<=n;i++) //遍历所有子串的开头所在的位置
        {
            int flag=1;
            for(int j=0;j<m;j++)//遍历子串中的所有字母 
            {
                if(haystack[i+j]!=needle[j])//该子串与needle不匹配了 
                {
                    flag=0;
                    break;
                }
            }
            if(flag==1) return i;//该子串匹配了,直接返回开头下标即可
        }
        return -1;//遍历完所有子串了,还没有return下标,说明没有匹配的子串,返回-1
    }
};
暴力求解提交结果

🌮KMP算法

Knuth-Morris-Pratt 算法的核心为前缀函数,记作 π(i),其定义如下:

对于长度为 m 的字符串 s,其前缀函数 π(i)(0≤i<m) 表示 s 的子串 s[0:i] 的最长的相等的真前缀与真后缀的长度。特别地,如果不存在符合条件的前后缀,那么 π(i)=0。其中真前缀与真后缀的定义为不等于自身的的前缀与后缀。

举例说明:字符串aabaaab的前缀函数值依次为0,1,0,1,2,2,3。

π(0)=0,因为 a 没有真前缀和真后缀,根据规定为 0(可以发现对于任意字符串 π(0)=0 必定成立);
​
π(1)=1,因为 aa 最长的一对相等的真前后缀为 a,长度为 1;
​
π(2)=0,因为 aab 没有对应真前缀和真后缀,根据规定为 0;
​
π(3)=1,因为 aaba 最长的一对相等的真前后缀为 a,长度为 1;
​
π(4)=2,因为 aabaa 最长的一对相等的真前后缀为 aa,长度为 2;
​
π(5)=2,因为 aabaaa 最长的一对相等的真前后缀为 aa,长度为 2;
​
π(6)=3,因为 aabaaab 最长的一对相等的真前后缀为 aab,长度为 3。

有了前缀函数,我们就可以快速地计算出模式串在主串中的每一次出现。

(KMP算法解析来自leetcode官方,更多解析欢迎到leetcode官网进行查询)

本题解法:

记字符串 haystack 的长度为 n,字符串 needle 的长度为 m。

我们记字符串 str=needle+#+haystack,即将字符串 needle 和 haystack 进行拼接,并用不存在于两串中的特殊字符 # 将两串隔开,然后我们对字符串 str 求前缀函数。

因为特殊字符 #\ 的存在,字符串 str 中 haystack 部分的前缀函数所对应的真前缀必定落在字符串 needle 部分,真后缀必定落在字符串 haystack 部分。当 haystack 部分的前缀函数值为 m 时,我们就找到了一次字符串 needle 在字符串 haystack 中的出现(因为此时真前缀恰为字符串 needle)。

实现时,我们可以进行一定的优化,包括:

  • 为了节约空间,我们无需真正地创建str,只需顺序遍历字符串needle、#和haystack即可

  • 无需保存所有前缀函数的结果,只需要保存字符串needle部分的前缀函数即可(#的前缀函数必定为0,这样我们计算 π(i) 时,j=π(π(π(…)−1)−1) 的所有的取值中仅有 π(i−1) 的下标可能大于等于 m。我们只需要保存前一个位置的前缀函数,其它的 j 的取值将全部为字符串 needle 部分的前缀函数)

  • 无需特别处理特殊字符 #\,只需要注意处理字符串 haystack 的第一个位置对应的前缀函数时,直接设定 j 的初值为 0 即可

这样我们可以将代码实现分为两部分:

  • 第一部分是求 needle 部分的前缀函数,我们需要保留这部分的前缀函数值。

  • 第二部分是求 haystack 部分的前缀函数,我们无需保留这部分的前缀函数值,只需要用一个变量记录上一个位置的前缀函数值即可。当某个位置的前缀函数值等于 m 时,说明我们就找到了一次字符串 needle 在字符串 haystack 中的出现(因为此时真前缀恰为字符串 needle,真后缀为以当前位置为结束位置的字符串 haystack 的子串),我们计算出起始位置,将其返回即可。

KMP算法代码
class Solution 
{
public:
    int strStr(string haystack, string needle) 
    {
        int n=haystack.size(),m=needle.size();
        if(m==0) return 0;
        vector<int> pi(m);
        for(int i=1,j=0;i<m;i++) //求needle部分的前缀函数
        {
            while(j>0&&needle[i]!=needle[j]) 
            {
                j=pi[j-1];
            }
            if(needle[i]==needle[j]) j++;
            pi[i]=j;
        }
        for(int i=0,j=0;i<n;i++) 
        {
            while(j>0&&haystack[i]!=needle[j]) 
            {
                j=pi[j-1];
            }
            if (haystack[i]==needle[j]) j++;
            if (j==m) 
            {
                return i-m+1;
            }
        }
        return -1;
    }
};

这个算法有难度,欢迎大佬们评论区留言⭐~

KMP算法提交结果

理论上KMP算法的时间复杂度比暴力求解小,内存消耗比暴力求解大,但可能是因为测评数据或者其他原因,导致提交结果如上两图所示。

欢迎大家提供更高效的代码,如果过后有更优化的思路我还会继续更新的,大家评论区见!

点赞收藏不迷路⭐~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值