KMP字符串匹配算法
KMP算法,Knuth-Morris-Pratt Algorithm,一种由Knuth(D.E.Knuth)、Morris(J.H.Morris)和Pratt(V.R.Pratt)三人提出的一种快速模式匹配算法。
KMP朴素算法
原理:子串pattern依次与目标串target中的字符比较,如果相等,继续比较下一个字符;如果不等,pattern右移一位,重新开始比较,直至匹配正确或超出target。
示例:子串pattern={aabaa},目标串target={aababaacaabaa},比较过程如下图:
特点:思路简单、代码直观;但效率低、有回溯、不够简洁、时间复杂度高
小结:在最坏的情况下,每次比较都在最后一个字符出现不等(如aaaaaaaaaaaaab和ab)
假设pattern长度为m,target长度为n,则每趟最多比较m次,最多循环比较(n-m)趟,总比较次数为m*(n-m),即时间复杂度为O(m*n)
KMP算法的演变
我们由上面KMP朴素算法的例子来引出一个问题。
为了便于问题分析,令P(pattern),T(target),字符数组下标从0开始。通过仔细分析,发现P(Pattern)前4个字符是匹配的,只有最后一个字符P[4]不匹配!
如果P右移1位,P前两字符aa又将与T(target)的ab不匹配
如果P右移2位,P第一个字符a就与T的b不匹配
如果P右移3位,P前两字符aa又将于T的ab不匹配(同右移1位的情况)
如果P右移4位,P第一个字符a就与T的b不匹配(同右移2位的情况)
如果P右移5位,即P跨过已经与T比较过的五位了,省去了右移1、2、3、4位的步骤
为什么是5位呢?我们再深入分析,转换思考问题的侧重点,发现5位字符正好是P(Pattern)子串的长度,是不是P子串本身就蕴含了模式匹配的奥秘?
答案是肯定的!
P: aabaa(X) 注意:最后一个字符不匹配,即a(X)
上图直观给出,P要么右移3位,要么右移5位,才有可能与T(target)出现匹配。
我们探索P本身的规律,发现P(aabaa)移位的大小,与其自身的首尾覆盖特性有关,即aa—b—aa(移3位跳过b字符,移5位跳过自身,从头开始比较)
于是我们引出了另外一个问题——覆盖函数
什么是覆盖函数呢?我们直接给出定义:
对于序列
找出这样一个k,使其满足
并且要求k尽可能的大!(原因后面再讲)
求P自身最大的k值,对于P(pattern)的前j序列字符(从下标0计起),有两种可能:
1、 pattern[j] == pattern[preOverlay+1] 时,overlay(j) = preOverlay + 1 = overlay(j-1) + 1
2、 pattern[j] != pattern[preOverlay+1] 时,overlay(j)需要在前preOverlay中找;使preOverlay = overlay[preOverlay],重复2过程
示例:KMP算法
KMP算法,是由KMP朴素算法演变而来的,主要分为两步:
第一步,当字符串比较出现不等时,确定下一趟比较前,应该将子串pattern右移多少个字符(预处理)
第二步,子串pattern右移后,应该从哪个字符开始和目标串target中刚才比较时不等的那个字符继续开始比较(查找)
下面给出完整的KMP算法:
测试示例:
pattern: aabaa
target: aababaacaabaa
运行结果:
总结:
第一步,其实就是KMP朴素算法对模式匹配子串pattern的预处理过程,上面已经给出了算法公式和代码示例
第二步,本质上就是KMP朴素算法,不同的仅仅是pattern右移的位数大小由其预处理过程决定
KMP算法不太容易理解,但其简洁、高效,时间复杂度为O(m+n)其中,O(m)是pattern子串预处理的时间复杂度,O(n)是target目标串查找的时间复杂度,总时间复杂度为O(m+n)
参考推荐:
KMP(百度百科)
Knuth-Morris-Pratt algorithm(Wikipedia)
Knuth-Morris-Pratt algorithm(String Matching)
Knuth-Morris-Pratt string matching