KMP算法

[数据结构与算法] 同时被 2 个专栏收录
8 篇文章 1 订阅
14 篇文章 2 订阅

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

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

1. 基本定义(Basic Definitions)

A A A 为一个字符集,并且 x = x 0 … x k − 1 x=x_0…x_{k-1} x=x0xk1 k k k 为自然数, x x x 是长度为 k k k 的在 A A A 上的一个字符串。

x x x前缀(prefix)为一个字串 u u u, 其中:
u = x 0 x 1 … x b − 1 , b ∈ { 0 , 1 , … , k } u = x_0x_1…x_{b-1}, b\in\{0,1, …,k\} u=x0x1xb1,b{0,1,,k}
x x x后缀(suffix)为一个字串 u u u, 其中:
u = x k − b x k − b + 1 … x k − 1 , b ∈ { 0 , 1 , … , k } u = x_{k-b}x_{k-b+1}…x_{k-1}, b\in\{0,1, …,k\} u=xkbxkb+1xk1,b{0,1,,k}
如果 u ≠ x ( 即 b &lt; k ) u \neq x (即b &lt; k) u̸=x(b<k), 则 x x x 的一个前缀或者后缀 u u u 被称为真前缀(proper prefix)或真后缀(proper suffix)。

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

其中:
r = x 0 x 1 … x b − 1 且 r = x k − b x k − b + 1 … x k − 1 , b ∈ { 0 , 1 , … , k − 1 } , k = ∣ x ∣ r = x_0x_1…x_{b-1} 且 r=x_{k-b}x_{k-b+1}…x_{k-1}, b∈\{0,1, …,k-1\},k=|x| r=x0x1xb1r=xkbxkb+1xk1,b{0,1,,k1},k=x

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

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

1.1 示例

  • 例1:设 x = a b a c a b x = abacab x=abacab.则 x x x 的真前缀分别为 { ε , a , a b , a b a , a b a c , a b a c a } \{\varepsilon, a, ab, aba, abac, abaca\} {ε,a,ab,aba,abac,abaca} 真后缀分别为 { ε , b , a b , c a b , a c a b , b a c a b , x } \{\varepsilon, b, ab, cab, acab, bacab, x\} {ε,b,ab,cab,acab,bacab,x},因此 x x x 具有两个border,分别是 ε 和 a b \varepsilon 和 ab εab

其中 border ε \varepsilon ε 表示宽度为0的串,border a b ab ab 的宽度为2,最宽 border 很明显为 a b ab ab.

把这个最宽 border 加红一下: x = a b a c a b x =\color{red}{ab}\color{black}{ac}\color{red}{ab} x=abacab,可以看到,这个红色部分非常像一个边框,这也是 border 的含义。

对于任意字符串 x ∈ A x\in A xA A A A是一个字符集),其中空串 ε \varepsilon ε 总为 x x x 的一个 border。空串 ε \varepsilon ε本身没有 border。
KMP算法中的位移量(shift distance)将会用到字符串中 border 的概念。

  • 例2:模式串与目标创匹配过程。
i0123456789
t a a a b b b c c c a a a b b b c c c a a a b b b d d d
p a \color{blue}a a b \color{blue}b b c \color{green}c c a \color{blue}a a b \color{blue}b b d \color{red}d d
p a \color{blue}a a b \color{blue}b b c c c a \color{blue}a a b \color{blue}b b d d d

位置 0 , … , 4 0,…,4 0,,4 的字符已经完全匹配,但是在位置5, c c c d d d 失配。于是模式串向右移动 3 个位置,接着在位置 5 继续进行比较。

其中位移量取决于已匹配的字符串(如上图中 a b c a b abcab abcab 部分)的最宽 border (上图的蓝色部分),在这个例子中,已经匹配的 a b c a b abcab abcab 的串长 j = 5 j=5 j=5 ,其中最宽 border a b ab ab 的宽度为 b = 2 b=2 b=2 ,于是位移量为 j − b = 5 − 2 = 3 j-b = 5-2=3 jb=52=3.

