KMP算法
next数组构建
定义: 对于字符串 p p p, n e x t [ i ] next[i] next[i]表示 p p p在 [ 0 , i ] [0, i] [0,i]上的子串的最长相同前后缀长度
暴力求解: 很明显,可以在 [ 0 , i ) [0, i) [0,i)上枚举长度来构建 n e x t next next,此时算法复杂度为 O ( m 2 ) O(m^2) O(m2), m m m为 p p p的长度
优化求解: 见到最长两字容易想到使用递推也就是动态规划去求解,此时状态就是next数组的定义;求解 n e x t [ i ] next[i] next[i]时已知字串 [ 0 , i − 1 ] [0, i-1] [0,i−1]的最长相同前后缀长度 n e x t [ i − 1 ] = j next[i-1]=j next[i−1]=j,也就是说该子串对应前缀为子串 [ 0 , j ) [0, j) [0,j);此时出现两种情况:
(1) p [ j ] = p [ i ] p[j]=p[i] p[j]=p[i],有 n e x t [ i ] = n e x t [ i − 1 ] + 1 ; next[i]=next[i-1]+1; next[i]=next[i−1]+1;
(2) p [ j ] ≠ p [ i ] p[j] \neq p[i] p[j]=p[i],此时需要不断缩短子串 [ 0 , j ) [0, j) [0,j),找到子串 [ 0 , i − 1 ] [0, i-1] [0,i−1]上的次长相同前后缀长度,但我们只知道最长相同前后缀长度 n e x t [ i − 1 ] next[i-1] next[i−1];但不难由 n e x t [ i − 1 ] = j next[i-1]=j next[i−1]=j得子串 [ 0 , j ) [0, j) [0,j)等于子串 [ i − j , i − 1 ] [i-j, i-1] [i−j,i−1],而子串 [ 0 , i − 1 ] [0, i-1] [0,i−1]的次长相同前后缀的前缀必定落在子串 [ 0 , j ) [0, j) [0,j)上、后缀必定落在子串 [ i − j , i − 1 ] [i-j, i-1] [i−j,i−1]上,由于这两个串相等,求解子串 [ 0 , i − 1 ] [0, i-1] [0,i−1]次长相同前后缀问题可转化为求解子串 [ 0 , j ) [0, j) [0,j)的最长相同前后缀,也就是 n e x t [ j − 1 ] next[j-1] next[j−1];不断递归 j = n e x t [ j − 1 ] j=next[j-1] j=next[j−1]直至 p [ j ] = = p [ i ] p[j]==p[i] p[j]==p[i]或 j = = 0 j==0 j==0,要么没找到相同字符,此时 n e x t [ i ] = 0 next[i]=0 next[i]=0,要么找到了,此时 n e x t [ i ] = j + 1 next[i]=j+1 next[i]=j+1;
此时根据均摊分析可知复杂度为 O ( m ) O(m) O(m)
字符串匹配
首先想到的还是暴力求解: 每次匹配失败时指针 i i i 在主串 S S S 上回到初始位置并向后移动一格,指针 j j j 在模式串 P P P 上回到初始位置
由上图可知这种移动方式是非常耗时的,因为字符 c c c 在前面几个字符 a b ab ab 中并没有出现,完全可以直接将模式串 P P P 相对于主串 S S S 移动到指针 i i i 指向的字符 a a a 处。
优化求解: 对于指针 j j j , n e x t [ j − 1 ] next[j-1] next[j−1] 表示子串 [ 0 , j − 1 ] [0, j-1] [0,j−1] 的最长相同前后缀;
如果 n e x t [ j − 1 ] = 0 next[j-1]=0 next[j−1]=0,意味着字符 P [ j − 1 ] P[j-1] P[j−1] 不会出现在 P [ 0 , j − 2 ] P[0, j-2] P[0,j−2]上,而且因为 P P P和 S S S 已部分匹配,有 S [ i − j , i − 1 ] = P [ 0 , j − 1 ] S[i-j, i-1]=P[0, j-1] S[i−j,i−1]=P[0,j−1] ,因此有 ∀ k ∈ [ 0 , j − 2 ] , P [ k ] ≠ S [ i − 1 ] \forall k \in [0, j-2], P[k] \neq S[i-1] ∀k∈[0,j−2],P[k]=S[i−1],即模式串P相对于主串 S S S 移动 [ 1 , j − 2 ] [1, j-2] [1,j−2] 位,都不会把 S [ i − 1 ] S[i-1] S[i−1] 匹配上,这时候就可以把 S [ i − 1 ] S[i-1] S[i−1] 放弃掉,此时对应的就是上面那种情况。
对于一般的 n e x t [ j − 1 ] ≠ 0 next[j-1] \neq 0 next[j−1]=0 ,如下图,此时 n e x t [ j − 1 ] = 2 next[j-1]=2 next[j−1]=2 ,同样的也有 S [ i − j , i − 1 ] = P [ 0 , j − 1 ] S[i-j, i-1]=P[0, j-1] S[i−j,i−1]=P[0,j−1] ,因此有 P [ j − 2 , j − 1 ] = S [ i − 2 , i − 1 ] = P [ 0 , 1 ] P[j-2, j-1] = S[i-2, i-1] = P[0, 1] P[j−2,j−1]=S[i−2,i−1]=P[0,1] ,也就是说 P [ 0 ] P[0] P[0] 和 P [ 1 ] P[1] P[1] 是能够匹配上 S [ i − 2 ] S[i-2] S[i−2] 和 S [ i − 1 ] S[i-1] S[i−1] 的,可以将 P P P 相对于 S S S 移动到该位置继续匹配 S [ i ] S[i] S[i] 和 P [ j ] P[j] P[j] ,如下图①和②;
但此时 S [ i ] S[i] S[i]和 P [ j ] P[j] P[j]依旧没有匹配上,且有 n e x t [ j − 1 ] = 1 next[j-1] = 1 next[j−1]=1 ,继续移动,如上图②和③, S [ i ] S[i] S[i] 和 P [ j ] P[j] P[j] 终于匹配上了,且随着 i i i 和 j j j 同时继续往下走,模式串 P P P 终于匹配上主串 S S S ;
这个过程中存在一个递归关系 j = n e x t [ j − 1 ] j=next[j-1] j=next[j−1],且递归终止条件为 j = = 0 ∣ ∣ S [ i ] = = P [ j ] j==0 || S[i] == P[j] j==0∣∣S[i]==P[j] ,这里的递归公式与动态求解 n e x t next next 数组一致,这个过程中 j j j 越来越小也就是说相对于初始位置、 P P P 相对于 S S S 移动的量越来越大,能够排除暴力求解中的一些情况的同时不漏掉所有可能匹配上的情况。
CPP代码
int strStr(string haystack, string needle) {
int m = needle.size(), n = haystack.size();
if (m == 0) return 0;
if (n < m) return -1;
vector<int> next(m);
for (int i = 1, j = 0; i < m; ++i) {
while (j > 0 && needle[i] != needle[j]) j = next[j-1];
if (needle[i] == needle[j]) ++j;
next[i] = j;
}
for(int i = 0, j = 0; i < n; ++i) {
while (j>0 && haystack[i] != needle[j]) j = next[j-1];
if (haystack[i] == needle[j]) {
++j;
if (j == m) return i-m+1;
}
}
return -1;
}