《数据结构》:KMP算法

注意:本文首发于我的博客,如需转载,请注明出处!

问题

  给定一个字符串 A A A,要求从字符串 A A A中查找另一个字符串 B B B是否出现。如果出现,返回查找的字符串 B B B A A A中的位置,否则返回-1。

最简单的解决方法——枚举法

  枚举法当然可以解决这个问题:我们可以在从 A A A字符串中取第一位,然后逐位往后与 B B B串比较。如果匹配则返回答案;若不匹配,则从 A A A字符串的第二位开始,重复上述操作,直到找到字符串位置或者字符串 A A A被遍历完毕。

  我们来分析一下这个的算法复杂度。我们假设字符串 A A A B B B的长度分别为 m m m n n n,那么这个算法的最差情况需要比较 m × n m\times n m×n次,即这个算法是 O ( m × n ) O(m\times n) O(m×n)的。

KMP算法

  KMP算法是一个很好的解决算法,它可以 最多只扫描一次字符串 A A A 就能完成任务,即它的算法复杂度是 O ( n ) O(n) O(n)的。之所以被叫作KMP算法,是因为它是由Knuth、Morris、Pratt三个人提出的。下面来介绍一下该算法的思想。

简单的直观模拟

  为了说明KMP的大致过程,我们先给出一个例子。

直观模拟

  我们参考上例,并且称大数组为 c h ch ch,目标数组为 t a r g e t target target。我们从第一位开始逐个比较 c h ch ch t a r g e t target target中的每一位,直到不匹配为止,如图(a)所示。我们发现, c h [ 6 ] ! = t a r g e t [ 6 ] ch[6]!=target[6] ch[6]!=target[6]。但是在 i ⩽ 5 i\leqslant 5 i5都有 c h [ i ] = t a r g e t [ i ] ch[i]=target[i] ch[i]=target[i]。我们此时不将 t a r g e t target target往后移动一位,选择往后移动两位,因为**这个时候 c h [ 2 ] ch[2] ch[2] c h [ 6 ] ch[6] ch[6]仍然和目标字符串是匹配的。**我们可以证明,这个位置是除了初始位置外第二个可能的匹配发生的位置。因为如果在此之前还存在一个匹配的字符串,那么我们要么已经找到了它,要么重定位会定位到它,而不是 c h [ 2 ] ch[2] ch[2] 。如果你能理解这点,这就是KMP算法的基本原理。

失效函数

