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