KMP算法新手向详解

732 阅读5分钟

写在最前面

因为这次的新型冠状病毒,整个新年假期都呆在家里相应国家的号召。
借着这段时间,复习一下以前学过的一些算法,顺便做一下记录,温故而知新。
I'm a big fans of The Big Bang Theory

KMP 算法

被多家公司作为校招算法题,比如2018年京东。 KMP算法,也就是Knuth-Morris-Pratt 字符串查找算法,时间复杂度为 O(N), 我以为是通过寻找字符串中对称的子串来实现算法的加速。

子串

字符串中的子串,我理解是字符串中的一部分,比如abccde都属于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 函数的原理,那么这个就很好立即了。同样是找对称,如果str1str2第一个字符就没有匹配上,那么跳到str1的下一个字符直到匹配上。如果有一段字符匹配上了,但是在arr2[i]字符没有匹配上,那么就从arr2[i]最长前序结束位置开始和arr1重新匹配。直到出发循环结束条件arr1.length, 如果匹配上了,那么返回str2开始位置,否则返回-1.

写在最后

我个人理解,KMP算法就是一个找对称序列的过程,通过加入最长前序和最长后序这么一个概念,减少运算时间。换句话说,就是我先把我对称的位置都算好了,和你匹配的时候,可以直接从我每个位置的对称结束位置开始匹配。

如果觉得有用的话,点个赞再走,谢谢!