其实这是两年前我自己写的一篇总结,最近复习算法又给翻出来了,顺便发到博客上来。
从我个人的学经历来看,学习KMP算法最大的困难在于版本太多。如果你看完一个博客只是懵懂,想再看看另一个博客,那么就很有可能遇到另一个版本的KMP算法,会让你更加混乱,所以我觉得整理一下KMP算法的不同版本其实更加重要。
不同版本KMP算法的主要流程其实都是相似的,其不同之处在于预处理数组的定义。
第一种KMP算法的预处理数组叫做LPS数组(longest proper prefix which is also suffix),其中proper prefix是指不包含字符串本身的前缀(A proper prefix is prefix with whole string not allowed)。上述定义是从GeeksforGeeks中截取的,和它定义相同的参考书为《算法导论》。用中文解释的话,LPS[i]存储的是模式串前i+1个字符所组成的子串中,最大的相同前后缀。例如,字符串ababaca的LPS数组如下:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
前缀 | a | ab | aba | abab | ababa | ababac | ababaca |
LPS[i] | 0 | 0 | 1 | 2 | 3 | 0 | 1 |
第二种KMP算法的预处理数组叫做next数组,也是网上博客提到最多的一种。不过next数组又要分两种:一种是K、M、P这三个人在其论文中所定义的next数组,它默认字符串是从1开始存储的,和它定义相同的参考书为严蔚敏的《数据结构》(C版)。这种数组相当于是把LPS数组整体右挪一位,然后在第一位填上“-1”后又整体加1得到的:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
前缀 | a | ab | aba | abab | ababa | ababac | ababaca |
next[i] | 0 | 1 | 1 | 2 | 3 | 4 | 1 |
另一种next数组则默认字符串从0开始存储,许多网上的博客都用的是这种(毕竟我们一般不从1开始存储字符串)。这种数组是将LPS数组整体右挪一位,然后在第一位填上“-1”后得到的:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
前缀 | a | ab | aba | abab | ababa | ababac | ababaca |
next[i] | -1 | 0 | 0 | 1 | 2 | 3 | 0 |
针对这两种next数组,我有两点需要提醒:
1、LPS数组的最后一位,也就是LPS[m-1]实际上在KMP算法的主要流程中是派不上用场的(假设模式串长度为m),这也是为什么经过整体右移后得到的next数组依然可以用于KMP算法的原因。
2、如果有人问你一个字符串的next数组是什么,一般来说是要写第一种next数组。当然,你也可以先和人家声明你的字符串是怎么存储的,然后再写相对应的next数组。
第三种KMP算法的预处理数组叫做nextval数组,不过这种数组实际上不用于KMP算法,而是用于优化后的KMP算法。当字符串重复性很大的时候,使用nextval数组可进行预处理可以进一步降低时间复杂度。由于网上已有详细的手算next数组和nextval数组的讲解,我也就不再展开了,贴个链接好了:
https://www.cnblogs.com/alice123/articles/5701665.html
注:该博客中默认字符串从0开始存储。