在http://blog.csdn.net/u012613903/article/details/79004094中写到了如何手工去求一个NEXT数组,这个在很多考试中可以用来解题。但是在实际的使用中,NEXT数组究竟发挥着什么样的作用,如何用代码实现KMP算法呢?
KMP算法是用来确定一个串是否能在另一个串中找到与之完全匹配的子串,那么首先来看一个字符串匹配的实际例子;
被匹配的字符串:
a | b | c | d | a | b | e | f |
要匹配的字符串:
a | b | c | d | a | b | w |
下面开始进行匹配:(i代表被匹配的字符串的当前字符下标;j代表要匹配的字符串的当前字符下标。)
0 | 1 | 2 | 3 | 4 | 5 | i | |
a | b | c | d | a | b | e | f |
a | b | c | d | a | b | w | |
0 | 1 | 2 | 3 | 4 | 5 | j |
可以看到,在{e,w}的位置匹配失败了,如果使用暴力的方法做,那么下一次的匹配就变成了这个样子:
0 | i | 2 | 3 | 4 | 5 | 6 | 7 |
a | b | c | d | a | b | e | f |
a | b | c | d | a | b | w | |
j | 1 | 2 | 3 | 4 | 5 | 6 |
这时候 i = i-j+1;j=0;这就是暴力算法的思想:一旦匹配失败,就让要匹配的字符串从头开始与被匹配的字符串进行匹配。
我们来观察第一次匹配失败后的情况:
0 | 1 | 2 | 3 | 4 | 5 | i | |
a | b | c | d | a | b | e | f |
a | b | c | d | a | b | w | |
0 | 1 | 2 | 3 | 4 | 5 | j |
在要匹配的字符串的{w}位置,往前看,看到♦的a,b与实心♦的a,b就是我们要求的最长的相同前缀与后缀字符串
其实它们代表的实际意义是这样的:
在{e,w}位置无法进行匹配了,但是在w之前的要匹配的字符串的所有字符都与在e之前的被匹配的字符串的所有字符完全匹配了;(这是个前提标记为@1)
下面具体分析实际情况:
a | b | c | d | a | b | e | f |
a | b | c | d | a | b | w |
在{e,w}位置匹配失败了,我们不想使用暴力方法;想省力,那该怎么办呢?
要匹配的串中的w和被匹配串的e不匹配,进行下次匹配的时候,必然会有要匹配串中的w之前的某个字符取代w。也就是要将要匹配的串向右平移一个量(j+这个量<=i)。这里我们列出所有的平移情况:
(1)
a | b | c | d | a | b | e | f | ||||||||
a | b | c | d | a | b | w |
(2)
a | b | c | d | a | b | e | f | ||||||||
a | b | c | d | a | b | w |
(3)
a | b | c | d | a | b | e | f | ||||||||
a | b | c | d | a | b | w |
(4)
a | b | c | d | a | b | e | f | ||||||||
a | b | c | d | a | b | w |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(5)
a | b | c | d | a | b | e | f | ||||||||
a | b | c | d | a | b | w |
(6)
a | b | c | d | a | b | e | f | ||||||||
a | b | c | d | a | b | w |
上面就是所有的平移情况,情况(1),(2),(3)从第一个就不匹配;情况(4)从第三个不匹配;(5),(6)不需要讨论;可以看到,在(4)之前都是从第一个字符起两个串就不匹配,直到(4)才出现不是在第一个字符就不匹配的情况。然后再看(4)中的匹配上的字符串,居然是 a,b,是要匹配的 最长的相同前缀与后缀字符串。为什么会这样,这就用到了上面的前提@1 。
再回到最初不匹配的情形下:
a | b | c | d | a | b | e | f |
a | b | c | d | a | b | w |
正如前提@1所说,e,w之前两个字符串完全匹配,也就是完全相同。所以要求被匹配串e之前与要匹配串从0开始相同的最长子串串也就变成了在要匹配字串中求w之前与要匹配串中从0开始相同的最长子串
a | b | c | d | a | b | e | f |
a | b | c | d | a | b | w |
然后(1)->(2)->(3)->(4)的平移过程,其实就是使用暴力法的过程;KMP算法就是利用了这个特性,在某个位置发生不匹配的情况后,直接将比较情况变成(4)跳过了(1)、(2)、(3)这些从一开始就不匹配的匹配情况,而且断定了在a,b已经匹配,只需要测试a,b之后的字符是否匹配就可以了,如下图所示:
0 | 1 | 2 | 3 | 4 | 5 | i | 7 | ||||||||
a | b | c | d | a | b | e | f | ||||||||
a | b | c | d | a | b | w | |||||||||
0 | 1 | j | 3 |
在i的位置发生了不匹配,直接平移到了上图中的情况,然后直接从j=2位置的要匹配字符串的字符与i=6位置的被匹配字符串的字符进行对比就可以了;
为什么能跳过暴力算法的从开始就不匹配的步骤,并且确定了这次比较中已经匹配的字符串的长度呢?
个人理解:在i位置发生了不匹配的情况,我们从i往前逐渐加大长度来取得字符串,然后确定这个字符串在要匹配的串中从0开始能否得到,知道走到得不到的时候,
也就是恰好如上图中的 a,b,c中的c与 d,a,b中的b不想同的时候,由于d的出现,就可以确定它们从一开始就无法匹配;而反之取到 a,b的时候,说明a,b是可以匹配的,只需要确定i以及i之后与a,b之后是否能够匹配就可以了。
最后确定一下j与NEXT数组的关系,求出要匹配的字符串的NEXT数组的值,如下:
a | b | c | d | a | b | w |
0 | 1 | 1 | 1 | 1 | 2 | 3 |
在这里,j=NEXT值-1; 其中当NEXT为0的时候,j=-1;这种情况说明第一个字符就不匹配,在算法中i会直接+1,然后j也直接+1;也就是从被匹配串的第i+1位置和要匹配串从0开始进行匹配。
下面是算法的具体实现,(最近在学python,所以就用python写了;PS:求next数组不是最优化的算法)
# -*- coding:utf-8 -*-
# Author: Evan Midef kmp(haystack, needle):slen = len(haystack)plen = len(needle)nexts = [0] * plenget_next(needle, nexts)print(nexts)i = 0j = 0
"""
当haystack[i] == needle[j]的时候,直接i++,j++看下一个是否匹配
一旦不匹配那么i不变,j=nexts[j]-1;边界情况下,j=nexts[0]-1=-1;
这中情况说明没有满足的NEXT值,也就是在i前面的已匹配字符串的长度为-1
也就是在i+1前面已匹配的字符串的长度为0(即从i+1与j=0处开始比较),
恰好也对应i++ (=>i+1),j++( =>0)然后看下一个是否匹配
"""while i < slen and j < plen:if haystack[i] == needle[j] or j == -1:i += 1j += 1else:j = nexts[j]-1if j == plen:return i-jelse:return -1"""
设要求的字符串为A
在求next数组的时候我们已经求的了位置j的NEXT值为k+1
也就是说在要求的字符串中从[0到k-1]与[j-k到j-1]完全相同,
所以如果A[k]=A[j]的时候,只需要j++;k++;nexts[j]=k+1 就求的了j+1位置的NEXT值
当A[k]!=A[j]的时候,我们把A进行复制,赋值一个A';让k对应与A',让j对应于A;且容易得到A'的NEXT数组和A完全相同
就可以看作在A'中[0,k-1]与A中[j-k,j-1]完全相同(也就是匹配成功),但是在{j,k}处匹配失败,所以j不变,而是将
k赋值为next[k]-1(在k之前的所有NEXT值是已经求得的),最差的情况k会取到NEXT[0]-1也就是k=-1;这时在j+1之前的A'
的长度是k+1=0;所以A[j+1]要与A'[0]对应,也就是k=0;所以也需要执行操作j++ ( =>j+1),k++( =>0) 然后nexts[j]=k+1
"""def get_next(needle, nexts):al = len(needle) #求出要求NEXT数组的字符串的长度nexts[0] = 0 #NEXT[0]是恒定值0k = -1j = 0while j < al - 1: if k == -1 or needle[j] == needle[k]:j += 1k += 1nexts[j] = k+1else:k = nexts[k]-1print(kmp("hello", "ll"))