一、假如你已经知道一些大概,只用求next数组的话。
我们假设next[k]表示以k结尾的最长前后缀相等长度,
(这样的话,我们在和主串比对的时候,子串j只移动到 前j-1段的相等前后长度next[j-1]的下一个位置与主串i比对,即j = next[j-1])
那么我们怎么求解next数组呢?先看代码
vector<int> calNext(const string &sub) {
int n = sub.size(), k = 0;
vector<int> next(n, 0);
for(int i = 1; i < n; ++i) {
while(k > 0 && sub[i] != sub[k]) k = next[k-1];
if(sub[i] == sub[k]) ++k;
next[i] = k;
}
return next;
}
1、i = 0时前后缀最长k是等于0,即next[0] = 0.
2、如果i位置以前的最长前后缀长度k的下一位等于当前i上字符,最长k+1;
否则因为前缀的下一个不能相等, 则即从前缀中寻找前缀,再比较下一位,注意前缀是next[k-1].
举两个栗子:
1、
假设我们正在计算next[3] :
由于到2最长前后相等缀k = 1, 我们判别sub[k] == sub[3]?
不等,那么判断k=next[k-1] = 0, sub[k] == sub[3]?,则next[3] = ++k = 1;
2、
假设我们正在计算next[4]
由于到3最长前后相等缀k=1, 判断sub[k] == sub[4] ? 相等,则next[4] = ++k = 2;
二、假如需要一些原理。
我们知道在求解是否存在子串问题时,暴力算法复杂度达到了O(mn), 而KMP算法只用O(m+n)。
其原因最本质的一点就是当主串遍历到i和子串遍历到j时,如果两者不等,i不必回退到i-j的下一个位置。
而是根据子串最长前后缀长度k, 判断子串下一个位置k上的字符是否与主串i位置相同。
达到只用移动子串上的指针不用移动主串指针。
证明:(为啥主串可以不用回移)假设当前不匹配情况下(s t r[i] != sub[j]),主串与子串的匹配初始点一定是在i-j后面某个位置p了,那么主串p到i上的字符一定要和子串前面一段字符匹配,所以i可以不后移,但j要回移了。
这个后移的量,即是子串sub以j-1结尾段的最长的前后缀相等长度。由于长度k的下标表示下一个位置,则后面只用判断sub[k]上字符是否与主串str[j]相同。
三、主程序
int m = str.size(), n = sub.size();
if(n == 0) return 0;
vector<int> next = calNext(sub);
int j = 0;
for(int i = 0; i < m; ++i) { //其实下面一段代码计算和next数组基本一致。
while(j > 0 && str[i] != sub[j]) j = next[j-1];
if(str[i] == sub[j]) ++j;
if(j == n) return i-n+1;
}
return -1;
}