题目
给你两个字符串 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无疑是一个很好的选择,举个例子。
当我们面对上面的模式串 T 和字串 P 时,我们首先想到就是暴力匹配。也就是利用双重循环分别遍历两个字符串,但是这样也就意味着一旦出现最坏情况,我们的时间复杂度就会变成O(m*n),m 和 n 分别代表 T 和 P 的长度。这样的算法是很糟糕的,因此我们可以使用一种新的算法,即 KMP 算法进行字符串之间的匹配问题。
我们先给出一张表,暂且不用管是怎么来的,仅仅会用就行。
当我们匹配到这里时,发生第一次失配现象
按照常理我们应该将字符串 P 向左移动一位,同时重头开始进行匹配。 但我们已经在第一次匹配中判断了前四个字符是相匹配的,因此我们再次进行的匹配做了很多的重复性工作。这时我们就要用到上面的表了。
按照表中的对应数字,当字符 b 发生失配时,我们就可以将此时 j 的位置指到对应的位置,也就是下标为 3 的地方。
当再次失配时,我们可以将其再次移动。
直到最后完全匹配,我们得到相匹配的位置。
下面我们给出上表的求法,我们先求出 P 字串的前缀表。
首先,要了解两个概念:“前缀"和"后缀”。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"aaaab"为例,
- "a"的前缀和后缀都为空集,共有元素的长度为0;
- "aa"的前缀为[a],后缀为[a],共有元素的长度为1;
- "aaa"的前缀为[a, aa],后缀为[aa, a],共有元素的长度2;
- "aaaa"的前缀为[a, aa, aaa],后缀为[aaa, aa, a],共有元素的长度为3;
- "aaaab"的前缀为[a, aa, aaa, aaaa],后缀为[aaab, aab, ab, b],共有元素长度为0;
我们得到了 0 1 2 3 0,我们称其为前缀表。我们在这里对其进行向右移动一位,空缺的地方使用 -1 代替。(也有不进行变化的算法,对于前缀表我们有多种处理办法,这里才用较为方便的一种)。
于是我们可以得到 -1 0 1 2 3 ,也就是我们所称的 next 数组。
了解了 KMP 算法的实现原理,我们在下面进行一下代码的实现。
首先第一个字符是不存在前后缀的,因此一定为 0 。
现在我们来给出一个 len 代表已经成立的最大前缀和,用 i 来代表需要求的前缀值的下标
我们可以看到当我们在求 i 对应字符的值的时候,我们已经知道了前面的字符的最大匹配值是2,也就是说如果此时的P[len] == P[i],我们的 len 值就可以直接加上一,并且赋值给 i 对应的前缀值。
但是当比较的字符不相同时,我们需要怎么办呢,在上面的情况中我们可能直接认为一旦出现不相等时,直接为0,但是如果出现下面的情况呢。
如果出现这样的情况,我们就不可以仅仅使用 0 来赋值了,很明显上面的值应该是 1 而非 0 ,我们需要利用已经成立的前缀和,但是此时 len 本身的前缀和是不行的,因为字符 b 本身就匹配失败,所以它的最大前缀和 ab 也是一定失败的,它的前缀和的值 2 也就无法使用了,因此我们需要向前看,也就是 len - 1 的值,len = prefix[len-1]。
此时仍然不满足字符相等,因此继续该操作,
我们发现此时满足字符相等,因此我们使 len 加 1,并且赋值给prefix[i]。
其实关于kmp算法,个人认为其本质的地方就在于利用已知并记录的数据来避免重复的判断。所谓的最大公共前后缀和也就是一个字符串前后缀的相似度,对于一个字符串 aabaaf ,当我们判断到 f 出现失配现象时,由于其前面的 aa 在前面出现过,也就是说因为直到 f 才出现失配现象,那么前面的 aa 是匹配的,而 aa 在前面也有一个,这是第二个 aa ,因此我们第一个 aa 无需判断,直接来到 aa 后面的第一个字符 b 即可。
我的思路并不完善,并且对于在进行 next 数组计算中出现现象时为什么 len = prefix[len-1] 的理解并不明朗,推荐两个讲的很好的链接,也是我进行 kmp 学习的链接
视频:
KMP字符串匹配算法1_哔哩哔哩_bilibili
文章:
字符串匹配的KMP算法 - 阮一峰的网络日志
代码:
class Solution {
public:
//计算前缀表
void prefix_table(const string& pattern,vector<int>& prefix,int n){
//初始化
prefix[0] = 0;
int len = 0;
int i = 1;
while(i < n){
//当判断的值相同时
if(pattern[len] == pattern[i]){
len++;
prefix[i] = len;
i++;
}else{//当判断的值不相同时
if(len > 0){
len = prefix[len-1];
}else{
prefix[i] = len;
i++;
}
}
}
}
//计算next数组
void next_table(vector<int>& prefix,int n){
int i;
for(int i = n-1;i > 0;i--){
prefix[i] = prefix[i-1];
}
prefix[0] = -1;
}
int strStr(string haystack, string needle) {
int n = needle.size();
vector<int> prefix(n);
prefix_table(needle,prefix,n);
for(int i = 0;i < n;i++){
cout << prefix[i] << " ";
}
cout << endl;
//计算next数组
next_table(prefix,n);
for(int i = 0;i < n;i++){
cout << prefix[i] << " ";
}
//使用next数组
int j = 0;
int i = 0;
while(i < haystack.size()){
// cout << haystack[i] << " " << needle[j] << endl;
if(j == n - 1 && haystack[i] == needle[j]){
return i - j;
}
if(haystack[i] == needle[j]){
i++;
j++;
}else{
j = prefix[j];
if(j == -1){
j++;
i++;
}
}
}
return -1;
}
};