本文主要参考了https://mp.weixin.qq.com/s/rbaPmBejID8-rYui35Snrg的表述,加上部分自己的理解
学习任何算法都要了解该算法解决什么问题?我们看看KMP算法主要解决什么问题。我们举一个例子,已知字符串1(ABCABBABAABBA)中查找字符串2(ABCAAB)是否存在,如果存在,字符串2在字符串1中的起始位置是多少?
对于上面这个列子,我们很快就可以回答出来字符串2(ABCAAB)在字符串1中存在,起始位置为5(假设从0开始)
暴力匹配:
不考虑任何效率的问题,当然就是从头到尾比较过去了,比如:
ABCABBABAABBA
ABCAAB→
比较下来,发现从第4个字符开始不同了,那没办法,说明前5个字符不是我们想要的字串,接下来我们就让字符串往前拱一步,继续比较:
ABCABBABAABBA
ABCAAB→
这一轮更惨,第一个就栽了,没关系,继续死磕往前拱:
ABABCBABAABBA
ABCAAB→
这一轮也不咋滴,比较到第3个就歇菜了。只要我们有毅力,这个过程坚持不懈,总能找到(或者找不到)答案。
暴力求解算法的复杂度为:假设长字符串为n,短字符串为m,在最极端的情况下(只有最后m个字符才互相匹配)我们需要走n-m轮,每一轮需要匹配m次,时间复杂度为O(nm)
现在的问题是:有没有更快的方法来找到匹配的字串呢?
KMP算法
KMP算法的核心意思就是:当我们发现一次比较下来字串没有完全匹配的情况下,下一次的比较也许可以不止往前拱一步,也许可以拱N步,关键是,究竟可以拱几步呢?
搞清楚这个问题前,先来搞清楚一个关键信息:“部分匹配值”,官方的解释太啰嗦了,我掰开了揉碎了讲给你听,就是这样:将我们要匹配的字串写出来,写两遍:
ABCAAB→
←ABCAAB
看到了吧,我们让他们一个向左一个向右,相向而行。算一下上一行的右边跟下一行的左边,最多能有几个字符是重合的(注意上一行的首字符不参与这个游戏,否则每个字串都是从头配到尾,就没意思了),这个数值N就是字串ABAAB的所谓部分匹配值,当然,你会发现此时N等于2,因为:
后缀
ABCAAB→
←ABCAAB
前缀
下边的字串再往前走,再也找不到更多的重合的字符了,因此字串"ABAAB"的部分匹配值为2,记为:
ABCAAB
2
来,继续算部分匹配值,接下来缩短一点,将最右边的B去掉,来看ABAA的部分匹配值:
ABCAA→
←ABCAA
显而易见,字串"ABAA"的部分匹配值是1,记为:
ABCAAB
1 2
不断重复上述过程,将每一个字串的“部分匹配值”都算出来,就是所谓的部分匹配表,如下所示。
ABCAAB
00 01 12
好了,花开两朵各表一枝,说回刚刚提到的问题:发现不匹配之后,究竟要向右拱几步呢?答案是:可以往前拱 (N-x) 步。N指的是匹配的字符数,x是部分匹配值,现在我们再把刚开始的步骤再做一遍:
ABCABBABAABBA
ABCAAB→
比较下来发现有3个字符匹配,此时N=4,查表得知ABCA的部分匹配值x=1,因此我们此时可以往前拱 (4-1)步,接下来比较:
ABCABBABAABBA
ABCAAB→
我想大家心中一定有这样一个疑问,为什么可以这样子移动,这样子移动不会漏掉匹配数据吗?
上面一行的红色字体ABCA的最后一个A其实就是ABCA的最长后缀
ABCABBABAABBA
ABCAAB→
下面一行的红色字体的ABCA的第一个字符A其实就是最长前缀
我们移动的目的就是要让最长后缀(A)与最长的前缀(A)重合,也就是让两头的部分匹配的区段重合在一起
为什么可以这样子移动?
因为我们知道ABCA前后缀匹配部分只有A,也就是说ABCABBABAABBA的前缀AB与ABCA的后缀CA是不匹配的。
如果我们把小字符串ABCAAB中的A移动到与ABCABBABAABBA中A前面任何一个位置,比如C位置得到的上下字符一定不匹配,因为它们公共部分只有A
ABCABBABAABBA
ABCAAB
你发现,此时我们就跳过了一些比较循环,让我们整个算法的效率得到极大的提升。其实KMP算法的核心,就是充分利用比较过的未能匹配的字串的信息,而不是一股脑将他们丢弃。