⭐今日份题目
给你两个字符串 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
仅由小写英文字符组成
⭐题目思路
还是先提取一下题目特征点:
-
完全匹配(与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算法的时间复杂度比暴力求解小,内存消耗比暴力求解大,但可能是因为测评数据或者其他原因,导致提交结果如上两图所示。
欢迎大家提供更高效的代码,如果过后有更优化的思路我还会继续更新的,大家评论区见!
点赞收藏不迷路⭐~