今天看到同窗在复习数据结构书上的KMP算法,突然发觉本身又把KMP算法忘掉了,之前就已经忘过一次,看样子仍是没有真正的掌握它,这回学聪明点,再次搞明白后记录下来。c++
KMP算法
是字符串匹配算法的一种改进版,通常的字符串匹配算法是:从主串(目标字符串)
和模式串(待匹配字符串)
的第一个字符开始比较,若是相等则继续匹配下一个字符, 若是不相等则从主串的下一个字符开始匹配,直到模式串被匹配完,则匹配成功
,或主串被匹配完且模式串未匹配完,则匹配失败
。匹配过程入下图:算法
这种实现方式是最简单的, 但也是低效
的,由于第三次匹配结束后的第四次和第五次是没有必要的
。数组
第三次匹配在j = 0(a)
和i = 2(a)
处开始,在j = 4(c)
和i = 6(b)
处失败,这意味着模式串和主串中:j = 0(a)
和i = 2(a)
、j = 1(b)
和i = 3(b)
、j = 2(c)
和i = 4(c)
、j = 3(a)
和i = 5(a)
这四个字符相互匹配。 数据结构
分析模式串的前3个字符:模式串的第一个字符j = 0是a
,j = 1(b)
、j = 2(c)
这两个字符和j = 0(a)
不一样,所以以这两个字符开头的匹配一定失败,在第三次匹配中,主串中i = 3(b)
、i = 4(c)
和模式串j = 1(b)
、j = 2(c)
相互匹配,所以匹配失败后,能够直接跳过主串中i = 3(b)
、i = 4(c)
这两个字符的匹配。 测试
继续分析模式串的j = 3(a)
和j = 4(c)
这两个字符,若是模式串匹配到j = 4(c)
这个字符才失败的话,由于j = 4(c)
的前一个字符j = 3(a)
和第一个字符j = 0(a)
是相同的,结合上一个分析得知:优化
1):下一次匹配中主串已经跳过了和
j = 3(a)
前两个相互匹配的字符i = 3(b)
、i = 4(c)
,将从i = 5(a)
开始匹配。
2):j = 3(a)
和i = 5(a)
相互匹配。url
所以下一次匹配认为j = 3(a)
和i = 5(a)
已经匹配过了,而j = 3(a)和j = 0(a)是相同的
,因此下次匹配能够从j = 2(b)
和i = 6(b)
开始,这样的话也跳过了j = 0(a)和i = 5(a)
这个字符的匹配。spa
同理可得第二次匹配也是不必的。code
利用KMP算法匹配的过程以下图:字符串
KMP算法的改进之处在于:可以知道在匹配失败后,有多少字符是不须要进行匹配能够直接跳过的
,匹配失败后,下一次匹配从什么地方开始可以有效的减小没必要要的匹配过程。
由上面的分析能够发现,KMP算法的核心在于对模式串自己的分析,其分析结果能提供在j = n
位置匹配失败时,从j = 0
到j = n - 1
这个子串中前缀和后缀的最长公共匹配的字符数,这样说可能比较难以理解,看下图:
在获得子串前缀和后缀的最长公共匹配字符数l
后,之后在i = x
,j = n
处匹配失败时,能够直接从i = x
,j = l
处继续匹配(证实过程参考:严蔚敏的《数据结构》4.3章
),这样问题就很明显了,咱们要求出n和l对应的值
,其中n
是模式串字符数组的下标,l
的有序集合一般称之为next数组
,前面两个模式串的next数组
和下标n
的对应以下:
有了这个next数组
,那么在匹配的过程当中咱们就能在j = n
处匹配失败后,根据next[n]
的值进行偏移,其中next[0]固定为-1
,表明在当前i
这个位置整个模式串和主串都没法匹配成功,要从下一个位置i = i + 1
及j = 0
处开始匹配,模式串2的匹配过程以下:
如今知道了next数组
的做用,也知道在有next数组
时的匹配过程,那么剩下的问题就是如何经过代码求出next数组
及匹配过程
了。
求
next数组
的过程能够认为是将模式串拆分红n个子串,分别对每一个子串求前缀和后缀的最长公共匹配字符数l
,这一点能够经过上图(最长公共匹配字符数)看出来(没有画出l=0
时的图解)看出来。
求next数组
的代码以下:
void get_next(string pattern, int next[]) { // !!!!!!!!!!由网友(评论第一条)指出该算法存在问题,已将有问题的代码注释并附上临时想到的算法代码。 // int i = 0; // i用来记录当前计算的next数组元素的下标, 同时也做为模式串自己被匹配到的位置的下标 // int j = 0; // j == -1 表明从在i的位置模式串没法匹配成功,从下一个位置开始匹配 // next[0] = -1; // next[0]固定为-1 // int p_len = pattern.length(); // while (++i < p_len) { // if (pattern[i] == pattern[j]) { // // j是用来记录当前模式串匹配到的位置的下标, 这就意味着当j = l时, // // 则在pattern[j]这个字符前面已经有l - 1个成功匹配, // // 即子串前缀和后缀的最长公共匹配字符数有l - 1个。 // next[i] = j++; // } else { // next[i] = j; // j = 0; // if (pattern[i] == pattern[j]) { // j++; // } // } // } int j = 0; next[0] = -1; int p_len = pattern.length(); int matched = 0; while (++j <= p_len) { int right = j - 1; int mid = floor(right / 2); int left = right % 2 == 0 ? mid - 1 : mid; int curLeft = left; int curRight = right; while (curLeft >= 0) { if (pattern[curLeft] == pattern[curRight]) { matched++; curLeft--; curRight--; } else { matched = 0; curLeft = --left; curRight = right; } } next[j] = matched; matched = 0; } }
根据next数组
求模式串在主串中的位置代码以下:
int search(string source, string pattern, int next[]) { int i = 0; int j = 0; int p_len = pattern.length(); int s_len = source.length(); while (j < p_len && i < s_len) { if (j == -1 || source[i] == pattern[j]) { i++; j++; } else { j = next[j]; } } if (j < pattern.length()) return -1; else return i - pattern.length(); }
测试代码以下:
int main() { string source = "ABCDABCEAAAABASABCDABCADABCDABCEAABCDABCEAAABASABCDABCAABLAKABCDABABCDABCEAAADSFDABCADABCDABCEAAABCDABCEAAABASABCDABCADABCDABCEAAABLAKABLAKK"; // string pattern = "abcaaabcab"; string pattern = "ABCDABCEAAABASABCDABCADABCDABCEAAABLAK"; int next[pattern.length()] = { NULL }; get_next(pattern, next); cout << "next数组: \t"; for (int i = 0; i < pattern.length(); i++) cout << next[i] << " "; cout << endl; int pos = search(source, pattern, next); if (-1 != pos) { cout << "匹配成功,模式串在主串中首次出现的位置是: 第" << pos + 1 << "位"; getchar(); return 0; } else { cout << "匹配失败"; } getchar(); return 0; }
执行结果:
next数组: -1 0 0 0 0 1 2 3 0 1 1 1 2 1 0 1 2 3 4 5 6 7 1 0 1 2 3 4 5 6 7 8 9 10 11 12 0 1 匹配成功,模式串在主串中首次出现的位置是: 第97位
再回过头去看模式串2的next数组
的图:
若是模式串和主串的匹配在j = 6(b)
处失败的话,根据j = next[6] = 1
得知下一次匹配从j = 1
处开始,j = 1处的字符和j = 6处的字符同为c
,所以此次匹配一定会失败。
一样的,模式串和主串的匹配在j = 7(c)
处或在j = 9(b)
处失败的话,根据next数组
偏移后下一次匹配也一定会失败。
考虑若是模式串是: aaaac,根据通常的KMP算法求出的next数组
及匹配过程以下:
显而易见,在第二次匹配失败后,第3、4、五次匹配都是没有意义的,j = next[3]、j = next[2]、j = next[1]、j = next[0]
这四处的字符都是a,在j = 3(a)
处匹配失败时,根据模式串自己就应该能够得出结论:能够跳过j = 2(a)、j = 1(a)、j = 0(a)
的匹配,直接从i = i + 1
、j = 0
处开始匹配,因此优化事后的next数组
应该是:
优化后的求next数组
的代码以下:
void get_next(string pattern, int next[]) { // !!!!!!!!!!由网友(评论第一条)指出该算法存在问题,更新后的代码在上方,新算法的优化代码暂未实现,可是优化思路是正确的。 // int i = 0; // i用来记录当前计算的next数组元素的下标, 同时也做为模式串自己被匹配到的位置的下标 // int j = 0; // j == -1 表明从在i的位置模式串没法匹配成功,从下一个位置开始匹配 // next[0] = -1; // next[0]固定为-1 // int p_len = pattern.length(); // while (++i < p_len) { // if (pattern[i] == pattern[j]) { // // j是用来记录当前模式串匹配到的位置的下标, 这就意味着当j = l时, // // 则在pattern[j]这个字符前面已经有l - 1个成功匹配, // // 即子串前缀和后缀的最长公共匹配字符数有l - 1个。 // next[i] = j++; // // // 当根据next[i]偏移后的字符与偏移前的字符向同时 // // 那么此次的偏移是没有意义的,由于匹配一定会失败 // // 因此能够一直往前偏移,直到 // // 1): 偏移前的字符和偏移后的字符不相同。 // // 2): next[i] == -1 // while (next[i] != -1 && pattern[i] == pattern[next[i]]) { // next[i] = next[next[i]]; // } // } else { // next[i] = j; // j = 0; // if (pattern[i] == pattern[j]) { // j++; // } // } // } }
但愿本文能对你有帮助, 若是有什么问题, 欢迎探讨。
严蔚敏的《数据结构》4.3章
kmp算法--百度百科