问题的提出
假设需要在文本串haystack中查找是否出现过了一个模式串needle,一般的编程逻辑是直接遍历文本串,与模式串对比(暴力求解)。参考力扣题目:https://leetcode.cn/problems/implement-strstr/
int my_strStr(string haystack, string needle) {
//模式串为空,直接返回
if (needle.size() == 0) {
return 0;
}
//遍历文本串,不需要遍历到最后,文本串长度小于模式串长度就可以停止遍历
for (int i = 0; i <= (int)haystack.size() - (int)needle.size(); i++) {
//每次都从模式串的第一个字符开始对比
if (haystack[i] == needle[0]) {
int index = i;
bool flag = true;
//逐个字符对比
for (int j = 0; j < needle.size(); j++) {
if (haystack[index++] != needle[j]) {
flag = false;
break;
}
}
//只需要求模式串在文本串中第一次出现的位置
if (flag) {
return i;
}
}
}
return -1;
}
这种暴力遍历是费时的。比如,假设在第i次对比中,前m个字符是匹配的,即P[0] ~ P[m-1] = D[i] ~ D[i+m-1],但在P[m]处发生了不匹配。暴力解法在第i+1次会从P[0]开始重新对比。
试想,如果已知P[0] ~ P[m-2] != P[1] ~ P[m-1],则第i+1次对比一定是没有意义的。(文本串中高亮部分与模式串中的是相等的)
同理,若P[0] ~ P[m-3] != P[2] ~ P[m-1],则第i+2次对比也一定会失败。
直到发现,P[0] ~ P[k-1] = P[m-k] ~ P[m-1],那么这一次对比才有意义,并且可以直接从P[k]处开始对比。
KMP算法思想
记P[0] ~ P[k-1] 为字符串的前缀,P[m-k] ~ P[m-1]为字符串的后缀,k为最长相等前后缀。
通过上面的分析可以发现,如果第i次对比中仅在P[m]处发生了不匹配,而且已知对于字符串P[0] ~ P[m-1],最长相等前后缀长度为k。则可以直接从P[k]处开始匹配。
所以,现在需要提前计算好模式串中每个位置上对应的最长相等前后缀长度,将其存在表中,即为next表。
next表的求取
如何求模式串中P[0] ~ P[m]对应的最长相等前后缀呢?显然可以暴力解,先判断P[0] ~ P[m-1] 与 P[1] ~ P[m]是否相等,若不等,再判断P[0] ~ P[m-2] 与 P[2] ~ P[m],不断缩小前后缀的长度,直到判断出P[0] ~ P[k] = P[m-k] ~ P[m],则最长相等前后缀长度为k+1,记录在表中next[m] = k+1。然后再求next[m+1]。
这种方法求next表是暴力的。在求next[m]时,如果已知m-1处的最长相等前后缀是k,则可以直接比较P[k]与P[m]。因为在此之前肯定不存在相等了,如果存在,那么m-1处的最长相等前后缀就会大于k。(这里和之前文本串与模式串匹配的思想是一样的)
判断P[k]与P[m]相等,则next[m] = k+1;
判断P[k]与P[m]不相等,若k-1处的最长相等前后缀为next[k-1] = j,则应该再去判断P[m]与P[j]…;
若相等,next[m] = j+1; 若不等,应该根据next表不断回调,直到判断P[m]与P[0]。
所以求next表的代码为:
vector<int> getNextArr(string pattern) {
//构建next数组
vector<int> next(pattern.size(), 0);
//通过指针i遍历数组,求出每个位置上对应的next[i];(next[0]=0)
for (int i = 1; i < pattern.size(); i++) {
//定义n为i-1处的最大相等前后缀
int n = next[i - 1];
//当 pattern[i] != pattern[n]时,通过next表回退,并重新计算此时的最大相等前后缀
while (n > 0 && pattern[i] != pattern[n]) {
n = next[n - 1];
}
//直到相等时,next[i]为此时的最长相等前后缀加1
if (pattern[i] == pattern[n]) {
next[i] = n + 1;
}
//若始终不相等
else {
next[i] = 0;
}
}
return next;
}
KMP算法
求出next表后,就可以用它去快速的匹配文本串中的模式串啦。
int KMP_strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
vector<int> next = this->getNextArr(needle);
int n = 0;
for (int i = 0; i < haystack.size(); i++) {
//对比haystack[i]与needle[n]
//相等,n++;
//不相等,按next[n-1]回退,直到n=0;
//不相等一直回退
while (n > 0 && haystack[i] != needle[n]) {
n = next[n - 1];
}
//相等
if (haystack[i] == needle[n]) {
n++;
}
//n已经指向needle末尾
if (n == needle.size()) {
return i - n + 1;
}
}
return -1;
}
参考文章:https://programmercarl.com/0028.%E5%AE%9E%E7%8E%B0strStr.html