KMP算法,俗称Kill Myself Personally。。。不过今天总算明白了。
理论
KMP算法不容易想,比较晦涩难懂。不过当你知道了它的思想后,算法实现就变得稍微容易起来。在我的上一篇博客算法导论——字符串搜索FiniteMachine有限状态机实现中,我们提到了状态 q q q。 q q q代表的是模式串 P P P和文本串 T T T已匹配的字符个数; 另外,我们还提到了状态转移表。事实上,KMP算法的核心便是基于有限状态机的改进——如何才能将二维的表转换成一个线性的表,并且通过状态 q q q实现字符串匹配?同样,最简单的,我们用一个例子来说明KMP算法。我们假设模式串 P = { a , b , a , d } P=\{a,b,a,d\} P={a,b,a,d}
前缀表
什么是前缀表?为什么我们要计算前缀表?事实上,我们现在学习的很多东西都是先知道是什么,然后才进行进一步的讲解。这实际上是不对的。我们应该先知道为什么,然后才能去归纳出方法,形成定义。所以,我将从“为什么”出发来解释前缀表。下面给出一幅图:
这就是KMP算法的全部思想,OK,讲述完毕。大家散了吧。
开个玩笑。从图中,我们可以看到,模式串
P
P
P的前3个元素与文本串
T
T
T相匹配,第4个元素不匹配。因为模式串
P
P
P的第三个元素(作为前三个元素的后缀)与模式串
P
P
P的第一个元素(作为前三个元素的前缀)相同,因此,下一次比较,我们直接可以滑动到如图所示的位置即模式串
P
P
P下标为1的地方进行。如果对于一个给定的模式串
P
P
P,我们知道了它的所有元素对应的滑动性质,那么我们便能够很快地进行字符串匹配了。前人据此定义出了前缀表,其具体定义我不愿多提,因为数学语言较为复杂,大家可以在书本上看到。下面我们给出模式串
P
P
P的前缀表。
KMP算法实现思路
现在我们已经有了我们的前缀表,我们把它命名为 Π \Pi Π。 Π [ q ] 代 表 如 果 模 式 串 P 的 第 q + 1 个 与 文 本 串 T [ i ] 不 匹 配 时 , 我 们 应 该 把 模 式 串 向 前 挪 动 q − Π [ q ] 的 距 离 。 \Pi[q]代表如果模式串P的第q+1个与文本串T[i]不匹配时,我们应该把模式串向前挪动q-\Pi[q]的距离。 Π[q]代表如果模式串P的第q+1个与文本串T[i]不匹配时,我们应该把模式串向前挪动q−Π[q]的距离。(看上面的例子, Π [ 3 ] \Pi[3] Π[3]代表 P [ 3 + 1 ] ≠ T [ i ] , 即 d ≠ c , 时 , 我 们 应 该 把 模 式 串 P 向 前 移 动 q − Π [ q ] = 3 − 1 = 2 的 距 离 , 此 时 q = 1 P[3+1] \ne T[i],即d\ne c,时,我们应该把模式串P向前移动q-\Pi [q]=3-1=2的距离,此时q=1 P[3+1]=T[i],即d=c,时,我们应该把模式串P向前移动q−Π[q]=3−1=2的距离,此时q=1)。这时大家就可以发现,这里的 q q q刚好就是我们的已经匹配的字符个数,即我们在有限状态机里的“状态”。状态的转移应该满足什么条件呢?
- 如果 T [ i ] = P [ q + 1 ] , 则 q = q + 1 。 例 如 : T[i]=P[q+1],则q=q+1。例如: T[i]=P[q+1],则q=q+1。例如:
此时
q
=
1
,
即
已
经
匹
配
了
1
个
字
符
‘
a
’
,
且
T
[
i
]
=
b
,
P
[
q
+
1
]
=
b
,
T
[
i
]
=
P
[
q
+
1
]
,
所
以
匹
配
字
符
应
该
加
1
,
即
p
=
2
q=1,即已经匹配了1个字符‘a’,且T[i]=b,P[q+1]=b,T[i]=P[q+1],所以匹配字符应该加1,即p=2
q=1,即已经匹配了1个字符‘a’,且T[i]=b,P[q+1]=b,T[i]=P[q+1],所以匹配字符应该加1,即p=2
- 如果 T [ i ] ≠ P [ q + 1 ] , 则 如 果 q > 0 , q = Π [ q ] , 一 直 循 环 。 就 以 上 面 的 例 子 为 例 : T[i]\ne P[q+1],则如果q>0,q=\Pi[q],一直循环。就以上面的例子为例: T[i]=P[q+1],则如果q>0,q=Π[q],一直循环。就以上面的例子为例:
此时
q
=
3
,
因
为
匹
配
了
3
个
字
符
′
a
b
a
′
,
而
T
[
i
]
=
c
,
P
[
q
+
1
]
=
d
,
T
[
i
]
≠
P
[
q
+
1
]
,
我
们
应
该
将
模
式
串
向
前
滑
动
,
滑
动
到
哪
里
呢
?
就
是
q
=
Π
[
q
]
=
1
的
位
置
了
,
即
此
时
只
匹
配
了
一
个
字
符
,
而
此
时
T
[
i
]
=
c
,
P
[
q
+
1
]
=
b
,
T
[
i
]
≠
P
[
q
+
1
]
,
此
时
应
该
继
续
滑
动
模
式
串
,
即
q
=
Π
[
q
]
=
0
,
也
就
是
说
,
此
时
文
本
串
和
模
式
串
没
有
相
匹
配
的
元
素
了
。
q=3,因为匹配了3个字符'aba',而T[i]=c,P[q+1]=d,T[i]\ne P[q+1],我们应该将模式串向前滑动,滑动到哪里呢?就是q=\Pi[q]=1的位置了,即此时只匹配了一个字符,而此时T[i]=c,P[q+1]=b,T[i]\ne P[q+1],此时应该继续滑动模式串,即q=\Pi[q] = 0,也就是说,此时文本串和模式串没有相匹配的元素了。
q=3,因为匹配了3个字符′aba′,而T[i]=c,P[q+1]=d,T[i]=P[q+1],我们应该将模式串向前滑动,滑动到哪里呢?就是q=Π[q]=1的位置了,即此时只匹配了一个字符,而此时T[i]=c,P[q+1]=b,T[i]=P[q+1],此时应该继续滑动模式串,即q=Π[q]=0,也就是说,此时文本串和模式串没有相匹配的元素了。直到下一次,我们有
T
[
i
]
=
P
[
q
+
1
]
时
T[i]=P[q+1]时
T[i]=P[q+1]时,这时,我们就能依据原则一进行下一轮的操作了。如下图所示:
- 当 q = m 时 , 也 就 是 模 式 串 的 所 有 字 符 都 已 经 匹 配 。 这 时 , 我 们 就 记 录 下 结 果 , 并 令 q = Π [ q ] , 因 为 此 时 我 们 将 要 准 备 下 一 轮 的 K M P 了 。 q=m时,也就是模式串的所有字符都已经匹配。这时,我们就记录下结果,并令q=\Pi[q],因为此时我们将要准备下一轮的KMP了。 q=m时,也就是模式串的所有字符都已经匹配。这时,我们就记录下结果,并令q=Π[q],因为此时我们将要准备下一轮的KMP了。为了说明这种情况,我们假设模式串 P = { a , b , a } P=\{a,b,a\} P={a,b,a},举出下述例子:
前缀表实现原理
前缀表的实现也可看成是字符串匹配,只不过是自己匹配自己,但是这里要注意的是 Π [ 0 ] = 0 \Pi[0]=0 Π[0]=0,也就是说,作为文本串的模式串不考虑 P [ 0 ] , 作 为 模 式 串 的 模 式 串 不 考 虑 P [ m ] P[0],作为模式串的模式串不考虑P[m] P[0],作为模式串的模式串不考虑P[m]。也就是说,匹配的初始状态如下图:
事实上,我们可以这么认为。作为文本串的模式串相当于后缀,而作为模式串的模式串相当于前缀,如果他们匹配上了,就说明此时前缀与后缀相等,于是,我们就能够知道模式串的前缀表了。
好了,不多BB了,开撸。
C#实现
Program.cs
using System;
using System.Collections.Generic;
namespace StringMatch
{
class Program
{
static void Main(string[] args)
{
string T = "Star, I Want to Love with U, I'm so in Love with U";
string CharacterSet = T;
string P = "Love with U";
Console.WriteLine("KMP算法");
KMP kMP = new KMP();
result = kMP.KMPStrategy(P, T);
GetResult(result, P, T);
}
private static void GetResult(List<int> result, string P, string T)
{
for (int i = 0; i < result.Count; i++)
{
Console.WriteLine("在位置" + (result[i] + 1) + "找到:");
for (int j = result[i]; j < result[i] + P.Length; j++)
{
Console.Write(T[j]);
}
Console.WriteLine(" ");
}
Console.WriteLine("==========================================");
}
private static void GetResult(List<int> result)
{
for(int i = 0; i < result.Count; i++)
{
Console.Write(result[i]);
Console.Write(" ");
}
Console.WriteLine(" ");
Console.WriteLine("==========================================");
}
}
}
KMP.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace StringMatch
{
public class KMP
{
/*演算
* 计算P[0...k] = P[n-k...n], k = 0,...,n-2,n-1
* 为什么是n-1?
* 因为我们是计算P的前缀和后缀,如果说k=n,那么两个P[0...k] = P[n-k...n]就变成
* P[0...n] = P[0...n],显然,这种情况并不是我们所说的前后缀, 两个字符串重合了。
* 问题转换为一个字符串匹配问题,不过匹配点直接从1开始,而不是0.
*/
public List<int> CalculatePreffixTable(string P)
{
int q = 0;
int m = P.Length;
List<int> pi = new List<int>();
pi.Add(0);
for(int i = 1; i < m; i++)
{
while(q > 0 && P[q] != P[i])
{
q = pi[q - 1];
}
if (P[q] == P[i])
{
q++;
}
pi.Add(q);
}
return pi;
}
public List<int> KMPStrategy(string P, string T)
{
List<int> result = new List<int>();
int m = P.Length;
int q = 0;
List<int> pi = CalculatePreffixTable(P);
for(int i = 0; i < T.Length; i++)
{
while(q > 0 && P[q] != T[i])
{
q = pi[q - 1];
}
if(P[q] == T[i])
{
q++;
}
if (q == m)
{
result.Add(i - m + 1);
q = pi[q - 1];
}
}
return result;
}
}
}