一、找出字符串中第一个匹配项的下标-LeetCode 28
Leecode链接: leetcode 28
文章链接: 代码随想录
视频链接: B站
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
思路
文本串为haystack,模式串为needle。kmp匹配特殊在于在遍历两个字符串时,如果当前文本串与模式串的某一个字符不匹配,kmp不会让模式串的指针从头重新遍历,而是寻找匹配好的子串后一项,从该项继续匹配,如下图所示。aa元素就是匹配好的子串,所以需要移动到aa后一项。
关于匹配好的子串的理解,换个名字也就是听的最多的名字:最长公共前后缀的子串。前缀子串就是不包含最后一个字符的子串,后缀子串就是不包含首个字符的子串。
以上图为例,把f去除后,needle中前缀子串有:a、aa、aab、aaba;后缀子串有:a、aa、baa、abaa。可见,最长公共前后缀字串为aa。aa也就是匹配好的子串。
因为f跟在后缀aa之后,而在前缀中也有aa子串,f不匹配,那么是不是可以将j移动到前缀的aa后一项继续匹配,从而避免将j移动到needle的开头了?
那现在问题就集中于:如何让j指针移动到needle的b元素呢,这时需要一个next数组,这个数组作用就是当j指针与i指针不匹配时,会告诉我们j指针该移动到哪个下标。
关于next的求解过程,它的求解过程与kmp匹配过程类似,就是加了个记录该跳到哪的数组next。求解时,使用两个指针j、i(这里指针与上面图中指针不是一个意思),i指向后缀末尾,j指向前缀末尾。j初始化为0,i初始化为1,为什么为1,因为只有为1时才能比较,next[0]初始化为0。首先判断如果i与j的字符相等,那就让next[i] = 1,且让j++,表示前后缀有一个相等,这很好理解;如果不等,那就让next[i] = 0,j不变。这里关于j何时自加我的的理解是:如果相等,表示前缀与后缀有一个字符已经相等了,因为j初始值为0,j需要自加为1然后将这个值赋值给next,注意,这个值就是前缀子串与后缀子串匹配成功的下一个位置,也是后面不等时需要退到下标值。如果不等,前缀的尾字符不满足后缀的尾字符,那么表明j需要回退,表示本次匹配失败需要看看前面的字符是否还有匹配的,直到回退到前缀的首字符,回退的原理与kmp匹配失败时回退的策略一致。
重复一遍:为什么要回退到next[j-1],因为当前字符不等了,我需要知道排除这个不字符后,前面的字符串中还有没有匹配好的字符(也就是最长公共前后缀子串),如果有,我就移动到那个位置。
可以画一个数组写出next数组,这样会更有体会
实现代码
//cpp
class Solution {
public:
//求next数组函数
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
//查找匹配的字符函数
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
vector<int> next(needle.size());
getNext(&next[0], needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
问题
忘了算法原理,当作复习。
总结
kmp算法需要多次理解,重点在于next数组存了什么,i指什么,j指什么。