写在最前面
因为这次的新型冠状病毒,整个新年假期都呆在家里相应国家的号召。
借着这段时间,复习一下以前学过的一些算法,顺便做一下记录,温故而知新。
I'm a big fans of The Big Bang Theory
KMP 算法
被多家公司作为校招算法题,比如2018年京东。
KMP算法,也就是Knuth-Morris-Pratt 字符串查找算法,时间复杂度为 O(N)
, 我以为是通过寻找字符串中对称的子串
来实现算法的加速。
子串
字符串中的子串,我理解是字符串中的一部分,比如
abc
和cde
都属于abcde
的子串
Next 算法
Next 算法属于KMP算法的核心部分,其目的是为了找到一个字符串中,每个字符的最长前序和最长后序。
前序和后序
先给了例子体会一下,假设我们有一个字符串
abcd
。
字母a
没有前序和后序。
字母b
的前序和后序都是a
,不符合前序和后序的规则,所以我们认定它没有前序和后序。
字母c
的前序是a
,后序是b
。
字母d
的前序是ab
,后序是bc
。
聪明的你一定发现了,某一字符的前序指的是从字符串的第一个字符开始,一直到该字符的前一位结束(不包括该字符的前一位字符)。而后序指的是从该字符后序指的是,从该字符的前一位开始,到这个字符串的开头(不包括第一位字符,也就是0位置上的字符)。
如果该字符的前序和后序重合,也就是上面例子中的b
字母,我们认定该字符没有前序和后序
最长前序和最长后序
了解了前序和后序,那么最长前序和后序也就非常好理解了。某个字符的最长前序和最长后序,指的是该字符前序和后序对称部分的长度。
举个例子abcabd
关于字母d
他的前序是abca
后序是bcab
最长前序和后序是ab
通过算法实现所有字符最长前序和后序的查找
talk is cheap show me the code
function next(arr){
const next_arr = []; //建立一个next_arr数组,记录输入数组每个字符的最长前序和最长后序
next_arr[0] = -1; // 0位置没有前序和后序
next_arr[1] = 0; // 1 位置前序和后续重合默认为0
let i = 2; // 设置两个指针,一个指向当前的值
let j = 0;// 另一个指向对应用来比较的值,同时也是回跳的值,上一个值最长前序的结束位置
while( i < arr.length ){
if(arr[i-1] === arr[j]){ // 如果两个指针所指向的值相同,前序和后序的长度增加
next_arr[i] = ++j; // 先增加在赋值,否则会在下一轮循环中,值才增加
i++;
} else if(j > 0){ // next_arr[1] = 0, 也就是说未能在之前的数组中找到可以复用的前序
j = next_arr[j]
} else{
next_arr[i] = 0;
i++;
}
}
return next_arr;
}
简单解释一下原理:
首先我们知道,在零位置和一位置的next_arr
值为 -1 和 0。利用数学归纳法,我们可以推出后面的值。
如果arr[0]
的值和arr[1]
值相等,那么next_arr[2] = 1
,如果arr[1] == arr[2]
那么就有next_arr[3] = 2
。我们把它抽象出来,那么如果arr[i-1] == arr[j]
, j= j + 1
。这里的i
指向我们想知道最长前序的值,j 指向arr[i - 1]
最长前序结束的位置。
举个例子
arr = abcab
。
现在arr[4]
的最长前序是1
,也就是字母a
,i
指向arr[4]
, j 指向arr[1]
。现在arr
扩展成abcabd
,那么i
指向arr[5]
,而 j 依旧指向arr[1]
,我们发现arr[i-1] == arr[j]
, 那么j++
, 所以arr[5]
的最长前序为2.
如果arr[i - 1] !== arr[j]
, 那么j
指向arr[j]
最长前序的结束位置。
当然还有别的情况,如果j
指向arr[1]
也就是next_arr[1] = 0
也就是说,在之前的字符串中没有找到可以复用的前序,那么next_arr[i] = 0 & i++
。
我理解其实就是一句话,如果对称的增加,那么最长前序边长。否则,往前跳找对称前序。
字符串匹配的算法实现
talk is cheap show me the code
function getIndex(arr1, arr2){
let i1 = 0;
let i2 = 0;
const next_arr = next(arr2);
while( i1 < arr1.length && i2 < arr2.length){
if(arr1[i1] === arr2[i2]){
i1++;
i2++
} else if(next_arr[i2] === -1){ // next_arr[0] = -1, 也就是说第一个也没有匹配上
i1++; // 跳到下一个
} else{
i2= next_arr[i2] // 指向以该位置的最长前序结束位置, 让i1和i2的最长前序结束位置去匹
}
}
i2 = arr2.length ? i1 - i2: -1; // 确认位置,如果没有匹配到返回-1
return i2;
}
如果明白了之前的 Next 函数的原理,那么这个就很好立即了。同样是找对称,如果str1
和str2
第一个字符就没有匹配上,那么跳到str1
的下一个字符直到匹配上。如果有一段字符匹配上了,但是在arr2[i]
字符没有匹配上,那么就从arr2[i]
最长前序结束位置开始和arr1
重新匹配。直到出发循环结束条件arr1.length
, 如果匹配上了,那么返回str2
开始位置,否则返回-1
.
写在最后
我个人理解,KMP算法就是一个找对称序列的过程,通过加入最长前序和最长后序这么一个概念,减少运算时间。换句话说,就是我先把我对称的位置都算好了,和你匹配的时候,可以直接从我每个位置的对称结束位置开始匹配。