这样做的原因是 border 有左右两个部分,如上面的 x = a b a c a b x =\color{red}{ab}\color{black}{ac}\color{red}{ab} x=abacab,我们知道左右两个部分是完全一样的。如果 p 0 p 1 . . . p j − 1 p_0p_1...p_{j-1} p0p1...pj1 部分已经完全匹配,但是在 j j j 处失配( p j ≠ t i p_j \neq t_i pj̸=ti)。此时假设我们已经知道 p 0 p 1 . . . p j − 1 p_0p_1...p_{j-1} p0p1...pj1 的最宽 border 的宽度,只需要把它左边界的部分移动到原先右边界的位置,继续匹配即可。

KMP 的核心思想:

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

2. 预处理(Preprocessing)

2.1 next-widest border

定理(Theorem):设串 r , s r, s r,s 都是串 x x x 的 border,其中 ∣ r ∣ &lt; ∣ s ∣ |r| &lt; |s| r<s,则串 r r r 是串 s s s 的 border。

证明:图1中的串 x x x 包含了两个 border r r r s s s.由于 r r r x x x 的前缀,同时它也是 s s s 的真前缀,而且 r r r也是 x x x 的后缀, r r r也是 s s s 的真后缀,又因为 r r r 的长度小于 s s s,因此 r r r s s s 的 border。

这里写图片描述

图1 border 的 border
  • 例3: 串 a b a c a b a abacaba abacaba有两个非空 border,一个是 a a a ,标红后为 a b a c a b a \color{red}{a}\color{black}{bacab}\color{red}a abacaba,还有一个是 a b a aba aba,标记成绿 a b a c a b a \color{green}{aba}\color{black}{c}\color{green}{aba} abacaba,很明显可以看到 a a a 也是 a b a \color{red}{a}\color{black}{b}\color{red}a aba 的 border。

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

如果串 s s s x x x 的最宽 border,则 x x x下一最宽 border (next-widest border)为 s s s 的最宽 border r r r.

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

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

2.2 border 延拓 (extend)

定义:设 x x x 是一个字符串,且 a a a A A A 上的一个字符,如果 r a ra ra x a xa xa 的一个 border,则称 x x x 的 border r r r 可通过字符 a a a 延拓(extend)。

这里写图片描述

图2 border 延拓

图2中,如果 x j = a x_j=a xj=a,则 x x x 的宽为 j j j 的 border 可通过字符 a a a 延拓为 r a ra ra r a ra ra x a xa xa的 border。

  • 例4 s = c b o b c b o s=cbobcbo s=cbobcbo, s s s 的一个子串 x = c b o b c b x=cbobcb x=cbobcb 可以通过字符 o o o 延拓为 s s s, 已知 x x x 的 border 是 c b cb cb,border 延拓后 c b o cbo cbo s s s 的 border。

2.3 模式串预处理

在预处理阶段,构造一个长度为 m + 1 m+1 m+1 的数组 b b b (m 是模式串 p 的长度),数组的每一项 b [ i ] b[i] b[i] 为模式串 p = p 0 p 1 ⋯ p m − 1 p=p_0p_1\cdots p_{m-1} p=p0p1pm1 的长度为 i i i 的前缀 ( p 0 p 1 ⋯ p i − 1 ) (p_0p_1\cdots p_{i-1}) (p0p1pi1)的最宽 border 的宽度 ( i = 0 , … , m ) (i=0,…,m) (i=0,,m)。由于长度为 0 的前缀 ε \varepsilon ε 没有 border,我们规定 b [ 0 ] = − 1 b[0] = -1 b[0]=1

这里写图片描述

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

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

可以使用归纳法。

假设 b [ 0.. i ] b[0..i] b[0..i] 的值已知,则 b [ i + 1 ] b[i+1] b[i+1] 的值可以通过检测串 p 0 … p i − 1 p_0…p_{i-1} p0pi1 的 border r r r 否可以通过字符 p i p_i pi 延拓来计算。其中 r = b o r d e r ( p 0 … p i − 1 ) r = border(p_0…p_{i-1}) r=border(p0pi1)

