KMP算法代码详解

tech2022-08-15  125

学习了kmp算法,思想其实很好理解,但是代码的实现一直看得很迷糊。看了很多博客,特别是对匹配信息的next数组有很多不同,比如数组有首位有-1的也有0的等。我自己也疏离了一遍,记录一下,方便之后以后回忆。 首先KMP算法主要是用来解决字符串(也叫主串)中的模式串定位问题,比如,有一个主串"abaabaabbabaaabaabbabaab",要找出主串中是否存在子串"abaabbabaab",并返回匹配到的具体位置。

如果是普通的暴力匹配,应该是将主串和模式串从左到右一个个匹配,如果这个过程中有某个字符不匹配,就跳回去,将模式串向右移动一位。比如 在匹配到第4位的时候,发现与模式位不匹配,则将模式串向右移动一位继续从头开始匹配。 再发现注册的第2位字符和模式串的第1位字符不匹配,模式串继续右移 然后再依次匹配,最终算出结果。这种方式的时间复杂度较高为O(m*n)。 其实观察上面的匹配过程,在第一次主串第4位和模式串第4位匹配不成功的时候,因为前三位其实已经匹配过了。并且模式串第2.3位没有与第1位a匹配的。也就是说,在暴力匹配中的第二次向右平移用主串的第二位b与模式串第一位a,第三次向右平移,用主串的第三位c与模式串第一位a做匹配,是必然失败的。所以可以省略这样的步骤。将模式串第一位a直接与主串刚刚匹配失败的第四位a做匹配。 在一一匹配后,发现主串的第10位与模式串的第7位不匹配,此时,之前与模式串匹配过的前6位中 abcdab,后缀ab与模式串开头ab是相同的,无须比较,可以将模式串的第三位移到主串不匹配的第10位底下进行比较。 KMP算法其实就是主串不去移回已经比较过的位置内容,利用已经匹配过的已知信息,在模式串中筛选出合理的位置字符,去与主串的字符做匹配,这样就提高了效率。这一步的时间复杂度就只有O(m)了。但是在匹配失败后怎么知道主串的这个字符要与模式串的哪个字符做匹配呢?这里就需要一张匹配表,一般都喜欢用next数组来表示。索引从0开始,一一对应模式串的字符。对应索引的数组值是匹配失败后,模式串下一个与主串对应字符匹配的字符索引(也可以理解为最长前后缀匹配的位数)。所以求next数组需要遍历模式串,时间复杂度为O(n),总体整个KMP算法的时间复杂度为O(m+n)。

这里说下怎么求next数组吧,具体的理论可以去参考其他的博客,这里就不再详细的介绍了,主要是来记录了解代码的。

首先在已匹配模式串中,如果以i代表模式串的索引,j代表匹配重复子串(最大前后缀)的索引+1。从开头到结束不包括模式串结尾属于前缀,从结尾到开头(不包括开头)叫后缀。比如模式串abcdabd,如果已经匹配了6位,最后第7位d匹配失败,d所对应的前缀都有 a、ab、abc、abcd、abcda,所对应的后缀有b、ab、dab、cdab、bcdab,他们的相同前缀为ab。已经匹配过的字符中,第3位c与匹配失败的第7位d有相同的前缀ab,并且ab是模式串的开头,所以在下一次匹配中,可以跳过ab,直接将第3位c移动到现在匹配失败d的位置。也就是主串对应的字符在匹配d失败后直接就与第3位c做匹配。那么next[6]的值就是c的索引2,也等于最大子串匹配的长度。

因为模式串开头第一个字符是没有前缀的,所以可以设置一个特殊的数字next[0]=-1,从第二个字符开始做匹配,i=1。j=0,因为比较重复子串都是从开头也就是模式串第一个字符做比较的。如果i字符与要比较的j字符相同,那么i、j分别加1,这个位置next[]数组就是对应j的值,所以如果最大重复子串长度为1则j在索引1的位置代表索引0位置的字符已经匹配。如果j位置匹配失败,则j回溯到next[j]位置继续匹配,next[]数组的值本来就是对应位置匹配失败后,应该去匹配的索引位置,前面已经介绍过了。

如果j在索引0的位置还是匹配失败了,就说明对应失败位置前面没有任何重复的子串,那么匹配的模式串不需要再匹配,值为0,再将i索引加1进行下个字符的匹配。而next[0]的值为-1,所以j的值也应该加1,重置为索引0,继续从开头做匹配。

所以大概的求next数组的代码是

public static int[] getNext(String s) { char[] chars = s.toCharArray(); int i = 0; int j = -1; int[] next = new int[s.length()]; next[0] = -1; while (i < next.length - 1) { if (j == -1 || chars[i] == chars[j]) { next[++i] = ++j; } else { j = next[j]; } } return next; }

有了next数组后,在主串与模式串不匹配的时候,就知道需要换模式串的哪个索引字符去和主串匹配了。

public static int kmp(String s, String t) { int[] next = getNext(t); //主串位置 int i = 0; //模式串位置 int j = 0; while (i < s.length() && j < t.length()) { if (j == -1 || s.charAt(i) == t.charAt(j)) { i++; j++; } else { j = next[j]; } } if (j == t.length()) { return i - j; } else { return -1; } }

s为主串,t为模式串,j==t.length(),只有在模式串完全匹配的情况下才会发生,不然j的值会不停被回溯为数组的其他索引上,方法会返回模式串与主串匹配的第一个字符索引,如果匹配不到完整的模式串,则返回-1。

这个kmp算法还有一些缺陷,看下面例子 下面是模式串,第6位b与主串第9位c不匹配,根据kmp算法可以算出第6位也就是next[5]的值是1,但是索引1位置也是个b,与第6位重复了,并且第6位与c已经比较过了,不匹配,所以回溯后,索引1位置b与c肯定也是不匹配的。这一步属于多余操作,最终他会与模式串索引是索引1位置的next数组值的字符做匹配。所以在匹配到相同前后缀后,还需要判断一下是否相同,如果相同则跳过。改进下求next数组的代码

public static int[] getNext(String s) { char[] chars = s.toCharArray(); int i = 0; int j = -1; int[] next = new int[s.length()]; next[0] = -1; while (i < next.length - 1) { if (j == -1 || chars[i] == chars[j]) { if (chars[++i] == chars[++j]) { chars[i] = chars[j]; } else { next[i] = j; } } else { j = next[j]; } } return next; }
最新回复(0)