字符串 - KMP算法
字符串算法中,字符串匹配是一个非常重要的应用。例如在网页中查找关键词,其实就是在对字符串匹配,也就是看一个主字符串中是否包含了一个子字符串。
而KMP算法在字符串匹配方法中一个很著名并且很聪明的算法,当然也确实比较难理解。甚至于有程序员因为无法理解KMP算法而直接改用暴力匹配。本身自己学算法起步较晚,第一次接触到KMP算法已经是研究生毕业一年了。虽然带着研究生的学历背景,但是刚开始看的时候依然是一脸懵逼。看了很多博主的讲解总算是明白了,所以在这篇博客中记录下来自己的理解,如果能帮助到别人也是万分荣幸了。本篇博客参考了孤~影~关于KMP算法的讲解。
先从暴力匹配方法感受一下字符串匹配的过程。其中i表示匹配过程中主字符串的位置,j表示子字符串的位置。可以看出,j总是从0开始匹配,而i也是从0开始,一步一步向后移。
注意上面的算法,是i和j同时在移动,如果遇到不匹配的,那么i和j同时回退。这种算法简单粗暴好理解,但是仍然有改进的地方。
以上图为例,想象一下是自己手动拿着P去匹配S,假如第一轮没匹配上,那么我们很自然想到直接从第一轮跳到第四轮,因为第二轮和第三轮的首字母A明显就和S中的字符B、C不符。
再来几个例子感受下这个更快速的移动过程,让我们试着发现一些规律。
上面三个例子中移动位置其实是比较理想的,假想是我们人为去匹配多半也是这样比划的。那么观察一下红框中的字符串,其实存在一定的规律。
其实在这些情况下之所以存在比暴力算法更快的匹配,就是因为子字符串本身就包含了一定的特殊性。我们可以从上图中看出,子字符串的首和尾其实存在着重复的字符串。更具一般性的,
子字符串P中的,P[0] ~ P[k-1] = P[j-k] ~ P[j-1]。也就是首尾重复字符串,注意也是最大的首尾重复字符串。注意以下一种情况。
以上就是为了说明,假如我们遇到了第j个字符不匹配时,可以直接移动到k,而不需要比较前k-1个字符。
于是现在的重点就在于怎么求k。
1)next数组是什么?
next数组记录了子字符串遇到不匹配时要回退的位置。上述举的例子恰好是P数组最后一个字符不匹配,但实际上不匹配可能出现在P的任何一个位置。因此我们为长度为n的P字符串建立一个长度也为n的next数组。next[j]表示,当P的第j个字符出现不匹配时,字符匹配子字符串应该移动到的位置,如下图所示。
具体来说,以"abbaabcab"为例,假设要计算next[6]。那么对于"abbaab"来说:
头部有: a ab abb abba abbaa(不包含最后一个字符)
尾部有: b ab aab baab bbaab (不包含第一个字符)
最长首尾相同字符串就是ab,长度为2。那么next[6] = 2。
2)如何求next数组?
假设k表示当前最大首尾相同字符串的头部,j指向
- 初始时刻,k指向-1,j指向0的位置。
- 当遇到首尾相同的字符时,k和j都向右移动一位
- 当遇到首尾不同的字符时,k向左回退到next[k]
也就是说,当P[k]不等于P[j]时,k必须要回退到次大的首尾相同字符串,也就是上图中的黄色块。
结合代码可能更清楚。求next数组的代码:
public static int[] getNext(String ps) { char[] p = ps.toCharArray(); int[] next = new int[p.length]; next[0] = -1; int j = 0; int k = -1; while (j < p.length - 1) { if (k == -1 || p[j] == p[k]) { next[++j] = ++k; } else { k = next[k]; } } return next; }
求得了next数组之后,就可以写KMP算法了:
public static int KMP(String ts, String ps) { char[] t = ts.toCharArray(); char[] p = ps.toCharArray(); int i = 0; // 主串的位置 int j = 0; // 模式串的位置 int[] next = getNext(ps); while (i < t.length && j < p.length) { if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归0 i++; j++; } else { j = next[j]; // j回到指定位置 } } if (j == p.length) { return i - j; } else { return -1; } }
以上就是KMP算法的原理了,总结一下:
1)KMP算法比暴力匹配改进的地方其实只在于当子字符串本身存在着一定的首尾相同的情况时,可以直接跳到头部字符串的末端。
2)KMP算法保证了主字符串的位置i不回退,而只回退子字符串的位置j,而且j不需要每次都从0开始。
参考资料: 《算法》第四版
https://www.cnblogs.com/yjiyjige/p/3263858.html