基于dfa的kmp算法思想

1.kmp算法的由来及其思想

        在一段字符串中查找指定的子串时(例如在一段由A,B,C字符组成的字符串中查找ABABAC),我们通常的做法是将模式字符串(在本例中即ABABAC)的开头字符与被查找字符串一个一个进行比较,当匹配成功时比较模式字符串的下一个字符和被查找字符串的下一个字符,当匹配失败时则回退模式串指针到第一个字符,以及回退文本指针到比较开始的下一个字符。具体情况在这里不需要作过多的介绍。需要注意的是:我们并不需要在每一次匹配失败后都完全回退模式串指针和部分回退文本指针。例如以下情况:当a与c比较后发生失配后,我们观察发现,其实并不需要像第二个图一样回退模式指针和文本指针,在继续向后移动文本指针的基础上,我们利用已知的匹配结果直接将文本中的a和模式串中的第一个a对齐(因为在比较过程中,模式串了解已匹配成功的字符情况),如第三个图。这也是kmp算法的核心思想:当发生匹配失败的时候,利用前几次匹配后的结果,我们总能在不回退文本指针的情况下,通过回退模式串指针,使得两者重新在新位置下成功匹配。

2.dfa状态机以及其引进的原因介绍

        在上例中,我们说利用已知的匹配信息可以使得文本指针不被回退。具体做法是:将文本字符串中的已匹配字符去头,并将模式串与去头子串(包括去头的且与模式串匹配成功的部分模式串和匹配位置失败的字符c)进行匹配,不断右移模式串,成功匹配的位置即是下一次比较中模式串应存的位置。但是有个问题是:通过这种做法,我们确实并不需要回退文本指针了,但是如果每一次匹配失败都将模式串与其子串进行匹配的话,又会有额外的时间支出。为了应对这个问题,我们引入了dfa状态机这个概念。dfa状态机是个什么东西呢?由于每次匹配失败后,都是将模式串与其子串进行比较,所以这个比较的结果可以存储起来,当在下次发现匹配失败且失败原因与之前相同的时候,就可以利用之前匹配失败后对应模式串的行为,而不用重新将模式串与其子串进行比较,造成不必要的开销。所以,此时我们引入了dfa状态机的概念,用来记录各状态(模式串与被匹配文本相等字符的个数)下的转移。dfa状态机的构造如下所示:

  

看不懂不用着急,我们会一步一步介绍并最终构造出dfa状态机。在图中,每一个圆圈各自代表一种状态,由于模式串有6个字符,故所对应有0-6共7种状态(0状态是针对没有一个模式串字符与文本匹配的情况来说的)。而图中的每一个带有箭头的有向边则是在该状态下遇到不同字符情况下对应的转移(例如对于状态2来说,也即模式串中A,B字符与文本匹配的情况下,当下一个文本字符为A时,我们就进入3状态——模式串有3个字符与文本匹配。但是,当下一个文本字符为B,C的时候,模式串会回退指针到0状态,即模式串指针重新指向开头)。现在,假设我们已经完成了dfa状态机的构造(具体构造方法在下面说明),我们程序的任务就是一个模拟运行状态机的过程。同样以模式串A B A B A C为例,当文本中出现B,C字符时,根据状态机可得模式串还停留在0状态。故下次进行匹配时模式串指针指向第一个字符。而当文本中出现A字符时,模式串转移至1状态,故下次模式串指针应该指向第二个字符。如此循环进行上述步骤,有两种方式可以使循环停止:①dfa状态最终转移至6(6个字符全部与文本某处匹配)②文本字符指针指向了最后一个字符(没有找到完全与模式串字符匹配的文本字符串)。这对应了查找的两种结果。


3.dfa状态机的构造

      我们将用三种方法对dfa状态机来进行构造,从对应实际的简单但耗时方法到抽象但效率高的方法。这是一个抽象但提升效率的过程。

      ①由于第n个状态下的转移,所代表的实际意义是:有n个模式串字符与文本匹配时,出现下一个字符后,模式串与文本重新又有多少个字符匹配。通过确定重新有多少个字符与模式串匹配,可以得知模式串指针的下一个位置以及dfa状态机所对应的新状态。从实际出发,对于0状态来说,有0个模式串字符与文本匹配,这时仅需要将任意一个字符与模式串字符的第一个字符进行比较,如果匹配则进入状态1(有1个字符匹配),否则重新回到状态0。其实,0状态的构建对于其它状态来说是独立的。对于1状态,此时有一个字符匹配,对于匹配的情况下,依然是进入下一个状态。但是对于不匹配的情况,不能简单地像对待0状态一样回到0。在字符串的暴力查找中,我们对应的操作是将模式串指针退到0后对齐下一个文本字符。这里其实类似,我们将模式字符串与去头的部分模式字符串(包括去头的匹配成功字符串和匹配失败的字符)进行比较,这里两者的重合字符数即下一个应该存在的状态。之后状态的构造与1的状态构造一样。

      ②这里介绍一种更简单的方法。状态0的构造与方法①类似,但对于之后状态的构造,我们不用拿着模式串与其多个子串进行一一比较。例如,对于状态1,如果匹配,直接进入状态2。如果不匹配,我们只需要将那部分的模式字符串(在这里是匹配失败的那个字符,因为1状态去头后为空)放在已经构造的状态机里面去跑一遍,便可以得到该状态下不同字符的转移。对于部分模式字符串有多个字符时,我们只需要将第一个字符放进去跑一遍,第二个字符接着第一个停留的状态继续跑,就可以得到该状态下的转移。(因为实际上是模式串与其自身匹配的过程)

      ③对于方法2,我们还可以继续进行优化。对于构造每一个状态的时候,我们并不需要将去头字符串的全部字符放在状态机里面跑一遍。我们可以记录之前状态下的转移。这样,对于第n个状态下的转移,就是第n-1个状态下转移的转移。举个例子,由于状态1去头后为空,所以它的转移其实是状态0的转移。由此可得,状态1下不匹配的转移和0状态对应字符所到达的状态一样。在构造状态2时,由于确定了下一个字符是B,我们把B放在状态1的转移0状态下跑一下,发现状态2的转移还是0状态,所以B状态的不匹配转移还是0状态下的转移。由此归纳,状态三确定了字符A后,其转移是状态1的转移。所以,我们仅需要在每次构建状态时,用新的所确定的字符放在之前所记录的转移中跑一下,即可得到该状态的转移应该是哪个对应状态的转移。


4.注意

      该篇博客只讲基于dfa的kmp算法思想,具体代码可以参考《算法》第四版下的kmp算法



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值