任何优秀的算法都是简约而美丽的。KMP更是如此。

下面这些定义是十分重要的,功欲善其事,必先利其器。

1. 基本定义(Basic Definitions)

KMP算法_算法 为一个字符集,并且 KMP算法_子串_02KMP算法_basic_03 为自然数,KMP算法_后缀_04 是长度为 KMP算法_basic_03 的在 KMP算法_算法

KMP算法_后缀_04前缀(prefix)为一个字串KMP算法_子串_08, 其中:
KMP算法_后缀_09
KMP算法_后缀_04后缀(suffix)为一个字串KMP算法_子串_08, 其中:
KMP算法_子串_12
如果KMP算法_kmp_13, 则 KMP算法_后缀_04 的一个前缀或者后缀 KMP算法_子串_08 被称为真前缀(proper prefix)或真后缀(proper suffix)。

KMP算法_后缀_04 的一个边框(border)(这个概念相当重要,往后读读就会明白)是一个子串 KMP算法_后缀_17。为了防止丢失语义,后面 border 不再翻译为中文。

其中:
KMP算法_kmp_18

borderKMP算法_后缀_04 的一个真子串,这个真子串由真前缀真后缀构成,且真前缀和真后缀相等。我们称长度 KMP算法_子串_20border 的宽度(width)(说是厚度也许更确切,更能表达意思),如果某个 border 的宽度是所有 border 中最大的,则称该 border 为最宽 border (widest border)。

可以看到 border 具有一个很好的性质:即左边界=右边界。

1.1 示例

  • 例1:设KMP算法_basic_21.则KMP算法_算法_22的真前缀分别为KMP算法_后缀_23真后缀分别为KMP算法_算法_24,因此KMP算法_算法_22具有两个border,分别是KMP算法_后缀_26

其中 border KMP算法_basic_27 表示宽度为0的串,border KMP算法_basic_28 的宽度为2,最宽 border 很明显为KMP算法_basic_28.

把这个最宽 border 加红一下:KMP算法_算法_30,可以看到,这个红色部分非常像一个边框,这也是 border 的含义。

对于任意字符串 KMP算法_子串_31KMP算法_算法是一个字符集),其中空串 KMP算法_basic_27 总为 KMP算法_后缀_04 的一个 border。空串KMP算法_basic_27本身没有 border。
KMP算法中的位移量(shift distance)将会用到字符串中 border 的概念。

  • 例2:模式串与目标创匹配过程。

i

0

1

2

3

4

5

6

7

8

9

t

KMP算法_kmp_36

KMP算法_后缀_37

KMP算法_kmp_38

KMP算法_kmp_36

KMP算法_后缀_37

KMP算法_kmp_38

KMP算法_kmp_36

KMP算法_后缀_37

KMP算法_kmp_44

p

KMP算法_算法_45

KMP算法_子串_46

KMP算法_算法_47

KMP算法_算法_45

KMP算法_子串_46

KMP算法_算法_50

p

KMP算法_算法_45

KMP算法_子串_46

KMP算法_kmp_38

KMP算法_算法_45

KMP算法_子串_46

KMP算法_kmp_44

位置 KMP算法_kmp_57 的字符已经完全匹配,但是在位置5,KMP算法_子串_58KMP算法_算法_59

其中位移量取决于已匹配的字符串(如上图中 KMP算法_子串_60 部分)的最宽 border (上图的蓝色部分),在这个例子中,已经匹配的 KMP算法_子串_60 的串长 KMP算法_算法_62 ,其中最宽 border KMP算法_basic_28 的宽度为 KMP算法_kmp_64 ,于是位移量为 KMP算法_kmp_65.

这样做的原因是 border 有左右两个部分,如上面的KMP算法_算法_30,我们知道左右两个部分是完全一样的。如果 KMP算法_basic_67 部分已经完全匹配,但是在 KMP算法_算法_68 处失配(KMP算法_后缀_69)。此时假设我们已经知道 KMP算法_basic_67

KMP 的核心思想:

  • 在预处理阶段,先求出模式串的每个前缀的最宽 border 的宽度。
  • 在搜索阶段,位移量可以根据已经匹配上的前缀计算得出。

2. 预处理(Preprocessing)

2.1 next-widest border

定理(Theorem):设串 KMP算法_子串_71 都是串 KMP算法_后缀_04 的 border,其中 KMP算法_子串_73,则串 KMP算法_后缀_17 是串 KMP算法_kmp_75

