KMP算法学习与理解

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的值书中提供的有工式:
kmp算法的公式
从这个公式,是从数学角度来说的,我们开发时,则需要改进一下,因为,它的索引是从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算法的自我理解与代码展示,说实话,虽然这个代码是这样写的,但是,有时候自己再去回想处理过程时,有时还是会觉得深奥,只能说发明这个算法的三个大佬是天才,有句话叫照抄都抄不懂,如是而已。不过这些东西都会靠积累与理解,或许以后会对这个算法有更深刻的印象与理解吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值