简而言之
用途:
kmp算法用于在【长串】中找【模式串】,并返回【配准的位置】,速度上优于暴力的算法。
暴力法简述:
长串A,短串B,从头(i = j = 0)开始逐个(++)比较,如果A[i] != B[j],j退回0,i退回本次匹配 i 初始位置,并i++,如此往复。如果在脑中模拟这一过程,好比【短串在长串中
滑动(向右),每滑一步,把短串中的内容从头到尾刷一遍<最坏情况>】,因此时间复杂度为 O(M * N)。
kmp 法简述:
1.我看到的所有讲kmp的blog,都提到kmp 的核心,在于【不回溯长串】,意思是说kmp进行匹配时,i是没有上面暴力法提到的【i退回本次匹配 i 初始位置,并i++】这一步,只增不减。这一下就使得时间复杂度降到了线性,O(M)。
2.如何做到这点,灵魂在于使用了【next数组】。而所谓next数组,是一个关注并记录【前缀后缀】位置信息的数组。用下图来解释,图中S代表长串,P代表短串。
ps:该图来自july博客。
想一个问题,如果已知
(1)P0 P1 ... Pk-1 == Pj-k Pj-k+1 ... Pj-1;
(2)Pj-k Pj-k+1 ... Pj-1 == Si-k Si-k+1 ... Si-1 且 Pj != Si
是否意味着两件事:
1、匹配位置不是Si-k,得继续去找配准位置;
2、已经确知【P0 P1 ... Pk-1 == Si-k Si-k+1 ... Si-1】,不需要再去比较了,只需要看 Pk 是否等于 Si就可以了。省掉了多次比较。脑中模拟这一次匹配,感觉就像【长串不动,短串很自信地向右狠狠滑了一下,并且从此以后从S0到Si-1都不需要再看了】。
那么,这个信心是谁给它的呢?答案就是上面提到的【next数组】。
也说过了,next数组里存放的信息,关注【前缀后缀】。那么什么是前缀后缀,又关注了些什么呢?
1)简而言之,一个字符串abcdefghijklmn,对于d来说,所谓前缀,即从头开始到d为止的字串:abc;所谓后缀,即从尾倒着往前数直到前缀长度为止的字串:lmn
2)对于n后的下一个字符x,next[x] == d
3)最后要注意到图中所说的【最长】二字
计算这个next数组的时间复杂度同样是线性的,O(N)。
故总的kmp时间复杂度为 O(M+N)。
附个完整代码如下:
#include <iostream> using namespace std; void get_next(char *a, int *next) { int len = strlen(a); int i = 0, j = -1; next[0] = -1; while(i < len - 1) { if(j == -1 || a[i] == a[j]) { ++i; ++j; if(a[i] != a[j]) //优化 next[i] = j; else next[i] = next[j]; } else { j = next[j]; } } } int kmp(char *src, char *tag) { int next[255]; get_next(tag, next); int lens = strlen(src), lent = strlen(tag); int i = 0, j = 0; while(i < lens && j < lent) { if(src[i] == tag[j] || j == -1) { ++i; ++j; } else { j = next[j]; } } if(j == lent) return i - j; else return -1; } int main(void) { char src[] = "DABCDABDE"; char tag[] = "ABD"; cout << kmp(src, tag) << endl; return 0; }