KMP算法的理解
近期工作不是太忙,就研究起了一些数据结构与算法的知识,看到了《大话数据结构》这本书,其中关于字符串查的算法有一个非常NB的算法:KMP算法
,就潜心研究了一下。索性将心得与我对其思路的理解写下,以便将来使用的时候能够快速复用。
KMP算法又称克努特(Knuth)-莫里斯(Morris)-普拉特(Pratt)算法,它是由上面的三位大神共同创造出来的算法,使复杂度从O(N^2)变成了O(N),匹配的效率提升了许多。
使用该算法之前,我们如果在一个字符串中匹配到指定子串的索引,可能使用双层for循环来完成,即外层遍历目标字符串,里层遍历子串,逐字符的进行比较,如果不相同,目标字符串索引加1,子串从头开始比较:
function findIndex(str, tarStr) {
let index = -1;
let flag = false;
if (str.length > tarStr.length) {
return -1;
}
// 只比较有效的字符长度。
for(let i = 0;i <= tarStr.length - str.length;i++) {
for(let j = 0;j < str.length;j++) {
if (tarStr.charAt(i + j) != str.charAt(j)) {
index = -1;
break;
}
index = i;
if (j==str.length - 1) {
flag = true;
break;
}
}
if (flag) {
break;
}
}
return index;
}
上面的代码是我们传统的写法,写固然是没问题的,但是效率不高,即目标字符串的每一个字符都要与子符串的每个字符作比较,这样就比较耗时,为了解决这个问题,于是上面提到的三位大佬就提出解决思路----KMP算法。
KMP算法为什么可以使我们的运算效率大大的提高呢?这得从传统方法与KMP算法的处理过程来分析:
有一个字符串str1="abcabcdabcdabde"
,我们的目的是从上面的字符串中找到str2="abcdabd"
首次出现的索引。对于传统方法:我们的步骤是:
将str1的每一个字符与str2的第一个字符比较,它们都为a 两者相等,继续比较
将str1的每二个字符与str2的第二个字符比较,它们都为b 两者相等,继续比较
将str1的每三个字符与str2的第三个字符比较,它们都为c 两者相等,继续比较
将str1的每四个字符与str2的第四个字符比较,前者为a后者为d 它们不相等,则从str1的第二个字符作为开始与str2比较
将str1的每二个字符与str2的第一个字符比较,前者为b,后者为a 它们不相等,则从str1的第三个字符作为开始与str2比较
… 此处流程图若干次比较
将str1的每十三个字符与str2的第七个字符比较,前者为d后者为d 它们相等,找到了目标的索引
通过上面的过程,我们发现了一个问题,str2中的abcd尽皆不等,str1的前三个字符与str2的前三个字符可以一一对应,那么,比较第二次比较str1第二个字符时,其实是没必要的了,其实是可以直接从str1的第4个字符重新比较的。同样,如果我们比较到str1的第十个字符时,发现str2的第七个字符为d而str1的第十个字符为c,两者不相等,但是,str2的前三个字符与str1的第八九十个字符可以匹配,这个需要下次只需要比较str1的第十一个字符与str2的第四个字符即可。为了计算这种下次比较的索引,我们需要分析这个str2,计算出它每个字符的下次比较的索引数组即可,而不需要从头判断str1已经比较过的值即可,而这个算法的难点就是计算这个数组next[]。
next的值书中提供的有工式:
从这个公式,是从数学角度来说的,我们开发时,则需要改进一下,因为,它的索引是从1开始的,但我们的索引都是从0开始,但是,原理是一样的原理,于是获取next的代码如下:
function getNext(str) {
let next = new Array(str.length);
let k = 2; // 为什么是2?因为公式中的判断条件是1~k-1,而我们的代码中的索引是从0开始,所以,0~k-2。索引不可能为负,所以,k最小得是2
next[0] = 0; // 第一个元素永远为0
for(let i = 1;i < str.length;i++) { // i的索引从1开始,最后一个字符不作自理
if (i > 1 && str.charAt(i - 1) == str.charAt(k - 2)) {
next[i] = k;
k++;
continue;
}
if (k > 2) { // 如果k大于2,证明重复的字串长大于1,上面条件不满足,但是需要重新判断一下第一个元素和当前元素是否相等
k = 2;
i--;
continue;
}
next[i] = 1;
k = 2;
}
return next;
}
得到了next数组后,我们就可以对两个数组进行比较了,比较的时候就可以使用到这个next数组了
function compare(str1, str2) {
if(str1.length > str2.length) {
return -1;
}
let index = 0;
let next_index = 0;
let next = getNext(str1);
while(index <= str2.length) {
if (str2.charAt(index) == str1.charAt(next_index)) {
next_index++;
index++;
if (next_index == next.length) {
return index - next.length;
}
} else {
next_index = next[next_index];
if (next_index === 0) {
index++;
continue;
}
next_index--; // 由于上面计算的索引都是从1开始的,所以,得到的索引要减1
}
}
return -1;
}
上面的代码已经基本实现了最大程度的优化,但不是最优解,因为子串中也是有重复的,如果当前的字符不相等,根据next的索引回溯到上个字符也是不相等的,也是就有了改良的next_val数组
function getNextVal(str, next) {
let next_val = new Array(next.length);
next_val[0] = 0;
for(let i = 1;i < str.length;i++) {
let val = next[i];
if (str.charAt(i) == str.charAt(val)) {
next_val[i] = next_val[val];
} else {
next_val[i] = next[i];
}
}
return next_val;
}
这时,再比较时,就不使用next数组了,只需要next_val数组即可。最后,为了方便使用,将其封装成一个工具类即可:
class KMP {
constructor(str) {
this.next = null;
this.str = str;
this.getNextVal(str);
}
getNext(str) {
let next = new Array(str.length);
let k = 2;
next[0] = 0;
for(let i = 1;i < str.length; i++) {
if (i > 1 && str[i - 1] == str[k - 2]) {
next[i] = k;
k++;
continue;
}
if (k > 2) {
i--;
k = 2;
continue;
}
next[i] = 1;
k = 2
}
return next;
}
getNextVal(str) {
let next = this.getNext(str);
let next_val = new Array(str.length);
next_val[0] = 0;
for(let i = 1;i < str.length;i++) {
let temp = next[i] - 1;
if (str.charAt(i) == str.charAt(temp)) {
next_val[i] = next_val[temp];
} else {
next_val[i] = next[i];
}
}
this.next = next_val;
}
compare(_str) {
if (_str.length < this.str.length) {
return -1;
}
let index = 0;
let next_index = 0;
let next = this.next;
while(index < _str.length) {
if (_str.charAt(index) === this.str.charAt(next_index)) {
index++;
next_index++;
if(next_index == next.length) {
return index - next_index;
}
} else {
next_index = next[next_index];
if (next_index == 0) {
index++;
continue;
}
next_index--;
}
}
return -1;
}
}
以上就是个人对kmp算法的自我理解与代码展示,说实话,虽然这个代码是这样写的,但是,有时候自己再去回想处理过程时,有时还是会觉得深奥,只能说发明这个算法的三个大佬是天才,有句话叫照抄都抄不懂,如是而已。不过这些东西都会靠积累与理解,或许以后会对这个算法有更深刻的印象与理解吧。