任何优秀的算法都是简约而美丽的。KMP更是如此。
下面这些定义是十分重要的,功欲善其事,必先利其器。
1. 基本定义(Basic Definitions)
设 为一个字符集,并且 , 为自然数, 是长度为 的在
的前缀(prefix)为一个字串, 其中:
的后缀(suffix)为一个字串, 其中:
如果, 则 的一个前缀或者后缀 被称为真前缀(proper prefix)或真后缀(proper suffix)。
设 的一个边框(border)(这个概念相当重要,往后读读就会明白)是一个子串 。为了防止丢失语义,后面 border 不再翻译为中文。
其中:
border是 的一个真子串,这个真子串由真前缀和真后缀构成,且真前缀和真后缀相等。我们称长度 为 border 的宽度(width)(说是厚度也许更确切,更能表达意思),如果某个 border 的宽度是所有 border 中最大的,则称该 border 为最宽 border (widest border)。
可以看到 border 具有一个很好的性质:即左边界=右边界。
1.1 示例
- 例1:设.则的真前缀分别为真后缀分别为,因此具有两个border,分别是。
其中 border 表示宽度为0的串,border 的宽度为2,最宽 border 很明显为.
把这个最宽 border 加红一下:,可以看到,这个红色部分非常像一个边框,这也是 border 的含义。
对于任意字符串 (是一个字符集),其中空串 总为 的一个 border。空串本身没有 border。
KMP算法中的位移量(shift distance)将会用到字符串中 border 的概念。
- 例2:模式串与目标创匹配过程。
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
t | ||||||||||
p | ||||||||||
p |
位置 的字符已经完全匹配,但是在位置5, 和
其中位移量取决于已匹配的字符串(如上图中 部分)的最宽 border (上图的蓝色部分),在这个例子中,已经匹配的 的串长 ,其中最宽 border 的宽度为 ,于是位移量为 .
这样做的原因是 border 有左右两个部分,如上面的,我们知道左右两个部分是完全一样的。如果 部分已经完全匹配,但是在 处失配()。此时假设我们已经知道
KMP 的核心思想:
- 在预处理阶段,先求出模式串的每个前缀的最宽 border 的宽度。
- 在搜索阶段,位移量可以根据已经匹配上的前缀计算得出。
2. 预处理(Preprocessing)
2.1 next-widest border
定理(Theorem):设串 都是串 的 border,其中 ,则串 是串
证明:图1中的串 包含了两个 border 和 .由于 是 的前缀,同时它也是 的真前缀,而且 也是 的后缀,也是 的真后缀,又因为 的长度小于 ,因此 是
图1 border 的 border
- 例3: 串有两个非空 border,一个是,标红后为,还有一个是,标记成绿,很明显可以看到也是
这个性质非常有用,后面可以看到。
如果串 是 的最宽 border,则 的下一最宽 border (next-widest border)为 的最宽 border .
说的直白点,下一个最宽 border = (的最宽 border)的最宽 border,用编程语言描述话就是这样:
2.2 border 延拓 (extend)
定义:设 是一个字符串,且 是 上的一个字符,如果 是 的一个 border,则称 的 border 可通过字符
图2 border 延拓
图2中,如果 ,则 的宽为 的 border 可通过字符 延拓为 ,是 的 border。
- 例4:,的一个子串可以通过字符延拓为, 已知的 border 是,border 延拓后是
2.3 模式串预处理
在预处理阶段,构造一个长度为 的数组 (m 是模式串 p 的长度),数组的每一项 为模式串 的长度为 的前缀的最宽 border 的宽度 。由于长度为 0 的前缀 没有 border,我们规定。
图3 模式串 p 的长度为 i 的前缀,它的最宽 border 宽度是 b[i]
那么如果求取模式串长度为 i 的前缀的最宽 border 的宽度呢?
可以使用归纳法。
假设 的值已知,则 的值可以通过检测串 的 border 否可以通过字符 延拓来计算。其中 。
在图 3 中,判断是否有 ,注意灰色部分是 的 border,其宽度就是 。如果 ,可以得到 。如果,则继续看 next-widest border 是否可以延拓。
可以利用 的降序,可以获得
预处理算法包括了一个含变量 的循环,用来遍历这些值,即
如果 ,则宽为 的 border 可以通过字符 延拓;否则,通过把 设为 ,即去查找next-widest border,看是否可以延拓,可以那就可以去设定 的值了,如果不可以,继续查找,直到再也找不到 next-widest border 为止,即 的时候。
每次出现 ++ 后, 的值就是的最宽 border 的宽度,因为找到了一个字符 可以把 border 延拓为新的 border ,因此 的最宽 border 的宽度就是串 的宽度加1。然后把 的值设置为 (也就是 ++ 后设置
- 例5:对于模式串,数组中保存的 borders 宽度分别如下。例如,长度为的前缀有一个宽度为3的 border,因此.
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)
假设我们把上面算法的模式串 (长度为)改成 ( 是模式串 和目标串 的连接,见图4),上面的预处理算法理所当然也适用于计算串 的 border 的宽度,如果把串 看成 的某个长度为 的前缀 的一个 border,只要找到了这样的前缀 ,那就等于找到了匹配串的位置,即 。
串 的某个前缀 正好有一个宽度为 的 border,那就说明搜索成功了,这个时候匹配位置为 ,接着继续匹配下一个位置(如图4)。
图4 pt 的一个前缀 x 的宽度为 m 的 border
搜索算法如下:
当内循环在 处无法延拓时,则检查下一最宽 border 是否可以延拓,即检查模式串的长度为 的最宽 border 是否可以延拓(如图5)。如果仍无法延拓,则继续检查下一最宽 border,直至下一最宽 border 为空(即时),或者可以延拓为止。
图5 在 j 处失配(无法延拓)后模式串的移动,即看是否可以延拓下一最宽 border
如果所有的 个字符都可以匹配上,这时候,函数
下面给出一个完整的代码:
参考文献:http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm