六(三续):KMP 算法之总结篇(必懂 KMP)
第一部 分、KMP 算法初解
1、普通字符串匹配 BF 算法与 KMP 算法的时间复杂度比较
KMP 算法是一种线性时间复杂的字符串匹配算法,它是对 BF 算法(Brute-Force,最基 本的字符串匹配算法的)改进。对于给的原始串 S 和模式串 P,需要从字符串 S 中找到字 符串 P 出现的位置的索引。
BF 算法的时间复杂度 O(strlen(S) * strlen(T)),空间复杂度 O(1)。
KMP 算法的时间复杂度 O(strlen(S) + strlen(T)),空间复杂度 O(strlen(T))。
2、BF 算法与 KMP 算法的区别
假设现在 S 串匹配到 i 位置,T 串匹配到 j 位置。那么总的来说,两种算法的主要区别在 于失配的情况下,对 的值做的处理:
BF 算法中,如果当前字符匹配成功,即 s[i+j] == T[j],令 j++,继续匹配下一个字符;如 果失配,即 S[i + j] != T[j],需要让 i++,并且 j= 0,即每次匹配失败的情况下,模式串 T 相对 于原始串 S 向右移动了一位。
而 KMP 算法中,如果当前字符匹配成功,即 S[i]==T[j],令 i++,j++,继续匹配下一个 字符;如果匹配失败,即 S[i] != T[j],需要保持 i 不变,并且让 j = next[j],这里 next[j] <=j -1,
即模式串 T 相对于原始串 S 向右移动了至少 1 位(移动的实际位数 j - next[j] >=1),
如果下次匹配是基于 T 向右移动一位,那么 i 之前的部分(即 S[i-j+1 ~ i-1]),和 j=next[j]
之前的部分(即 T[0 ~ j-2])仍然相等。显然,相对于 BF 算法来说,KMP 移动更多的位数,
起到了一个加速的作用! (失配的特殊情形,令 j=next[j]导致 j==0 的时候,需要将 i ++,
否则此时没有移动模式串)。
3、BF 算法为什么要回溯
首先说一下为什么 BF 算法要回溯。如下两字符串匹配(恰如上面所述:BF 算法中,如果当 前字符匹配成功,即 s[i+j] == T[j],令 j++,继续匹配下一个字符):
i+j(j 随 T 中的 j++变,而动)
S:aaaacefghij
j++
T:aaac
如果不回溯的话就是从下一位开始比起:
aaaacefghij
aaac
看到上面红颜色的没,如果不回溯的话,那么从 a 的下一位 c 比起。然而下述这种情况就 漏了(正确的做法当然是要回溯:如果失配,即 S[i + j] != T[j],需要让 i++,并且 j= 0):
aaaacefghij
aaac
所以,BF 算法要回溯,其代码如下:
1. int Index(SString S, SString T, int pos) { 2. //返回 T 在 S 中第 pos 个字符之后的位置 3. i=pos; j=1;k=0;
4. while ( i< = S[0] && j< = T[0] ) {
5. if (S[i+k] = = T[j] ) {++k; ++j;} //继续比较后续字符 6. else {i=i+1; j=1; k=0;} //指针回溯到 下一首位,重新开始
7. }
8. if(j>T[0]) return i; //子串结束,说明匹配成功 9. else return 0;
10. }//Index
不过,也有特殊情况可以不回溯,如下:
abcdefghij(主串) abcdefg(模式串)
即(模式串)没有相同的才不需要回溯。
4、KMP 算法思想
普通的字符串匹配算法必须要回溯。但回溯就影响了效率,回溯是由 T 串本身的性质决 定的,是因为 T 串本身有前后'部分匹配'的性质。像上面所说如果主串为 abcdef 这样的,大 没有回溯的必要。
改进的地方也就是这里,我们从 T 串本身出发,事先就找准了 T 自身前后部分匹配的位 置,那就可以改进算法。
如果不用回溯,那模式串下一个位置从哪里开始呢?
还是上面那个例子,T(模式串)为 ababc,如果 c 失配,那就可以往前移到 aba 最后一个 a 的位置,像这样:
...ababd...
ababc
->ababc
这样 i 不用回溯,j 跳到前 2 个位置,继续匹配的过程,这就是 KMP 算法所在。这个当 T[j]
失配后,j 应该往前跳的值就是 j 的 next 值,它是由 T 串本身固有决定的,与 S 串(主串) 无关。
5、next 数组的含义
重点来了。下面解释一下 next 数组的含义,这个也是 KMP 算法中比较不好理解的一点。
令原始串为: S[i],其中 0<=i<=n;模式串为: T[j],其中 0<=j<=m。
假设目前匹配到如下位置
S0,S1,S2,...,Si-j,Si-j+1...,Si-1, Si, Si+1,....,Sn
T0,T1,...,Tj-1, Tj, ...
S 和 T 的绿色部分匹配成功,恰好到 Si 和 Tj 的时候失配,如果要保持 i 不变,同时达到 让模式串 T 相对于原始串 S 右移的话,可以更新 j 的值,让 Si 和新的 Tj 进行匹配,假设新 的 j 用 next[j]表示,即让 Si 和 next[j]匹配,显然新的 j 值要小于之前的 j 值,模式串才会是 右移的效果,也就是说应该有 next[j] <= j -1。那新的 j 值也就是 next[j]应该是多少呢?我们 观察如下的匹配:
1)如果模式串右移 1 位(从简单的思考起,移动一位会怎么样),即 next[j] = j - 1, 即 让蓝色的 Si 和 Tj-1 匹配(注:省略号为未匹配部分)
S0,S1,S2,...,Si-j,Si-j+1...,Si-1, Si, Si+1,....,Sn
T0,T1,...,Tj-1, Tj, ... (T 的划线部分和 S 划线部分相等
【1】)
T0,T1,...Tj-2,Tj-1, ... (移动后的 T 的划线部分和 S 的划 线部分相等【2】)
根据【1】【2】可以知道当 next[j] =j -1,即模式串右移一位的时候,有 T[0 ~ j-2] == T[1
~ j-1],而这两部分恰好是字符串 T[0 ~j-1]的前缀和后缀,也就是说 next[j]的值取决于模式 串 T 中 j 前面部分的前缀和后缀相等部分的长度(好好揣摩这两个关键字概念:前缀、后缀,
或者再想想,我的上一篇文章,从 Trie 树谈到后缀树中,后缀树的概念)。
2)如果模式串右移 2 位,即 next[j] = j - 2, 即让蓝色的 Si 和 Tj-2 匹配
S0,S1,...,Si-j,Si-j+1,Si-j+2...,Si-1, Si, Si+1,....,Sn
T0,T1,T2,...,Tj-1, Tj, ...(T 的划线部分和 S 划线部分 相等【3】)
T0,T1,...,Tj-3,Tj-2,...(移动后的 T 的划线部分和 S 的划线部分相等【4】)
同样根据【3】【4】可以知道当 next[j] =j -2,即模式串右移两位的时候,有 T[0 ~ j-3]
== T[2 ~ j-1]。而这两部分也恰好是字符串 T[0 ~j-1]的前缀和后缀,也就是说 next[j]的值取 决于模式串 T 中 j 前面部分的前缀和后缀相等部分的长度。
3)依次类推,可以得到如下结论:当发生失配的情况下,j 的新值 next[j]取决于模式串 中 T[0 ~ j-1]中前缀和后缀相等部分的长度, 并且 next[j]恰好等于这个最大长度。
为此,请再允许我引用上文中的一段原文:
“
KMP 算法中,如果当前字符匹配成功,即 S[i]==T[j],令 i++,j++,继续匹配下一个字符;如果匹配失败,即 S[i] != T[j],需要保持 i 不变,并且让 j = next[j],这里 next[j] <=j -1,即模式串 T 相对于原始串 S 向右移动了至少 1 位(移动的实际位数 j - next[j] >=1),同时移动之后,i 之前的部分(即 S[i-j+1 ~ i-1]),和 j=next[j]之前的部分(即 T[0 ~ j-2])
仍然相等。显然,相对于 BF 算法来说,KMP 移动更多的位数,起到了一个加速的作用! (失 配的特殊情形,令 j=next[j]导致 j==0 的时候,需要将 i ++,否则此时没有移动模式串)。”
于此,也就不难理解了我的关于 KMP 算法的第二篇文章之中:
“
当匹配到 S[i] != P[j]的 时候有 S[i-j…i-1] = P[0…j-1]. 如果下面用 j_next 去匹配,则有 P[0…j_next-1] =S[i-j_next…i-1] = P[j-j_next…j-1]。此过程如下图 3-1 所示。
当匹配到 S[i] != P[j]时,S[i-j…i-1] = P[0…j-1]:
S: 0 … i-j … i-1 i …
P: 0 … j-1 j …
如果下面用 j_next 去匹配,则有 P[0…j_next-1] = S[i-j_next…i-1] = P[j-j_next…j-1]。
所以在 P 中有如下匹配关系(获得这个匹配关系的意义是用来求 next 数组):
P: 0 … j-j_next .…j-1_ … P: 0 … .j_next-1 …
所以,根据上面两个步骤,推出下一匹配位置 j_next:
S: 0 … i-j … i-j_next … i-1 i … P: 0 … j_next-1 j_next …
图 3-1 求 j-next(最大的值)的三个步骤
下面,我们用变量 k 来代表求得的 j_next 的最大值,即 k 表示这 S[i]、P[j]不匹配时 P 中下一个用来匹配的位置,使得P[0…k-1] = P[j-k…j-1],而我们要尽量找到这个 k 的最大 值。
”。
根据上文的【1】与【2】的匹配情况,可得第二篇文章之中所谓的 k=1(如 aaaa 的形 式),根据上文的【3】与【4】的匹配情况,k=2(如 abab 的形式)。
再次总结下,如下图:
从上图中我们看到,当 S 移动到 i,P 到 j 的时候失配。这时候 i 不回朔,而只是将 P 向前移 动尽可能的距离,继续比较。
假设,P 向右移动一定距离后,第 k 个字符 P[k]和 S[i]进行比较。此时如上图,当 P[j]和 S[i]
失配后,i 不动,将 P 前移到 K,让 P[k]和 S[i]继续匹配。现在的关键是 K 的值是多少?
通过上图,我们发现,因为黄色部分表示已经匹配了的结果(因为是到了 S[i]和 P[j]的时候才
失配,所以 Si-j+1Si-j+2…Si-1= P1P2…Pj-1,见黄色的部分)。所以有:
1、 Si-k+1Si-k+2…Si-1 = Pj-k+1Pj-k+2…Pj-1。 所以当 P 前移到 K 时,有:
2、 Si-k+1Si-k+2…Si-1 = P1P2…Pk-1。 通过 1,2=>
Pj-k+1Pj-k+2…Pj-1 = P1P2…Pk-1。
而 P1P2…Pk-1和Pj-k+1Pj-k+2…Pj-1就相当于P串的前缀和后缀,前已说过,你心中一定要有前缀和后缀的概念或意识。
所以,归根究底,KMP 算法的本质便是:每一次匹配都是基于前一次匹配的结果,如何 更好地利用这前一次匹配的结果呢?针对待匹配的模式串的特点,判断它是否有重复的字 符,从而找到它的前缀与后缀,进而求出相应的 Next 数组,最终根据 Next 数组而进行 KMP 匹配。接下来,进入本文的第二部分。