证明:图1中的串 KMP算法_后缀_04 包含了两个 border KMP算法_后缀_17KMP算法_kmp_75.由于 KMP算法_后缀_17KMP算法_后缀_04 的前缀,同时它也是 KMP算法_kmp_75 的真前缀,而且 KMP算法_后缀_17也是 KMP算法_后缀_04 的后缀,KMP算法_后缀_17也是 KMP算法_kmp_75 的真后缀,又因为 KMP算法_后缀_17 的长度小于 KMP算法_kmp_75,因此 KMP算法_后缀_17KMP算法_kmp_75

KMP算法_kmp_90


图1 border 的 border

  • 例3: 串KMP算法_后缀_91有两个非空 border,一个是KMP算法_子串_92,标红后为KMP算法_basic_93,还有一个是KMP算法_后缀_94,标记成绿KMP算法_basic_95,很明显可以看到KMP算法_子串_92也是KMP算法_basic_97

这个性质非常有用,后面可以看到。

如果串 KMP算法_kmp_75KMP算法_后缀_04 的最宽 border,则 KMP算法_后缀_04下一最宽 border (next-widest border)为 KMP算法_kmp_75 的最宽 border KMP算法_后缀_17.

说的直白点,下一个最宽 border = (KMP算法_算法_22的最宽 border)的最宽 border,用编程语言描述话就是这样:

// 假设 border 函数可以求取最宽 border
s = border(x);
// 称 r 为 x 的 next-widest border
r = border(s);
// 也有结论:
r = border(border(x));

2.2 border 延拓 (extend)

定义:设 KMP算法_后缀_04 是一个字符串,且 KMP算法_后缀_105KMP算法_算法 上的一个字符,如果 KMP算法_basic_107KMP算法_后缀_108 的一个 border,则称 KMP算法_后缀_04 的 border KMP算法_后缀_17 可通过字符 KMP算法_后缀_105

KMP算法_后缀_112


图2 border 延拓

图2中,如果 KMP算法_后缀_113,则 KMP算法_后缀_04 的宽为 KMP算法_算法_68 的 border 可通过字符 KMP算法_后缀_105 延拓为 KMP算法_basic_107KMP算法_basic_107KMP算法_后缀_108的 border。

  • 例4KMP算法_算法_120,KMP算法_后缀_121的一个子串KMP算法_子串_122可以通过字符KMP算法_basic_123延拓为KMP算法_后缀_121, 已知KMP算法_算法_22的 border 是KMP算法_算法_126,border 延拓后KMP算法_后缀_127KMP算法_后缀_121

2.3 模式串预处理

在预处理阶段,构造一个长度为 KMP算法_后缀_129 的数组 KMP算法_子串_20 (m 是模式串 p 的长度),数组的每一项 KMP算法_kmp_131 为模式串 KMP算法_子串_132 的长度为 KMP算法_kmp_133 的前缀KMP算法_算法_134的最宽 border 的宽度 KMP算法_子串_135。由于长度为 0 的前缀 KMP算法_basic_27 没有 border,我们规定KMP算法_算法_137

KMP算法_basic_138


图3 模式串 p 的长度为 i 的前缀,它的最宽 border 宽度是 b[i]


那么如果求取模式串长度为 i 的前缀的最宽 border 的宽度呢?

可以使用归纳法。

假设 KMP算法_basic_139 的值已知,则 KMP算法_算法_140 的值可以通过检测串 KMP算法_basic_141 的 border KMP算法_后缀_17 否可以通过字符 KMP算法_后缀_143 延拓来计算。其中 KMP算法_basic_144

在图 3 中,判断是否有 KMP算法_子串_145,注意灰色部分是 KMP算法_basic_141 的 border,其宽度就是 KMP算法_kmp_131。如果 KMP算法_子串_145,可以得到 KMP算法_basic_149。如果KMP算法_子串_150,则继续看 next-widest border 是否可以延拓。

可以利用 KMP算法_basic_151 的降序,可以获得 KMP算法_basic_141

预处理算法包括了一个含变量 KMP算法_算法_68 的循环,用来遍历这些值,即KMP算法_算法_154

如果 KMP算法_子串_155,则宽为 KMP算法_算法_68 的 border 可以通过字符 KMP算法_后缀_143延拓;否则,通过把 KMP算法_算法_68 设为 KMP算法_子串_159,即去查找next-widest border,看是否可以延拓,可以那就可以去设定 KMP算法_算法_140 的值了,如果不可以,继续查找,直到再也找不到 next-widest border 为止,即 KMP算法_子串_161的时候。

