链接:28. 找出字符串中第一个匹配项的下标
思路:
KMP为匹配字符串最重要的算法之一,理解起来也比较有难度,要点为理解前缀表的含义。前缀表由一个和模式串相等长度的数组组成,其下标代表了前后缀相同的最长的字符串的长度。
前缀:所有从第一个字符开始,不包括最后一个字符的连续子字符串。
后缀:所有不包括第一个字符并以最后一个字符结尾的连续子字符串。
如字符串abcabd,如果要填充这个字符串的前缀表,我们要进行如下操作。
首先考虑子字符串a,因为只有第一个字符,前缀为a,后缀不存在,所以最长相同前后缀长度为0。
然后考虑子字符串ab,前缀为a,后缀为b,最长相同前后缀长度也为0。
子字符串abc, 前缀为a、ab,后缀为b、bc,最长相同前后缀长度还是0(注意位置必须对应上,如‘b'虽然重复出现,但是在前缀为第二个字符,后缀为第一个字符,这样也不行)。
子字符串abca,前缀为a、ab、abc,后缀为a、ca、bca,我们发现相同的前后缀为a,所以最长相同前后缀长度为1。
子字符串abcab,前缀为a、ab、abc、abca,后缀为b、ab、cab、bcab,我们发现相同的前后缀为ab,所以最长相同前后缀长度为2。
子字符串abcabd,前缀为a、ab、abc、abca、abcab,后缀为d、bd、abd、cabd、bcabd,这个时候没有相同的前后缀,所以最长相同前后缀长度为0。
综上所述,字符串abcabd的前缀表为[0, 0, 1, 2, 0]。有些kmp的实现会把前缀表的所有元素减1,其实都是可以的,只是具体实现稍微不一样。我们可以用一个函数getNext()来实现前缀表。这里以前缀表都减1为例,首先初始化j和next数组,因为都减了1,所以前缀表的第一个元素为-1。
int j = -1;
// 前缀表的第一个数为-1
next[0] = j;
然后我们需要两个指针i和j,分别代表后缀的第一个字符和前缀的第一个字符。然后用一个for循环遍历模式串,注意i初始化为1,因为后缀是不包含第一个字符的。每次循环,我们都要考虑s[i]和s[j+1]的匹配情况(这里要j+1因为j之前减了1),如果不匹配,就要不断退回到上一个匹配的地方,或者退回到next[0]的位置。
while (s[i] != s[j+1] && j >= 0)
j = next[j];
如果相匹配,就增加j,最后再更新next的值。
if (s[i] == s[j+1])
j++;
// 更新next数组
next[i] = j;
通过以上步骤完成前缀表后,便可以用前缀表快速匹配文本串和模式串。遍历文本串,每次匹配的过程中,如果文本串和模式串有不匹配的字符,按照同样的方式退回j,直到完整匹配模式串的字符,或者i走到文本串终点未找到匹配模式串的字符。
代码:
class Solution {
public:
int strStr(string haystack, string needle) {
int next[needle.size()];
getNext(next, needle);
// 从-1开始
int j = -1;
for (int i = 0; i < haystack.size(); i++)
{
// 如果文本串没有匹配模式串,j就从前缀表里退回一个位置
while (haystack[i] != needle[j+1] && j >= 0)
j = next[j];
// 如果匹配了的话,i和j同时向后移动
if (haystack[i] == needle[j+1])
j++;
if (j == needle.size() - 1)
return i - needle.size() + 1;
}
return -1;
}
void getNext(int* next, const string s)
{
// 统一减1的前缀表
int j = -1;
// 前缀表的第一个数为-1
next[0] = j;
// 注意i从1开始,因为后缀不包括第一个字符
for (int i = 1; i < s.size(); i++)
{
// i表示后缀的第一个字符
// j表示前缀的第一个字符
// 如果前后缀不相同,j根据next数组上的值退回
while (s[i] != s[j+1] && j >= 0)
j = next[j];
// 如果前后缀相同,更新j
if (s[i] == s[j+1])
j++;
// 更新next数组
next[i] = j;
}
}
};
时间复杂度:O(m+n)
空间复杂度:O(m)
其中m是模式串的长度,n是文本串的长度
链接:459. 重复的子字符串
思路:
这道题也可以用KMP来解决。通过getNext找到前缀表后,我们可以发现如果这个字符串是由重复的子字符串构成的,那么最长相同前后缀长度在经过第一个子串后,一定是增加的。如字符串abcdeabcdeabcde,其前缀为[0,0,0,0,0,1,2,3,4,5,6,7,8,9,10]。这是因为在重复的子字符串下,后缀一定可以和前缀匹配上。我们可以通过next[s.size()-1]找到最长相同前后缀长度,在这里为10,然后用字符串的长度减去10,可得重复的字串的长度15-10=5。这时再用字符串的长度模除子串的长度,如果能被字串整除,则代表字符串是由重复的子串组成的。
代码:
class Solution {
public:
bool repeatedSubstringPattern(string s) {
int next[s.size()];
getNext(next, s);
if (next[s.size() - 1] != 0 && s.size() % (s.size() - next[s.size() - 1]) == 0)
return true;
return false;
}
void getNext(int *next, const string s)
{
// 前缀表不统一减1
int j = 0;
next[0] = j;
for (int i = 1; i < s.size(); i++)
{
while (s[i] != s[j] && j >0)
j = next[j-1]; //因为要退回到前一个所以要减1
if (s[i] == s[j])
j++;
// 更新前缀表
next[i] = j;
}
}
};
时间复杂度:O(m+n)
空间复杂度:O(m)
其中m是模式串的长度,n是文本串的长度