注:本文只讲解KMP怎么工作,不讲解KMP为什么这样工作。
KMP的介绍我不再进行赘述,现在就进入正题吧:
现在有以下两个字符串,我们想在文本串中找到与模式串一摸一样的子串。
在说到KMP如何工作之前,我们先了解一个东西:
最长公共前后缀
KMP算法实际上并不怎么关心文本串,我们所说的最长公共前后缀是特别针对模式串来说的,我们将上图所示的模式串分为以下子串:
A | ||||
A | B | |||
A | B | A | ||
A | B | A | B | |
A | B | A | B | C |
我们需要注意的是,在找最长公共前后缀的个数时,这个值必须小于当前子串的长度;
1.我们看第一个子串A,显然在这个串中没有与A一样的前后缀,同时A和A本身不能够算作公共前后缀(原因在前方标红了),所以这个子串的最长公共前后缀为0;
2.“AB”,很显然,仍然为0;
3.“ABA”,前一个A与后一个A相同,所以为1;
4“ABAB”,前面的AB,后面的AB(我们要找最长的,所以即使前A后A也相同我们也不要),为2;
5.“ABABC”,为0.
好了,于是我们得到了一个表格:
0 | 0 | 1 | 2 | 0 |
关于两种next数组的形态
在KMP算法中,next数组是非常重要的,我们在比对的时候是要根据next数组决定指针该怎么移动。
在我观看不同的教程中发现,next数组形态主要有两种(不要怕,不难,接下来一起看看)
形态一:直接沿用上面的前缀表
上面由模式串中得出的一张表,我们直接作为next数组:
模式串 | A | B | A | B | C |
next数组 | 0 | 0 | 1 | 2 | 0 |
当我们比对文本串和模式串的时候,我们发现模式串最后一个字符匹配不上:
此时KMP算法会找到C对应的next数组值的前一个值——2,接下来,模式串将会发生这样的变化:
刚好,我们的模式串向后移动了两个位置,再度开始匹配。
我想提醒大家的一点是:实际上我们的字符串不会像这样移动,上述只是便于理解,真正起到作用的是指针,是指针的移动营造出了字符串移动的“假象”。
我们在文本串中给一个指针,模式串给一个指针,两个指针同时开始遍历,当文本串、模式串的指针遍历到上图位置时会发现此时比对的字符串不同,于是指针i查找next数组,跳到了下标为2的位置。
大家应当知道的是,我们需要比对的是两个指针所指向的和这之后的字符,而非之前的。
形态二:去尾添首
我们接着来说next数组的第二种形态。还记得之前的前后缀表吗?我们把这张表拿过来,将这张表的最后一个值去掉,在表的开头添上-1,于是我们得到:
模式串 | A | B | A | B | C |
next数组 | -1 | 0 | 0 | 1 | 2 |
下标 | 0 | 1 | 2 | 3 | 4 |
OK,接下来的事情也是一样的;当指针遍历到(模式串)最后一个字符时,发现不匹配于是查找数组,发现对应数值是2,于是把下标为2的字符移动到当前位置——实际上仍然是指针跳到下标为2的字符上。
看完后你可能会产生的一些疑惑
如果模式串是类似于“AAA...”这样的格式,该如何找出最长公共前后缀呢?
其实也是一样的,如下图所示:
A | 0 | ||
A | A | 1 | |
A | A | A | 2 |
如果还是有点懵圈的小伙伴可以自己找一下。
当使用形态的二的next数组时,我们找到的数值是-1怎么办?
如果是在考试答题中,我相信大家都会自然而然的去画示意图,自然而然的去向后移动一位。
但是如果自己写代码的话,碰到0和-1有什么不一样呢?
当碰到0的时候,你肯定知道是把模式串中的指针移到0;那么仔细想想,如果碰到-1你还是只挪动模式串的指针(而且这时候指针肯定指向首元素),有什么意义呢?
这时候我们挪动的当然是文本串的指针,将其向后移动一位。
相信大家如果看懂这篇文章,一定能自己写出指针是如何移动的代码;当然,next数组是如何生成的,代码如何编写,就靠各位的智慧了。