• 沒有找到結果。

分、KMP 算法初解

在文檔中 十五个经典算法研究与总结 (頁 168-174)

六(三续):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-1Pj-k+1Pj-k+2…Pj-1就相当于P串的前缀和后缀,前已说过,你心中一定要有前缀和后缀的概念或意识。

所以,归根究底,KMP 算法的本质便是:每一次匹配都是基于前一次匹配的结果,如何 更好地利用这前一次匹配的结果呢?针对待匹配的模式串的特点,判断它是否有重复的字 符,从而找到它的前缀与后缀,进而求出相应的 Next 数组,最终根据 Next 数组而进行 KMP 匹配。接下来,进入本文的第二部分。

在文檔中 十五个经典算法研究与总结 (頁 168-174)

相關文件