KMP算法详解
KMP算法就是一种字符串匹配算法,对于给定的两个字符串,str1与str2,查询str1中是否包含str2。
KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
在这个算法中,主要采用的一种思想是利用一个额外的next数组,来记录str2中每个字符的相同最长前缀和后缀,从而来利用优化匹配速度。
例如对于str1:abababababce , str2:abababcestr2的next数组为{-1,0,0,1,2,3,4,0},其中我们规定最长前缀和后缀不得包括最后一个字符与最前一个字符,因为这样会导致一个字符串的最长前缀和最长后缀都等于其自身,这将毫无意义。
对于0位置上的字符来说,因为它前面没有字符,不存在前缀和后缀的说法,所以我们人为规定其为-1,对于1位置上的字符‘b’来说,由于我们规定最长前缀和后缀不得包括最后一个字符与最前一个字符,所以它即为0。对于2位置的‘a’来说,它的前缀为'a',后缀为‘b’,不等,所以也为零。
而后,对于3位置的‘b’来说,他的前缀‘a’,等于前缀‘b’,并且没有更长的相同的前缀和后缀,所以3位置在next数组中对应的值为1,对于4位置上的‘a’来说,它的相同最长前缀为'ab',后缀为‘ab’,所以为2,对5位置的‘b’来说,相同的最长的前缀为‘aba’,后缀为‘aba’,所以对应next为3。对于6位置的‘c’来说,相同最长前缀和后缀变成了‘abab’,对应next数组值为4,对于最后的七位置的字符'e',没有相同的前缀和后缀,所以对应的next数组为0。
上述的next数组是我们观察出来的,但是具体的算法是如何实现的呢。
首先我们规定0位置上的值为‘-1’,1位置上的值为‘0’,而后对于任意 i 位置上的next数组值为多少呢。
我们根据i-1位置上的值来推断,我们比较i-1位置上的字符是否和next数组i-1位置上的值作为下标的字符相等,如果相等,则i位置对应的next数组的值为i-1位置上的值再加一。否则,我们根据i-1位置的值跳到next数组上对应的下标next[i-1],再度比较i-1位置上的字符是否与next数组next[i-1]位置上的值作为下标的字符相等,如果相等,则i位置对应的next数组的值为next[i-1]位置上的值再加一,否则再度进行上述过程,直至对应的坐标到0位置。
这个过程可能光靠文字难以理解,我对应上面的str2:abababce 与next数组{-1,0,0,1,2,3,4,0}再讲一遍。
首先,对于0位置上的-1,和1位置上的0,无须赘述。
对于i=2位置时,我们查询i-1=1位置的next数组的值,为0,所以我们直接比较0位置上的字符与1位置上的字符‘a’与‘b’,不等,所以2位置上的next数组的值为0.
对于i=3位置时,我们查询i-1=2位置的next数组的值,为0,所以我们直接比较0位置上的字符与2位置上的字符‘a’与‘a’,相等,所以3位置上的next数组的值为1。
对于i=4位置时,我们查询i-1=3位置的next数组的值,为1,所以我们直接比较1位置上的字符与3位置上的字符‘b’与‘b’,相等,所以3位置上的next数组的值为3位置上的值再加一,为2.
……
对于i=7位置时,我们查询i-1=6位置的next数组的值,为4,所以我们直接比较4位置上的字符与6位置上的字符‘a’与‘c’,不等。继续向前跳,我们查询 4位置上的值,为2,所以我们直接比较2位置上的字符与6位置上的字符‘a’与‘c’,不等。继续向前跳,我们查询2位置上的值,为0,所以我们直接比较0位置上的字符与6位置上的字符‘a’与‘c’,不等。所以7位置上的next数组的值为0。
证明推导如下:
当i-1位置上字符值和next数组i-1位置上的值作为下标的字符相等时,为什么next[i] = next[i-1]+1呢,为什么不能为更大的值呢。
我们假设它存在这样一个更大的值,这就意味着i-1位置的后缀能和更长的前缀匹配,超过next数组中记录的值,很明显错误。
那为什么如果不匹配要根据next[i-1]位置继续往前跳呢,原因很简单,因为我们的后缀必须要包含i-1位置的字符,它的最长前缀和后缀不行,我们就需要依次往前找小一点的前缀和后缀。
求解next数组的具体代码如下:
public static int[] next(char[] arr){ if(arr.length == 1){ return new int[]{-1}; } int[] next = new int[arr.length]; next[0] = -1; next[1] = 0; int i = 2; int cn = 0; while (i<next.length){ if(arr[i-1] == arr[cn]){ next[i++] = ++cn; }else if(next[cn] > 0){ cn = next[cn]; }else { next[i++] = 0; } } return next; }
将上述过程理解了,相应的代码是很简单的。
KMP算法的关键就是next数组,那我们如何根据next数组来加快字符串的匹配速度呢。
对于str1:abababababce , str2:abababce 我们定义两个下标p1,p2,依次匹配两个字符串。对于前面的‘ababab’这一截字符串都相等,但是str1下一个字符为a,而str2的下一个字符为c,不相等,按照暴力方法的话,我们只能从1位置又继续比较,而不能利用前面的比较信息。但是对于KMP算法而言,我们直接查询匹配到的最后一个字符对应的next数组的值,为3,而后我们直接将str1中第6位置上的值和str2位置上的第三个位置的值继续进行比较,p1,p2指针向前,匹配完成。
而这件事情的实质是什么,实质是我们否定了str1上0到3位置上的值能匹配到str2的可能性,而不否定4位置以后能够继续匹配到str2的可能性。也就是说,我们只认为对应后缀的开始字符串有可能匹配到str2,而前面的字符串不可能匹配到str2.
证明如下:
如果存在4位置之前的字符能够继续匹配成功str2,那么4位置之前的字符串加上最长后缀,就等于最长前缀再加一截字符,这就意味这i位置上的最长相同前缀后后缀的值能更大,也就是next数组中的值能更大,错误。
代码如下:
public static int getIndexOf(String s1,String s2){ if(s2 == null || s2.length() > s1.length()){ return -1; } char[] str1 = s1.toCharArray(); char[] str2 = s2.toCharArray(); int[] next = next(str2); int p1 = 0; int p2 = 0; while (p1 <str1.length && p2 < str2.length){ if(str1[p1] == str2[p2]){ p1++; p2++; }else if(next[p2] == -1){ p1++; }else{ p2 = next[p2]; } } return p2 == str2.length ? p1 - p2 : -1; }
返回值为如果能匹配到str2,就返回开始的下标。
主函数如下:
public static void main(String[] args){
String str1 = "abcdabcabcd";
String str2 = "abcabc";
int n = getIndexOf(str1,str2);
if(n == -1)
System.out.println("str1中不存在str2");
else
System.out.println("str1中存在str2,从str1的第:"+n+" 个位置");
}