KMP算法应该是经典算法中的经典算法了,背景我也不介绍了,当然背景我也不怎么了解。
那它解决的问题是什么呢?
在我粗浅的理解中,主要解决的是包含问题,如经典的字符串包含问题,如解决字符串中abccccc中是否包含abc这样一个问题。
好了,你也许会说,这还不简单吗,直接两边循环嵌套,暴力碾压过去。当然,这是可以做到的,这就是传统的BF算法。但是,毫无疑问,这不优雅美观。丑陋且恐怖的O(n*m)复杂度相信你不会喜欢的,有极客精神的程序员都努力避免这样高复杂度算法的产生。
KMP的优雅之处就是可以将时间复杂度压缩到O(n)。
设基础问题为求str1是否包含str2
首先,我们需要有一个基础概念:我们可以求一个字符串中,一个字符之前的字符串,最长前缀和最长后缀的匹配长度(且前缀不能包含最后一个字符,后缀不能包含第一个字符)
这是什么意思呢,有点拗口,举个例子:
abaabac中,我们若要求c的最长前缀和最长后缀的匹配长度。
显然前缀a后缀a匹配,但不是最长。
前缀aba后缀aba匹配,是最长。所以匹配长度为3
根据此规则,我们可以将str2中每个字符计算匹配长度从而得出一个数组。就暂且称为next数组吧。
next数组怎么能快速高效的求出来呢?毕竟我们不可能每个字符位置都重新去计算一遍,这样舍本逐末还不如直接使用BF算法呢。
下图为求next数组的方法:
求第i位用的是数学归纳法,证明第i位的求法可以用反证法。
没理解的话可以看看这篇文章:http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html
那有什么用呢?
继续往下看
我们接下来开始比对str1中是否包含str2,在传统的BF算法中,只是单纯的循环比对。在KMP算法中,我们来看看是如何加速这个过程的:
我们假设下图是比对字符串的某一过程,str1在i位置(前面或许还有已匹配失败的字符串段)开始与str2初始位置o匹配,直到X位置与Y位置不匹配(注意:X与Y位置后面或许还有剩余字符串)
由于我们已经知道了str2字符串任意位置的最长前缀与最长后缀(即上述的next数组),在X与Y匹配失败后,我们可以直接跳过到最长后缀对应的第一个位置:str中的J位置(因为前提是最长前缀最长后缀)。这是第一个加速过程。
又因为最长前缀最长后缀,所以就有理所应当的第二个加速过程:假设Y位置的最长前缀长度为N,那我们可以直接从J+N位置继续匹配。
证明过程:即大致是围绕证明最长前缀和最长后缀的关系当前仅当相等且最长时,后续观点成立。(即否定跳过字符串中不可能包含str2等一系列操作)
LeetCode中也有比较直白的使用KMP算法的题,如第28题实现strStr()。
BF算法实现版:
class Solution {
public int strStr(String haystack, String needle) {
if (needle==null||haystack==null||needle=="") {
return 0;
}
char[] haychar=haystack.toCharArray();
char[] needchar=needle.toCharArray();
int hayindex=0,tempindex;
boolean finish=true;
while (hayindex<=(haychar.length-needchar.length)) {
tempindex=hayindex;
for (int i = 0; i < needchar.length; i++) {
if (needchar[i]!=haychar[tempindex++]) {
finish=false;
break;
}
}
if (finish) {
return hayindex;
}else{
finish=true;
hayindex++;
}
}
return -1;
}
}
KMP实现版:
class Solution {
public int strStr(String haystack, String needle) {
if (needle==null||haystack==null||needle.length()<=0) {
return 0;
}
char[] haychar = haystack.toCharArray();
char[] needchar = needle.toCharArray();
int hayindex = 0;
int needindex = 0;
int[] next = getNextArray(needchar);
while (hayindex < haychar.length && needindex < needchar.length) {
if (haychar[hayindex] == needchar[needindex]) {
hayindex++;
needindex++;
} else if (next[needindex] == -1) {
hayindex++;
} else {
needindex = next[needindex];
}
}
return needindex == needchar.length ? hayindex - needindex : -1;
}
public static int[] getNextArray(char[] ms) {
if (ms.length == 1) {
return new int[] { -1 };
}
int[] next = new int[ms.length];
next[0] = -1;
next[1] = 0;
int pos = 2;
int cn = 0;
while (pos < next.length) {
if (ms[pos - 1] == ms[cn]) {
next[pos++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[pos++] = 0;
}
}
return next;
}
}
可以看到KMP算法减少了运行时间(当然,OJ上的运行时间其实很容易产生较大波动,说服力不够)
若是用在超长字符串上,就可以感受到两种算法的明显差距了。