Day09
- KMP
- 28. 实现 strStr()
- 459.重复的子字符串
KMP
KMP是三个人人名缩写,用于在文本字符串text
中搜索pattern
字符串,返回在text
中第一出现的位置。
算法做法就是在暴力匹配的基础上加速匹配。通过对pattern
字符串求next
数组(该数组也成为前缀表),来跳过text
部分匹配。next
数组是为了实现跳过部分字符串的功能来设计的。暴力匹配的时间复杂度为
O
(
m
×
n
)
O(m\times n)
O(m×n),KMP的时间复杂度为
O
(
m
+
n
)
O(m + n)
O(m+n)。
如何求出next
数组?通过求出pattern
所有字符子串的最长相等前、后缀构成next
数组。具体实现可能存在其他加工,如对next
数组减一,位移一位等。
假设有abbabbk
,对于字符k
有一个信息,k
之前字符串中,前、后缀匹配最大长度为3。其中前、后缀为k前字符串的真子字符串,即不包含整体abbabb
字符串。为什么不包含,因为是next
数组对应长度的下标不能等于它本身,k
是第6
个元素,next[k] != 6
,而且整体字符串也一定相等,无意义。
int next[] = {-1/*人为规定*/, 0/*根据定义*/, 0/1, ...};
求next数组是往前跳的过程,最后用到前三个元素来得到最终数组。
//通过回跳得到完整的next数组
void getNext(const string& s, int* next) {
//初始化next数组
next[0] = -1;
//由于next数组是看之前的字符。当s只有一个长度时,之前没有字符。所以只能是在此跳出。
if (s.size() == 1) return;
next[1] = 0;
int i = 2;//遍历next数组,next数组的下标
int cn = 0;//回跳的值,也是next数组的值
while (i < s.size()) {
if (s[i - 1] == s[cn]) {//next[i]是用next[i-1]求
next[i++] = ++cn;//i为cn+1
} else if (cn > 0) {//cn还可以往前跳
cn = next[cn];
} else {
next[i++] = 0;//next[i]为0
}
}
}
int KMP(const string& text, const string& pattern) {
int next[pattern.size()];
getNext(pattern, next);
int i = 0/*遍历text*/, j = 0/*遍历pattern*/;
//保持i的位置不变,通过调节j的位置来匹配字符,直到i匹配完毕,匹配i+1
while (i < text.size() && j < pattern.size()) {
if (text[i] == pattern[j]) {//匹配上了,对比下一个
i++;
j++;
} else if (j != 0) {//j还可以往回跳
j = next[j];
} else {//j跳不动了。i匹配不上,匹配i+1
i++;
}
}
//是否是根据j跳出的边界
//j跳出while表示已经配出相同的了,才会导致j++到出界
//不是因为j跳出的while,说明没有找到
return j == pattern.size() ? i - j : -1;//i-j对应找到的下标
}
对第六行代码 if (s.size() == 1) return;
,没有这一句,当字符串s
只有一个字符时,next
数组应该直接返回next[1] = {-1}
,如果不跳出,则返回next[2] = {-1, 0};
所以会造成dynamic-stack-buffer-overflow
。
本地编译器来说,不加这一句也能通过,应该是给优化掉了。
LeetCode
没有优化掉,不过还是应该填上这一句,保证代码的稳定。
28. 实现 strStr()
题目链接:28. 实现 strStr()
KMP代码同上
class Solution {
public:
void getNext(const string &s, int* next) {
next[0] = -1;
if (s.size() == 1) return;
next[1] = 0;
int i = 2;
int cn = 0;
while (i < s.size()) {
if (s[i - 1] == s[cn]) {
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
}
int strStr(string haystack, string needle) {
int next[needle.size()];
getNext(needle, next);
int i = 0;
int j = 0;
while (i < haystack.size() && j < needle.size()) {
if (haystack[i] == needle[j]) {
i++;
j++;
} else if (j > 0) {
j = next[j];
} else {
i++;
}
}
return j == needle.size() ? i - j : -1;
}
};
459.重复的子字符串
题目链接: 459.重复的子字符串
移动匹配
字符串s
中有重复,s+s
也会包含s
。erase()
时间复杂度为
O
(
n
)
O(n)
O(n)
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string ss = s + s;
ss.erase(ss.begin());
ss.erase(ss.end() - 1);
return ss.find(s) != string::npos ? true : false;
}
};
KMP
用到了next
数组,根据next
数组的构成原则可得,重复的子字符串 = 字符串 - 最长相等前后缀。如果没有最长相等前后缀,直接返回false
。
class Solution {
public:
void getNext(const string& s, int* next) {
next[0] = -1;
if (s.size() == 1) return;
next[1] = 0;
int i = 2;
int cn = 0;
while (i < s.size()) {
if (s[i - 1] == s[cn]) {
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
}
bool repeatedSubstringPattern(string s) {
int len = s.size();
int next[len + 1];//多一位是因为next数组的构造不同,需要整个字符串的next数组
getNext(s + 'A'/*只要加一个不满足s要求的字符都行*/, next);
//比如字符串是"aba",变成"abaA"
return (next[len] != 0/*等于0,直接跳出为false*/ && len % (len - next[len]) == 0) ?
true : false;
}
};