参考
题目一:Leetode 28. 实现 strStr()
class Solution {
public:
int strStr(string haystack, string needle) {
if(needle.size() > haystack.size()) return -1;
int i,j;
for(i = 0;i <= haystack.size() - needle.size();i++)
{
for(j=0;j<needle.size();j++)
{
if(haystack[i+j] != needle[j]) break;;
}
if(j == needle.size()) return i;
}
return -1;
}
};
就这个题本身而言,用暴力匹配也是可以的。这种方法的实现过程如下:
从主串S = “goodgoogle” 中找到 模式串 T = "google"通常需要以下步骤(注意字串和模式串的区别):
1.主串第一位开始,S与T前三个字母匹配成功,但是S第四个字母d而T的是g,匹配失败。
2.从主串的第二位开始匹配,S第一位是o,T第一位是g,匹配失败。
3.从主串 S 的第三位开始匹配,S的第一位是o,T的第一位是g,匹配失败。
4.从主串的第四位开始匹配,S的第一位是d,T的第一位是 g,匹配失败。
5.从主串的第五位开始匹配,六个字符都相同,匹配成功。
简单地说,就是对主串的每一个字符作为字串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或全部遍历完成为止。这种算法的时间复杂度是O((m-n+1)*n),m和n分别是主串和模式串的大小。有一种专门用作字符串匹配的算法叫KMP算法,该算法的时间复杂度更低。
什么是KMP算法
kmp模式匹配算法由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
KMP算法的原理和实现
什么是前缀表
前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。其中前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串,后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
为什么要前缀表
在暴力匹配中,从主串的i位置开始匹配,如果匹配失败则下一次从i+1位置开始匹配。在KMP算法中,下一次开始匹配的位置由前缀表告诉我们,并且可以省去很多不必要的匹配来提高效率,这就是前缀表的作用。
如何计算前缀表
根据前缀表的定义,以字符串"aabaaf"为例来描述如何计算前缀表。(这里用i来遍历字符串)
- 当i = 0时,小于等于i的子串为"a",其最长相同前缀后缀的长度为0(注意前缀表、前缀和后缀的定义)
- 当i = 1时,小于等于i的子串为"aa",其最长相同前缀后缀(“a”)的长度为1
- 当i = 2时,小于等于i的子串为"aab",其最长相同前缀后缀的长度为0
- 当i = 3时,小于等于i的子串为"aaba",其最长相同前缀后缀(“a”)的长度为1
- 当i = 4时,小于等于i的子串为"aabaa",其最长相同前缀后缀(“aa”)的长度为2
- 当i = 5时,小于等于i的子串为"aabaaf",其最长相同前缀后缀的长度为0
所以前缀表为:0 1 0 1 2 0
前缀表与next数组
next数组即可以就是前缀表,也可以是前缀表统一右移一位。这并不涉及到KMP的原理,而是具体实现。
如何通过前缀表(next数组)得到下一次匹配的位置
代码随想录里并没有说怎么通过next数组得到下一次匹配位置,这里按照自己的想法整理一下。
在为什么要使用前缀表里也说了,前缀表的作用就是当匹配失败失败告诉我们下一个开始匹配的位置。这个位置是什么呢?
如下图所时,当匹配到模式串的字符’f’的时候匹配失败,也就是说在此之前的所有字符都已经匹配上了(听起来是废话,但是我觉得很重要)。
此时前缀表就用上了,j指针指向的上一个位置处前缀表的值为2,根据前缀表的定义,说明j指针前面的这个子串的前缀后缀相同的长度为2,如下图所示:
再想想刚刚强调的那句“废话”,是不是已经有答案了。按照下图的定义,根据遍历匹配,已经直到子串S1 = S2,根据前缀表知道S2 = S3,所以可以推出S1 = S3(似乎又是废话),根据这个结论,j指针下一次的位置从S3子串之后开始(即图中的字符’b’)匹配即可,而图中这个位置对应下标2,即前缀表中j-1的位置处。
如果把前缀表直接当作next数组,那么下一个位置就是next[j-1]。
代码实现
class Solution {
public:
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;
}
int next[needle.size()];
getNext(next, 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算法的原理明白了,但是上面的代码还是不太清楚,等周末再补一下。
题目二:Leetode 459.重复的子字符串
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string sub,tmp;
for(int length=1;length <= s.size()/2;length++)
{
sub += s[length-1];
if(s.size() % length != 0) continue;
int cnt = s.size() / length;
tmp = "";
while(cnt--) tmp += sub;
if(tmp == s) return true;
}
return false;
}
};
满足条件的子字符串一定是从头开始的一个子串,所以改变子串的长度,遍历所有情况,逐一进行比较,通过这样暴力求解的方法也是能可以的。利用KMP求解的方法留到周末再补。
双指针回顾
在很多情况下,通过双指针可以将O(n^2)的时间复杂度降低为O(n),使用双指针可以降低时间复杂度。
- 27. 移除元素:一个指针遍历数组,一个指针值指向新的存储空间
- 344.反转字符串:一个指针指向开头,一个指针指向末尾,两个指针同时向中间移动的过程中交换字符
- 剑指Offer 05.替换空格:先遍历字符串得到替换空格后的大小,然后扩充存储空间,再用两个指针从后往前移动,一个指针遍历字符串,一个指针指向存储空间(类似与移除元素)
- 151.翻转字符串里的单词:先去除多余的空格,然后反转整个字符串,再反转每一个单词,反转每一个单词可以用双指针
- 206.反转链表:一个指针遍历链表,一个指针指向反转后当前节点的下一个节点,这个过程注意要保存反转前当前节点的下一个节点
- 19.删除链表的倒数第N个节点:利用双指针将来定位倒数的第n个节点。先让right指针移动n步,然后两个指针一起移动,当right指针指向最后一个节点时,left指针指向要删除的节点的上一个节点
- 面试题 02.07. 链表相交:将两个链表“右对齐”,然后两个指针一起向右移动,当两个指针相等时该节点就是链表的第一个交点
- 142.环形链表II:两个指针同时从头节点出发,快指针fast一次移动两个节点,慢指针一次移动一个节点,若有环,则两个指针必定再环内相遇;一个指针从第一次相遇的节点开始,另一个指针从头节点开始,两个指针一起移动(每次一个移动一个节点),当两个指针相遇时的节点就是环的入口。
今日总结
今天的KMP算法还是挺难的,算法原来明白了,但是代码实现上还是有问题,代码问题留到这个周末补。此外,还总结了以下双指针的用法,之前做过的双指针求解的题还记得解法,理解之后双指针也没那么难。