在图 3 中,判断是否有 p i = p b [ i ] p_i = p_{b[i]} pi=pb[i],注意灰色部分是 p 0 … p i − 1 p_0…p_{i-1} p0pi1 的 border,其宽度就是 b [ i ] b[i] b[i]。如果 p i = p b [ i ] p_i = p_{b[i]} pi=pb[i],可以得到 b [ i + 1 ] = b [ i ] + 1 b[i+1]=b[i]+1 b[i+1]=b[i]+1。如果 p i ≠ p b [ i ] p_i \neq p_{b[i]} pi̸=pb[i],则继续看 next-widest border 是否可以延拓。

可以利用 b [ i ] , b [ b [ i ] ] , b [ b [ b [ i ] ] ] … b[i],b[b[i]],b[b[b[i]]]… b[i],b[b[i]],b[b[b[i]]] 的降序,可以获得 p 0 … p i − 1 p_0…p_{i-1} p0pi1 所有的 borders。注意到这个降序序列,读成一句话就是“border 的宽度,border 的 border 的宽度,border 的 border 的宽度……”

预处理算法包括了一个含变量 j j j 的循环,用来遍历这些值,即 b [ j ] , b [ b [ j ] ] , b [ b [ b [ j ] ] ] … b[j],b[b[j]],b[b[b[j]]]… b[j],b[b[j]],b[b[b[j]]]

如果 p j = p i p_j=p_i pj=pi,则宽为 j j j 的 border 可以通过字符 p i p_i pi延拓;否则,通过把 j j j 设为 b [ j ] b[j] b[j],即去查找next-widest border,看是否可以延拓,可以那就可以去设定 b [ i + 1 ] b[i+1] b[i+1] 的值了,如果不可以,继续查找,直到再也找不到 next-widest border 为止,即 j = − 1 j = -1 j=1的时候。

每次出现 j j j++ 后, j j j 的值就是 p 0 … p i p_0…p_i p0pi的最宽 border 的宽度,因为找到了一个字符 p j = p i p_j=p_i pj=pi 可以把 border p 0 p 1 … p j − 1 p_0p_1…p_{j-1} p0p1pj1 延拓为新的 border p 0 p 1 … p j p_0p_1…p_{j} p0p1pj,因此 p 0 … p i p_0…p_i p0pi 的最宽 border 的宽度就是串 p 0 p 1 … p j − 1 p_0p_1…p_{j-1} p0p1pj1 的宽度加1。然后把 b [ i + 1 ] b[i+1] b[i+1] 的值设置为 j j j (也就是 i i i++ 后设置 b [ i ] b[i] b[i] 的值)。下面是预处理处算法代码:

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:对于模式串 p = a b a b a a p=ababaa p=ababaa,数组 b b b 中保存的 borders 宽度分别如下。例如,长度为 5 5 5 的前缀 a b a b a ababa ababa 有一个宽度为3的 border,因此 b [ 5 ] = 3 b[5]=3 b[5]=3.
j0123456
p[j]ababaa
b[j]-1001231

3. 搜索算法(Searching algorithm)

假设我们把上面算法的模式串 p p p(长度为 m m m)改成 p t pt pt ( p t pt pt 是模式串 p p p 和目标串 t t t 的连接,见图4),上面的预处理算法理所当然也适用于计算串 p t pt pt 的 border 的宽度,如果把串 p p p 看成 p t pt pt 的某个长度为 i i i 的前缀 x x x 的一个 border,只要找到了这样的前缀 x x x,那就等于找到了匹配串的位置,即 i − m i-m im

p t pt pt 的某个前缀 x x x 正好有一个宽度为 m m m 的 border,那就说明搜索成功了,这个时候匹配位置为 i − m i-m im,接着继续匹配下一个位置(如图4)。

这里写图片描述

图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];
        }
    }
}   

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

这里写图片描述

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

如果所有的 m m m 个字符都可以匹配上,这时候 j = m j=m j=m,函数 r e p o r t report report 的作用就是上报匹配位置。接下来,把最宽 border 做降序操作,继续匹配下一个地方。直到完成外层整个循环。

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

#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

  • 1
    点赞
  • 2
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值