失效函数原理

  那么随之而来的问题是:我们如何确定应该向后移动几位呢? 以及,如果每次向后移动都可以看做字符串 t a r g e t target target正在比较元素的后退(正如上例中本在比较 t a r g e t [ 6 ] target[6] target[6] c h [ 6 ] ch[6] ch[6],向后移动后相当于比较 t a r g e t [ 4 ] target[4] target[4] c h [ 6 ] ch[6] ch[6] 那么到底后退几位呢? 如果我们把字符串中正在比较的元素位置记作 j j j,后退后正在比较 j ′ j^\prime j,那么我们可以知道它们满足: t a r g e t [ 0 ] target[0] target[0] t a r g e t [ j ′ ] target[j^\prime] target[j]之间的字符串和 t a r g e t [ j − j ′ ] target[j-j^\prime] target[jj] t a r g e t [ j ] target[j] target[j]之间的字符串完全相同。这里需要读者仔细理解一下,我们实际上取的 j ′ j^\prime j是使得 t a r g e t target target中前 j ′ + 1 j^\prime+1 j+1和后 j ′ + 1 j^\prime+1 j+1个字符完全相同,也就是我们在前一节说的直观模拟的结果。

  我们把 j j j j ′ j^\prime j的对应关系称为失效函数,即
j ′ = P [ j ] . j^\prime =P[j]. j=P[j].
有了这个对应关系,我们就可以轻松决定,当字符串正在比较位置 i , j i,j i,j相同,即 c h [ i ] = t a r g e t [ j ] ch[i]=target[j] ch[i]=target[j]时,但位置 i + 1 , j + 1 i+1,j+1 i+1,j+1不同,即 c h [ i + 1 ] ≠ t a r g e t [ j + 1 ] ch[i+1]\neq target[j+1] ch[i+1]=target[j+1]时, j j j需要更新成什么了。

失效函数的算法

  我们从上述论断中可以知道,失效函数的产生只和目标数组 t a r g e t target target有关,因此我们可以对目标数组预处理,得到其失效函数。那么,失效函数的算法是什么呢?

  首先,我们知道对于 t a r g e t [ 0 ] target[0] target[0],是没有满足失效函数定义的下标 j ′ j^\prime j的,那么,我们定义这种条件下,其失效函数值为-1。然后我们用递推的方式求出其他元素的失效函数。我们现在要求下标为 j j j的元素的失效函数,那么我们考察 j − 1 j-1 j1位置上的元素,其失效函数的值为 j 0 j_0 j0,那么也就是说, t a r g e t [ 0 ] target[0] target[0] t a r g e t [ j 0 ] target[j_0] target[j0]之间的字符串和 t a r g e t [ j − 1 − j 0 ] target[j-1-j_0] target[j1j0] t a r g e t [ j − 1 ] target[j-1] target[j1]之间的字符串完全相同,那么我们只要比较 t a r g e t [ j 0 + 1 ] target[j_0+1] target[j0+1] t a r g e t [ j ] target[j] target[j]是否相同,即比较 t a r g e t [ P [ j − 1 ] + 1 ] target[P[j-1]+1] target[P[j1]+1] t a r g e t [ j ] target[j] target[j]是否相同。如果也相同,那么 P [ j − 1 ] + 1 = P [ j ] P[j-1]+1=P[j] P[j1]+1=P[j];否则,我们可以令 P [ j 0 ] P[j_0] P[j0]是新的 j 0 j_0 j0,继续反复上次的比较,直到找到 t a r g e t [ 0 ] target[0] target[0]并且 t a r g e t [ 0 ] target[0] target[0] t a r g e t [ j ] target[j] target[j]也不相等,那么其失效函数的值为-1

失效函数的实现

  这节给出失效函数failurefunc()的实现。

int* failurefunc(const seqString& target)const{
    int* p=new int[target.len];//申请一个数组,存放失效函数
    int j;

    p[0]=-1;//首元素的失效函数值为-1
    for(int i=1;i<target.len;++i){
        j=i-1;//往前退一位

        while(j>=0&&target.data[p[j]+1]!=target.data[i]) j=p[j];//如果最长相同子序列不存在,那么缩短相同子序列长度
        if(j<0) p[i]=-1;//直到找完都没有找到,则失效函数为-1
        else p[i]=p[j]+1;//找到了,更新失效函数的值
    }//end for

    return p;
}//end function

查找函数

  有了上面的失效函数,我们可以完成KMP算法中的查找部分了。

查找函数的原理

  查找函数使用两个指针 i i i j j j,分别指向字符串 c h ch ch t a r g e t target target中正在比较的元素。从两字符串头开始,对 i , j i,j i,j一同自增,直到发现 c h [ i ] ! = t a r g e t [ j ] ch[i]!=target[j] ch[i]!=target[j]就停止;此时,利用失效函数,对 j j j的值进行更新,更新为其前一个元素失效函数的值后的元素,即 P [ j − 1 ] + 1 P[j-1]+1 P[j1]+1,然后重新比较 i i i j j j,重复上述步骤,直到 j = 0 j=0 j=0时还是无法匹配,则固定 j j j,对 i i i自增,直到匹配为止。搜索过程退出的条件是:找到了 t a r g e t target target的位置,即 j = t a r g e t . l e n − 1 j=target.len-1 j=target.len1;或者未找到,即 i = c h . l e n − 1 i=ch.len-1 i=ch.len1

查找函数的实现

  本节实现查找函数。

int find(const seqString & ch, const seqString & target){
    int* p=NULL;//存放失效函数
    p=failurefunc(target);//获取失效函数

    int i=j=0;//定位指针

    while(i<ch.len&&j<target.len){
        if(ch.data[i]==target.data[j]) {i++;j++;}//相等则自增
        else if(j==0) i++;//不相等但是j已经为零时,i自增
            else j=p[j-1]+1;//否则更新j的值 
    }//end while

    delete p;//释放内存

    if(j==target.len) //比较完最后一个字符后,j又自增了一次,所以应该是最后一个元素的下标+1
        return i-j;//如果找到了以后才退出,那么返回位置
    else
        return -1;//否则就是没找到,返回未找到
}

后记

  KMP算法的时间复杂度是 O ( n ) O(n) O(n)的,因为我们即便可能需要扫描目标字符串许多遍,但主字符串我们只需要扫描一遍,也就是说不会超过 n n n次。KMP算法为什么能节省时间,本质上使我们利用了扫描匹配过程中匹配失败的信息。对于匹配失败的节点,我们利用两个指针直接跳过了许多次扫描,因而节省了时间。可以说,KMP算法相比于枚举法有着更高的信息利用率。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值