上一节,我们研究了KMP算法的实现原理,这节,我们从分析的角度看看KMP算法的时间复杂度和它的正确性。
我们先看计算匹配字符串最长后缀数组的的函数:
private int getLongestSuffix(int s) throws Exception {
if (s <= 0 || s > Pi.length) {
throw new Exception("Illegal index");
}
if (Pi[s] != -1) {
return Pi[s];
}
Pi[s] = 0;
int k = getLongestSuffix(s-1);
do {
if (P.charAt(k) == P.charAt(s - 1)) {
Pi[s] = k + 1;
return Pi[s];
}
if (k > 0) {
k = getLongestSuffix(k);
}
} while (k > 0);
return Pi[s];
}
要确定该函数的时间复杂度是O(m),(其中m是匹配字符串的长度),不是一件容易的事。一是该函数中有递归调用,同时函数中又有while循环,所以确保该函数的时间复杂度是线性的,需要一定的分析。
getLongestSuffix第一次调用时,传入的参数是P.length(P是匹配字符串), 也就是该函数调用是,输入参数最大也就是m.
do.. while 循环的条件是变量k大于0,并且在循环中,要进行递归调用的话,同样需要k > 0. 一旦k等于0,那么do…while循环和递归调用将会停止。
如果if(k>0) 条件成立,进入递归调用,我们可以确定,返回的k值是递减的,一旦递减到0,那么递归调用和do..while循环都会停止进行。getLongestSuffix返回的是Pi数值中的元素值,也就是说,k的数值跟Pi数组中某一个元素是对应的。
一旦k == 0, 那么下次do..while循环能够循环两次以上,或者说下次能够再次满足if(k>0) 以便进行递归调用的话,判断if (P.charAt(k) == P.charAt(s – 1)) 必须满足,这样k值才重新获得一个大于0的值。这就意味着do…while循环的次数以及递归调用的次数之和,小于if (P.charAt(k) == P.charAt(s – 1)) 成立的次数。而该条件成立的次数最多不超过匹配字符串的字符个数,也就是m, 并且一旦成立,函数里面返回,这也意味着,do..while循环的总次数和递归调用的总次数之和不会大于m,因此这个函数的时间复杂度就是O(m).
我们再看匹配函数match的实现:
public int match(String T) {
int n = T.length();
int m = P.length();
int q = 0;
for (int i = 0; i < T.length(); i++) {
while (q > 0 && P.charAt(q) != T.charAt(i)) {
q = Pi[q]; //获取最长后缀,并判断最长后缀的下一个字符是否能跟当前比对位置的下一个字符匹配
}
if (P.charAt(q) == T.charAt(i)) {
q = q + 1;
}
if (q == m) {
return i - m + 1;
}
}
return -1;
}
在match的实现中,有一个循环间套,外层循环for的循环次数是匹配文本T的长度,如果我们能确保,for里面的while循环总次数为O(T.length), 那么就算有两个循环间套,那么循环的总次数也就是T.length.
要想进入while循环,必须满足一个条件是q>0, 当进入循环后,执行语句 q = Pi[i];
这条语句的执行,必定会使得q值变小,while每循环一次,q值就减少,如果q减少到0后,while循环就不会再进行了。
那么什么时候,while循环可以进行呢,显然至少要q>0满足,当q减少到0之后,要想重新大于0,必须执行语句:
if (P.charAt(q) == T.charAt(i)) {
q = q + 1;
}
上面的语句是间套在for循环里面的,for循环最多进行T.length次,也就是匹配文本的长度,由此说来,q由0到大于0的次数最多只有T.length次,这就意味着while循环最多也只能进行T.length次。
这就保证了match函数的复杂度是O(T.length).