KMP算法

一篇关于基本的kmp算法学习博客

KMP算法

引入

  在介绍主体内容之前,我们先来考虑对一个字符串的匹配:匹配’AAAAAAB’中的’AAB’
  那么朴素算法思想很简单也很暴力:逐个匹配
  那么匹配方式就是:从第一个开始A与A匹配,A与A匹配,然后A与B不匹配,所以前三个不是,于是从第二个开始,A与A匹配&……
  所以简单来说就是每个下标为一次起点,一轮一轮地比较,最糟糕的情况下,时间复杂度可以来到O(nm),其中n是目标字符串的长度,m是要找的字符串(此后称为模式串)
  毕竟比如类似上面的那样,只有最后一个不同,而且匹配模式串的部分还在末尾,相当于两个字符串中的任意两各字符都要两两比较
  这就表示如果n与m都不小,极有可能光是查找就会超时
  因而这一次介绍的就是KMP算法,一种可以把时间复杂度降低到O(n)的强大算法

基本内容与作用

找到一个合适的查询方式

  KMP算法的核心是尽可能降低目标串中字符与模式串内字符比较的次数
  现在我们依然假设目标串是AAAAAAB,而模式串依然是AAB
  从刚才的例子来看,我们在第三个位置之前的查找其实都是无效的,毕竟它们的样式十分相似,何不试试跳过这样的无效状况
  那么假设我们在第三个位置失配时,直接把下一次匹配的起点调到第三个,在这种情况下我们就跳过了一次匹配,而如果模式串更长一些还能跳过更多
  但是这显然是有很大漏洞的,假如说目标串是AAAAABBAA,而模式串是AAABBAA,这时按照上一个粗暴的跳过方式来运行就会导致第二次匹配的起点变为了第四个,这样一来,本来能接上的那处B现在反而接不上了,然后这下彻底找不到匹配的串了
  因为这个字符串在后面虽然发生了失配,但是其实只是有两个字符长度的错位,那么现在就非常纠结,如果想要避免错位的情况被忽略,就得顺序找过去,而我们也知道这样效率低下
  现在问题变成了一个左右为难的状况
  于是我们现在开始思考,刚才所作的行为是不是从事实上就是找一个恰当的起始位置,就像刚才的例子,如果我们可以刚好发现下一个恰当的起始位置是第三个字符,我们就可以略过两次无效查找,而快速找到匹配的部分了
  那么这是从左往右捋的思考方式,换个角度,我们从失配处来想这意味着什么:
  找到一个合适的起点,意味着我们从这个起点处开始匹配可以让匹配到失配处时刚好可以接上并继续匹配,换句话说,其实就是从失配处开始往前扫,看看能不能扫出来一个子串刚好能匹配上模式串前缀部分,前缀匹配上了,失配处就能继续匹配试试看了
  出于节约时间的目的,这个查找自然不能是在线查找,否则同样是极度耗时的行为
  所以我们考虑离线查询的方式,于是考虑用一个数组存储一下待查询的信息
  那么这么想我们恐怕不适合把离线查询的信息和目标串进行对应,毕竟如果这种内容都完成了,那其实目标串约莫都被摸透了
  那么这么看下来,目标就很明确了:建立一个数组实现对模式串匹配的离线查找
  然后我们再来考虑,查找到的内容所给出的信息应该覆盖掉尽可能全面的内容,否则可能就会造成合适的起点被略过的状况
  再综合一下前面提到的,可以把起点的选择转换为已匹配的前缀字符串,所以离线查找得到的信息就可以被表示为:匹配到模式串的当前位置时,不包含当前匹配状况(因为已经发生失配了,当然不是目前这个状况),已匹配到的前缀字符串最长的情况

利用查询并执行匹配

  这样我们就可以利用这个信息直接获取接下来应该和谁匹配,比如排除掉目前情况后,匹配到模式串这个字符的时候,已经匹配的前缀字符串最长可能长度为5,我们所失配的这个字符就可以直接与模式串的第六个进行比较
  相当于执行了一次回滚,这样就跳过了部分无效可能,又避免了错位状况的忽略
  然后如果这次匹配又失配了,就继续看,匹配到第五个字符时,去掉当前情况,最长可能是什么,然后又进行一次回滚直到匹配或完全不匹配
  这就相当于考虑了从最开始的位置到目前失配位置之间,所有可能的错位状况全部被试错了一次,但如果提前找到了合适的起点就从合适起点处继续,如果完全匹配就结束,如果依然不完全匹配那么又进行下一轮查找
  值得注意的是,在这个匹配方式下,事实上只有失配处可能发生了多次匹配(同时也意味着被匹配过的部分不会再一次被关注),其他位置是不需要真正执行匹配的,因为我们相当于已经知道了前面部分被匹配到了
  对于这个与模式串一一绑定的数组,在KMP中被称之为next数组

原理与细节

next数组

关于匹配

  上文中提到在利用next数组实现回滚到过程中,如果有多个适配的前缀,在遇到第一次可继续匹配的长度后就结束回滚
  这也意味着可能会有比较短的前缀所对应的起点虽然也符合但是会被忽略掉
  那么这会不会导致匹配时状况的遗漏呢?
  我们考虑两种可能,第一是:原本被遗漏的在后来的失配中刚好被又一次用上了,那么这就没有漏,当然,值得注意的是这种情况不会是以一模一样的前缀回滚,而仅仅是起点相同,毕竟,这种情况的失配处必然不会是原来的失配位置,这也是KMP的特点
  而第二种情况比较重要:这个可能的起点由于后面遇到的失配点位所对应的next数组根本不存在包含此起点的前缀
  那么这种无视为什么合理呢?
  当我们在新的失配点调用next数组回滚时,其实可以理解为我们的目光放在了失配处的前一个符号,我们知道想要把这里及其前方的一段字符串和模式串前缀匹配在一起可以有哪些选择,而不在这些选择的就相当于我们已经知道从那里过来一定会被前面的字符挡住
  所以那些"被遗漏"的起点既然不在候选名单,就意味着以我们当前视角来看已经明白那里过来必定失配了

关于空值

  本来,next数组的默认空值是-1,因为下标的初始位置从零开始,然后

模式串与文本串的匹配

模式串本身next数组的获取

完整代码

搭配状态栈实现的批量查找删除