字符串匹配
在了解kmp算之前,先来看一下最简单的字符串匹配算法。
BF算法
这个算法,比较暴力,也比较容易理解,不做过多描述,下面给出一张图,一般都能理解。
很简单,下面直接给出代码。
1 |
|
KMP算法
之所以先讲一个简单的方法,主要是让大家先了解一下这个算法是干嘛的,我认为kmp比较难的地方是他的next数组的计算方法,之前也写过一篇kmp相关的博客,但是自己回头看看讲的还是不够详细,这里再次完善一下。
很多书都是直接直接将的next数组的求解公式,直接给出来,但是即使理解了公式也不会写代码,很多都是靠死记硬背写出来的,所以我就想把这个过程详细的记录一下。
next数组
首先要明白,next数组它求的到底是个什么东西,下面简单举个例子。
a | b | a | c | a | b | a |
---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 |
比如我们要求解下标为3的位置上的next值,那我们需要求的就是:
a | b | a | c | a | b | a |
---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 |
下标为3的next值是通过计算它之前元素的最长公共前后缀得出的。
a | b | a |
---|---|---|
它的最长公共前后缀就是带颜色标记的,长度为1。(注:前缀和后缀不包含它本身)
下标为零的next值默认为-1。
下面在举一个比较通用的例子:
图1
前面的都已经求出来了,现在想要求下标11位置的next值,那我就可以根据10位置的next值求出来,蓝色框住的部分是已经求好的,而蓝色框后面的元素都是b所以,我可以在原有基础上加1,得出11位置的值为5。(注:求解最长公共前后缀的时候,前缀一定有第一个,后缀一定有最后一个)
为什么加一?
图2
这里蓝色块表示前一个位置的最长公共前后缀,而当我们多加了一个元素a的时候,在求最长公共前后缀的时候,就得比较新增j指向的元素和前缀的后一位,即i指向的元素,看他们两个是否相同,如果相同直接加一。
当然,不可能每次比较都会相同,现在我们来看一个不相同的例子。
图3
这个时候i指向的元素和j指向的元素就不相同了,这是我们因该怎么做呢?
请看下面这幅图:
图4
这幅图中,我们需要求的是下标为12的index值,我们需要求的就是m之前的最长公共前后缀,下面我们来看如何基于已有的值推出下一个位置的index值。
我们看到i和j指向的元素不同,这时我们需要对j的指向进行调整,j位置的next值为2,将j的指向移动到下标为2的位置。
图5
这时,发现i和j能够匹配上了。下面在简单解释一下为什么会这样,后面的部分也会用到。
图6
我用几种不同颜色把公共的部分标注出来。
现在我把图4中已经匹配好的以及即将要匹配的部分拿出来,即abkaby和abkabk这两个字符串,我们这次匹配是在abkab这个最长公共前后缀的基础上求的,但是在这个基础上我们发现他们匹配不上,这是我们需要找一个第二长的公共前后缀,我们找到了ab,然后i和j就变成了图6现在的指向了。(注:我们求的是最长公共前后缀,所以必须包含前缀的第一位和后缀的最后一位,剩下的就自己思考吧)。
这时,我们就可以把思路整理一下,然后开始撸代码了。
求next串的代码
上面有一种特殊情况没考虑,这里说明一下。
在第三次匹配的时候,它也没匹配到(第一位的next值为-1),所以下一次匹配他会匹配到找下标为-1的值,但是很显然-1这种情况就是没有公共前后缀,所以给他赋值为0。
1 |
|
主串和模式串的匹配代码
匹配过程,如下所示。
例子:
j | 0 | 1 | 2 | 3 |
---|---|---|---|---|
模式串 | a | a | a | b |
next[j] | -1 | 0 | 1 | 2 |
首先从头开始匹配,直到找到一个不匹配的字符。
j下标指向 | i | |||||||
---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
主串 | a | a | b | a | a | a | a | b |
模式串 | a | a | a | b | ||||
j下标指向 | j |
这时我们需要将将模式串往前回溯,但是回溯到哪里呢?这时就用到了我们的next数组。
j指向的位置的next的值表示它之前的最长公共前后缀,并且这里是到j位置才不能匹配上的,所以之前的都是相同的,那我们就可以把模式串的前缀匹配到主串的后缀(注:这里所说的前后缀表示的是已经匹配上的字符串),变成下面这种情况。
j下标指向 | i | |||||||
---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
主串 | a | a | b | a | a | a | a | b |
模式串 | a | a | a | b | ||||
j下标指向 | j |
可以看出,j之前的元素都已经匹配上了,这就是前后缀next值帮我们做到的,这样我们就不用像BF算法一样每次都从头开始匹配了,但是i和j指向的元素仍然没有匹配上,我们需要继续往前回溯,变成下面这样。
j下标指向 | i | |||||||
---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
主串 | a | a | b | a | a | a | a | b |
模式串 | a | a | a | b | ||||
j下标指向 | j |
这时依然没有匹配上,但是模式串已经到末尾了,所以将i往前移动一位。
j下标指向 | i | |||||||
---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
主串 | a | a | b | a | a | a | a | b |
模式串 | a | a | a | b | ||||
j下标指向 | j |
下面就同理了,不在赘述了,最终结果:
j下标指向 | i | |||||||
---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
主串 | a | a | b | a | a | a | a | b |
模式串 | a | a | a | b | ||||
j下标指向 | j |
1 |
|