【算法学习笔记】KMP算法之构造回溯表(backtrack table)的思路和技巧

      【备注】本文为KMP算法的学习笔记,重点说明KMP算法中最关键的回溯表的构造思路。若对KMP算法涉及到的基础概念(如串的proper suffix, proper prefix等名词)不熟悉,建议先参照阅读本文最后部分给出的参考文献。

        Knuth–Morris–Pratt string searching algorithm (简称KMP算法)是一种高效的子串查找算法,

        提到子串查找,一种最简单易实现的方法是从源串第1个字符开始,与目标串进行逐次匹配,匹配思路如下所示(串S为长度为n的源串,W为长度为k的目标串,我们要实现的是在S中查找W,若查找成功,则返回其首次匹配成功的子串的首字符位置): 

for(i = 0; S[i] != '\0'; ++i) 
{
    for(j = 0; S[i+j] != '\0' && W[j] != '\0' && S[i+j] == W[j]; j++);
    if (W[j] == '\0') { 
        // found a match
        return i;
    } 
    else {
        // match failed
        return -1;
    }
}
          上述方法优点是实现起来简单,缺点是时间复杂度高,最坏情况下的复杂度为O(nm)。

       不过在实际使用中,通常在逐次匹配(上述代码第2个for循环所示的过程)的早期就会出现dismatch,从而可以及时回溯进行下次匹配,故这种实现方法效果通常也不会很差,因此较为常用,c的库函数strstr()就是采用这种思路实现的(strstr()的一种实现方法)。

       为方便描述,本文约定:S[0, n-1]表示长度为n的源串,W[0, k-1]表示长度为k的目标串。

       假设串W的第j个字符W[j-1]与串S的第(i+j-1)个字符S[i+j-1]不相等引起本次匹配失败,这意味着在匹配失败的这个字符之前,串S与串W的对应字符均相等,即S[i, i+j-2]与W[0, j-2]逐字匹配成功。每一次的匹配过程均可用得到类似的、比较隐晦的“历史信息”。

       上面的代码中,每次出现dismatch后,外循环的index+1,内循环的index清零,重新开始匹配。可见,该方法并没有充分利用上次匹配失败过程中得到”历史信息“,这是导致其复杂度较高的根本原因。

       KMP查找算法正是充分利用了这些匹配失败时的“额外信息”,对回溯过程进行了优化,从而将最坏情况下的时间复杂度降低到了O(k+n),其中k为目标串W的长度,n为源串S的长度。若k远小于n,则KMP算法几乎是在线性时间内完成查找,效率提高的相当明显。

       为尽快引入本文的重点(回溯表的构造方法),我们将wikipedia中给出的KMP算法伪码(文末参考文献里有链接)摘出如下:

algorithm kmp_search:
    input:
        an array of characters, S (the text to be searched)
        an array of characters, W (the word sought)
    output:
        an integer (the zero-based position in S at which W is found)

    define variables:
        an integer, m ← 0 (the beginning of the current match in S)
        an integer, i ← 0 (the position of the current character in W)
        an array of integers, T (the table, computed elsewhere)

    while m+i is less than the length of S, do:
        if W[i] = S[m + i],
            if i equals the (length of W)-1,
                return m
            let i ← i + 1
        otherwise,
            let m ← m + i - T[i],
            if T[i] is greater than -1,
                let i ← T[i]
            else
                let i ← 0
            
    (if we reach here, we have searched all of S unsuccessfully)
    return the length of S
        上述伪码中,标红且加粗的部分是KMP算法的关键,其对匹配失败时的回溯过程做了优化:不是简单的将外循环变量m+1且对内循环变量清零( m = m + 1, i = 0,这是传统查找过程的做法),而是借助回溯表T,将外循环变量m更新为(m + i - T[i]),其中m+i为引起dismatch的字符在源串S中的index,同时,根据T[i]的值对内循环变量i做更新

       对这两个循环变量的优化是KMP算法的精髓,优化过程中用到的回溯表T则是关键中的关键。

       假设目标串W长度为k,则回溯表T是由k个元素构成的数组,其中T[i]决定了源串S的第(m+i)个字符与目标串W的第i个字符匹配失败时,两个串的回溯步骤(从伪码中两个循环变量m, i的更新均用到T[i]可以得到这一结论)。

       我们从很多介绍性文章中都可以看到类似的描述:只要给出目标串W,就可以构造出回溯表T,与源串S无关。

       相信不少初学KMP算法的童鞋会有疑问:既然T的构造与S无关,那为啥S可以根据T做回溯?为神马这么神奇?反正我当时就有这样的疑问。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值