每次出现 KMP算法_算法_68++ 后,KMP算法_算法_68 的值就是KMP算法_后缀_164的最宽 border 的宽度,因为找到了一个字符 KMP算法_子串_155 可以把 border KMP算法_kmp_166 延拓为新的 border KMP算法_算法_167,因此 KMP算法_后缀_164 的最宽 border 的宽度就是串 KMP算法_kmp_166 的宽度加1。然后把 KMP算法_算法_140 的值设置为 KMP算法_算法_68 (也就是 KMP算法_kmp_133++ 后设置 KMP算法_kmp_131

void kmpPreprocess()
{
//i: 当前指针。j: 当前 border 的宽度。
int i = 0, j = -1;
b[i] = j; //初始化b[0]
while (i < m)
{
//查找下一最宽 border,直到可以延拓
while (j >= 0 && p[i] != p[j]) j = b[j];// border 宽度大于等于0且无法延拓
// 进行延拓。
i++; j++;
b[i] = j;
}
}
  • 例5:对于模式串KMP算法_算法_174,数组KMP算法_算法_175中保存的 borders 宽度分别如下。例如,长度为KMP算法_后缀_176的前缀KMP算法_basic_177有一个宽度为3的 border,因此KMP算法_算法_178.

j

0

1

2

3

4

5

6

p[j]

a

b

a

b

a

a

b[j]

-1

0

0

1

2

3

1

3. 搜索算法(Searching algorithm)

假设我们把上面算法的模式串 KMP算法_basic_179(长度为KMP算法_后缀_180)改成 KMP算法_后缀_181 (KMP算法_后缀_181 是模式串 KMP算法_basic_179 和目标串 KMP算法_后缀_184 的连接,见图4),上面的预处理算法理所当然也适用于计算串 KMP算法_后缀_181 的 border 的宽度,如果把串 KMP算法_basic_179 看成 KMP算法_后缀_181 的某个长度为 KMP算法_kmp_133 的前缀 KMP算法_后缀_04 的一个 border,只要找到了这样的前缀 KMP算法_后缀_04,那就等于找到了匹配串的位置,即 KMP算法_kmp_191

KMP算法_后缀_181 的某个前缀 KMP算法_后缀_04 正好有一个宽度为 KMP算法_后缀_180 的 border,那就说明搜索成功了,这个时候匹配位置为 KMP算法_kmp_191,接着继续匹配下一个位置(如图4)。

KMP算法_算法_196


图4 pt 的一个前缀 x 的宽度为 m 的 border 搜索算法如下:

void kmpSearch()
{
//i: 当前指针 j: 当前 border 的宽度
int i = 0, j = 0;
while (i < n)
{
// 当前位置无法延拓就继续搜索下一最宽 border,直到可以延拓。
while (j >= 0 && t[i] != p[j]) j = b[j];
i++; j++;
//如果 border 的宽度正好为 m,说明匹配到模式串了
if (j == m)
{
//上报匹配结果
report(i j);

//将j降到下一最宽 border,然后继续执行下一次匹配。实际上,这行代码也可略去不写。
//因为一下次执行到while(j>=0&&t[i]!=p[j])j=b[j];也会因为无法延拓而执行循环体。
//此时的p[j]肯定等于字符'\0'
j = b[j];
}
}
}

当内循环在 KMP算法_算法_68 处无法延拓时,则检查下一最宽 border 是否可以延拓,即检查模式串的长度为 KMP算法_算法_68 的最宽 border 是否可以延拓(如图5)。如果仍无法延拓,则继续检查下一最宽 border,直至下一最宽 border 为空(即KMP算法_子串_161时),或者可以延拓为止。

KMP算法_算法_200


图5 在 j 处失配(无法延拓)后模式串的移动,即看是否可以延拓下一最宽 border


如果所有的 KMP算法_后缀_180 个字符都可以匹配上,这时候KMP算法_后缀_202,函数 KMP算法_后缀_203

下面给出一个完整的代码:

#include "string.h"

char t[] = "ababbababacabacababacacbacababacababaa";
char p[] = "ababac";
const int n = strlen(t);
const int m = strlen(p);
int b[7] = {0};

void KmpPreprocess()
{
int i = 0, j = -1;
b[i] = j;
while (i < m)
{
while (j >= 0 && p[i] != p[j]) j = b[j];
i++; j++;
b[i] = j;
}
}
void report(int nIndex)
{
printf("%d ", nIndex);
}
void KmpSearch()
{
int i = 0, j = 0;
while (i < n)
{
while (j >= 0 && t[i] != p[j]) j = b[j];
i++; j++;
if (j == m)
{
report(i - j);
j = b[j];
}
}
}

int _tmain(int argc, _TCHAR* argv[])
{
KmpPreprocess();
KmpSearch();
getchar();
return 0;
}

参考文献:​​http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm​