串的模式匹配算法——KMP

声明

原文链接:http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm ;
原文的“text”翻译为主串,通常用S表示,长度为n;
原文的"pattern"翻译为模式串,通常用T表示,长度为m;
括号中是自己的一些注释;

思想

在移动了模式串之后,原始的算法(应该就是指普通的BF吧~)忽略了之前匹配的信息,所以可能造成主串一遍又一遍的回溯,最坏情况下的复杂度为 Θ( nm ) 。
KMP算法利用了从之前的匹配中获得的信息,它不会重复比较匹配过的主串(也就是S的指针永远不会回溯),所以它的复杂度为O(n)。
无论如何,分析模式串的结构是非常必要的。这个过程的复杂度是O(m),而 m<=n ,所以整个算法的复杂度为O(n)。

基本定义

定义:
(前缀后缀不再罗嗦了。。就是不包括同 x 串完全相等的,翻译为真前缀和真后缀)
x串的border(不知道怎么翻译,保留),是这样一个子串r,使得r  =  x0 ... xb-1并且  r  =  xk-b ... xk-1   b element {0, ..., k-1} (其实就是串 x 的前面一部分和后面一部分是相等的,相等部分的子串的长度是b,想想为什么定义这个border,很好的思想哦~这样主串的指针就不必回溯了~后面会说明~)
x串的border即是真前缀,也是真后缀,我们将b称为x的border的长度。

比如:
让 x = “abacab”
真前缀:ε, a, ab, aba, abac, abaca
真后缀:ε, b, ab, cab, acab, bacab
border:ε(长度是0), ab(长度是2)

空串 ε 总是x的border,而它自己没有border。

下面的例子说明了KMP算法是怎样根据一个串的border来决定移动的距离的。(说是移动的距离,体现在程序中,其实就是模式串的指针 j 要向前怎么移动才能保证主串S的指针不回溯就能继续判断匹配)

例子:
0 1 2 3 4 5 6 7 8 9 ...
a b c a b c a b d
a b c a b d
          a b c a b d

可以看出0到4都匹配了,5没有匹配,模式串可以移动三个位置(即不必判断模式串的前两位,指针 j 移动到第三位 c 的位置),主串的位置仍然保留在5。
移动的距离是根据最长匹配的前缀得到的,是该前缀的border长度的最大值。在这个例子中,最长匹配前缀是abcab,它的长度是 j = 5.他最长的border是ab,长度是2,所以移动的距离是 j - b = 5 - 2 = 3.
在这个过程中,每个模式串的前缀的最长的border值都可以得到。然后在查找的过程中,移动距离就可以根据匹配的前缀计算出来了。

过程

理论: 让r, s作为串 x 的border,并且满足|r| < |s|,那么r一定是s的border.
证明:下图 展示了r,s作为两个border 的串 x 。r是x的前缀,也是真前缀,因为它比s短。而r也是x的后缀,因此,也是s的真后缀。所以r是串s的border。
                                             Borders r, s of a string x
定义:
让 x 作为一个串, a 作为一个字母,如果ra是xa的border,那么x的border就可以从 r 扩展到 ra(过程算法的理论依据哦~称为T1.1)。
下图展示了这个过程:
                                                                            Extension of a border

在这个过程中,一个m+1为长度的数组会被计算出来。b[i]表示模式串(i = 0, ..., m)中长度为i的前缀的最长border的长度。由于空的前缀没有border,所以我们将b[0] 置为-1。 下图是拥有长度为b[i]的border的模式串的前缀。

                                                                          Prefix of length i of the pattern with border of width b[i]

假设b[0], ..., b[i] 的值都是已知的,b[i+1] 的值可以通过检查前缀p0 ... pi-1 的border是否可以由pi扩展。这就是 pb[i] = pi 的情况,如 上图。那么borders就可以通过前面 b[i], b[b[i]] 等等的值计算出来。
这个过程算法包含一个 j 的循环。如果pj = pi 的话,长度为 j 的border又可以被 pi 扩展,否则pj <> pi的话,下一个最长的border 就能通过让 j = b[j] 获得。这个循环结束的条件是没有border可以被扩展(j = -1)。 在 j++语句之后,这个值就被写入b[i+1]。
(也就是说,根据T1.1,如果pj = pi,扩展后b[i+1] 的值就是b[i] 的值+1即可;但如果不相等的话,令j = b[j] ,再判断新的 pj 与 pi 是否相等,依此类推,也就是程序中的while循环!因为不相等时,border的长度肯定更短,而b[j] 就是前缀p0, p1,...,pj-1的border,因为前后部分一样,下面在此判断pi 和 pj 是否相等又是在重复前面的过程,如果相等还是根据T1.1加一得到b[i+1]的值,这点不太好想,有点递归的赶脚。。)
过程算法:
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;
    }
}

例如:
对于模式串  p = ababaa,数组b中的border的长度有下面的值。比如我们有 b[5] = 3,因为长度为5的前缀 ababa 拥有长度为3的border.                                                                                                                                                                               

查找算法

上面的过程算法可以被应用到串pt 上来。如果只有最长的borders被计算出来,那么pt 的前缀的长度为m的border会匹配模式串 t ,假设border不是自我重叠的,如下图所示:
                                                                                Border of length m of a prefix x of pt
这解释了过程算法和下面的查找算法的相似之处。
查找算法:
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];
        }
    }
}
在内层循环中,在 j 处不匹配的时候,匹配模式串的前缀中最长的border会被考虑,如下图:
                                                        Shift of the pattern when a mismatch at position j occurs
继续在b[j]处比较,border的长度会导致模式串的移动。如果发生不匹配的情况,考虑下一个最长border, 以此类推,直到没有剩下的border,即j = -1 或者下一个符号匹配了。然后我们就有了新的模式串的匹配前缀,可以继续进行外层循环了。

如果模式串的所有m个符号都和主串匹配完毕,即j = m,会调用一个函数来返回匹配的位置i-j。然后,模式串会在最长border允许的范围内尽可能多的移动。

下面的例子演示了查找算法,绿色代表匹配,红色代表不匹配。

例子:

                                                                                  

分析

过程算法内层的 while 循环将 j 的值减少到至少为1,因为b[j] < j. 循环在 j = -1的时候停止,因此,它最多是和 j++ 的次数相等。由于j++ 在外层循环中执行了m次,所以内层循环的整体次数最多是m,所以过程算法需要O(m) 步。
相似的查找算法需要O(n)步。上面的例子展示了这个“阶梯状的图”的宽度至少和高度相等(因为每次不匹配都会移动,最极端的情况是每一次都移动1,此时宽度和高度相同),因为最多有2n次比较。
所以KMP算法的复杂度为 O ( n ).

其他模式值

其实next 函数也有其他的定义,但思想都是利用前面的比较来消除回溯提高效率,参考链接 : http://www.cppblog.com/oosky/archive/2006/07/06/9486.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值