KMP算法学习笔记
KMP算法本科的时候简单学习过,可是好久不用,回头看才发现它还认识我,我却已不认得我了,惭愧!重新拿起课本啃了下,看了半天也回过神来,暂且做个笔记简单记下。
首先为描述方便,先定义两个串:其中数组 des[N] 表示目标串 ,pat[M] 表示模式串, 数组下标均从 1 开始,des[i...j] 表示从i到 j 的子串,pat[i...j] 类似。
然后说下算法本身。对于原始算法而言。如果 des[i...i+j] 和 pat[1...j] 进行比对。设前面 j - 1 次比对都相同但第 j 次比对不同时,目标串回退 j - 1 位,接下来再继续拿 des[i+1] 和 pat[1] 进行重新比对。如此往复,直到比对完毕。但 KMP算法却几乎不需要像这样回退。而是利用之前 des[i...i+j] 和 pat[1...j] 的前 j-1 次正确匹配的数据。找到合适的回退位置。具体回到哪个位置,接下来进行讨论。
1)生成next数组,先上代码(C)
void get_next(String T ,int[] next){ //求模式串中T的next函数值并将其存入next数组 int i,j; i = 1; j = 0; next[1] = 0; while(i < T[0]) if(j == 0 || T[i] == t[j] ){ ++ i; ++ j; next[i] = j ; } else{ j = next[j]; } }
首先必须清楚的是next数组与目标串没有关系,从代码中也可以看出,其目的是对pat[]进行预处理,然后将与处理结果保存在next[]数组中。next[i]中存放的是 pat[1...i] 中前缀和后缀相同的最大长度,也就是 pat[1...next[i]] 既是 pat[1...i] 的前缀也是他的后缀。而且是满足条件最长的。
举个例子理解前面这句话。假设模式串 abaabac 。注意这里的前缀和后缀都是子串,不含其本身。比如abc , a 和 ab 都是允许的前缀,但在 KMP中abc则是不能算作前缀的。而 next[1] 对应于子串 a ,不包含他本身的前缀和后缀都不存在,所以 next[1] = 0,对于 next[2] 对应的子串是 ab ,不包含他本身的前缀有 a,但 a是不合要求的,因为没有一个后缀为 a。我们前面说了要有相同的前缀和后缀。所以 next[2] = 0。next[3] 对应子串aba 。前缀有 a 和 ab 。但只有 a 是符合要求的。因为 a 既是aba的前缀也是 aba的后缀。因此 next[3] = 1; next[4] 对应于 abaa。前缀有a,ab,aba,但只有前缀 a 是符合要求的。因为另外两个都不是他的后缀。next[5] 对应于abaab。前缀有四个。其中ab 是唯一符合要求的前缀,没有之一。next[6] 对应于 abaaba。总共五个前缀,其中 a 和 aba都既是前缀也是后缀。但 aba 的长度更长,因此 next[6] = 3。abaabac一种 6 个前缀但都不合要求,因此 next[7] = 0 。至此应该知道对 next 有了一定的了解。
上面说明了next[]中存储的内容,但具体怎么求还有待解决。严蔚敏的教材中是用递推的方式求解next[],也即是后面的求解要依赖于前面的求解。
还是举abaabc 为例。next[1] 不用说,肯定为 0 , next[1] = 0。对于i > 1,next[i] 要拿 next[i - 1] 之后的字符和第 i 个字符进行比对。也就是 pat[next[i - 1] + 1] 和 pat[i] 进行比对。为什么要拿第 next[i - 1] + 1 个字符和 pat[i] 进行比较呢?其实也好理解,因为前面说过next[i - 1] 是符合 pat[1...i-1] 的最长前缀,同时他也是符合要求的最长后缀,如果pat[next[i-1]+1]与pat[i]相等,则意味着pat[1...next[i-1]+1]为pat[1...i]的最长相等前缀和后缀,即next[i] = next[i-1] + 1。
这是一种理想情况,但如果pat[next[i-1]+1]与pat[i]不相等呢?这时就是从大到小遍历一遍 pat[1...i-1] 的所有既是前缀又是后缀的子串,并找到一个最长的符合要求的。这里符合要求是指:假设有 pat[1...i-1] 的既是前缀也是后缀的子串为 pat[1...k] , 如果 pat[k+1] == pat[i] 就是符合要求的。我们的目的就是要在所有这样符合要求的子串中找一个最长的。
我们知道 next[i-1] 是 pat[1...i-1] 的最长的既是前缀又是后缀的子串,次长的则是 next[next[i-1]],再次长则是 next[next[next[i-1]]] 。依次往复。
为什么呢?这里我们进行解释。首先 next[i-1] 是 pat[1...i-1] 最长的既是前缀又是后缀的子串这个毫无疑问的,因为 next[] 就是这么定义的。
但为什么次长就是 next[next[i-1]] 呢?因为首先 pat[1...i-1] 既是前缀又是后缀的次长子串的长度肯定小于 next[i-1] 。这里我们暂记 next[i-1] = t 。 pat[1...i-1] 的既是前缀又是后缀的子串肯定是 pat[1...t] 的前缀,同时因为 pat[1...i-1] 既是前缀又是后缀的次长子串是 pat[1...i-1] 的后缀,并且长度小于 t ,所以他肯定也是 pat[1...t] 的后缀。因为 pat[1...t] 是 pat[1...i-1] 的最长后缀 (这是因为 pat[1...t] 是 pat[1...i-1] 既是前缀又是后缀的最长子串)。所以 pat[1...i-1] 的次长子串既是 pat[1...t] 的前缀也是 pat[1...t] 的后缀。而 next[next[i-1]] 是满足要求的最长的。所以 next[next[i-1]] 就是 pat[1...i-1] 的次长子串。后面的依次类推。因此我们要从大到小的遍历 pat[1...i-1] 的所有既是前缀又是后缀的子串。只需要按照上面的策略循环一遍。如 abaabac 的例子 next[4] 求 next[4] 首先拿 pat[next[3] + 1] 和 pat[4] 比对结果一个为 'b' 一个为 'a'。然后拿 pat[next[1] + 1] 和 pat[4] 比对结果都为'a'。next[4] = next[1] + 1。
2)既然知道了怎么推导next[]数组,下面开始匹配
KMP强大就在于她不需要回退 des 的 i 指针。但如果是原始算法是要回退 j - 1 个字符的。此时只需要对 j 进行回退。j = next[j ] 。然后拿 des[i] 和 pat[j] 继续比对。为什么这样就行了呢?很多讲解 KMP的介绍都没有阐明这点。都是告诉大家就是要这样移动。导致大家不懂的还是不懂。第一个是不懂为什么不移动 i 。第二个是不懂为什么回退 j 到 next[j ]。
代码如下:
int KMP_index(String S,String T,int pos){ int i = pos; j = 1; while(i < S[0]) && j < T[0]){ if(j== 0 || s[i] = s[j]){ ++i;++j;} else{ j =next[j] } if(j > T[0]) return i - T[0]; else return 0; }
浙公网安备 33010602011771号