彻底搞懂kmp算法

246 阅读3分钟

kmp算法是字符串问题中常见算法,主要用于字符串的匹配。整体思路上面比较绕,今天让我们一起彻底搞懂kmp算法。

基本概念

在学习kmp算法之前我们需要先了解几个基本概念:

1.前缀与后缀

前缀:必须从第0个字符开始,到最后一个字符之前任意一个字符,所构成的子串,都叫做该字符串的前缀
后缀:从字符串的第2个字符以后(包括第二个)任意一个字符,到最后一个字符所构成的子串,都叫做该字符串的后缀

2.相等的前后缀

相等前后缀是说,一个字符串的前缀和后缀是相等的
举例:
aabaabaa
相等前后缀为:aa

3.next数组

next数组是kmp算法的一个核心,该数组记录的目标字符串中,从第0个字符开始,到第i个字符的相等前后缀长度

      a a b a a f
next=[0,1,0,1,2,0]

kmp算法

匹配字符串除了使用kmp算法,还可以使用暴力匹配,暴力匹配需要我们反复移动主串上的指针,而kmp算法的精髓就在于主串上的指针只会向后移动,不会向前移动

function kmp_search(str, patt) {
  // 先根据目标字符串构建next数组
  const next = build_next(patt)
  let i = 0, //主串指针
    j = 0 //子串指针
  while (i < str.length) {
    // 如果主串[i]和目标串[j]相等,则将i和j向后移动,继续进行匹配
    if (str[i] == patt[j]) {
      i++
      j++
      // 如果不相等,且已经匹配目标串的一部分,这时候就需要使用到next数组,下面会进行详解
    } else if (j > 0) {
      j = next[j - 1]
      // 如果不相等,且匹配过的字符串还没有和目标串相同的部分,就直接往后继续移动主串就好了
    } else {
      i++
    }
    if (j == patt.length) {
      return i - j
    }
  }
}

next数组使用

从上面例子我们看到,当目标字符串和主串已经有一部分匹配,但是第i个字符串不匹配了,这时候不需要将目标字符串上指针指回初始位置,而是借助了next数组,其实这不难理解
image.png
从图中我们看出,主串的b和目标字符串的f不匹配,但是f之前的字符都匹配上了,这时候我们就要充分利用这些匹配过的子串,我们不难发现,既然之前已经匹配上了,那么我们只要找到目标字符串的最长相等前后缀,可以直接将指针从后缀移动到前缀,在这个例子中也就是直接从f移动到b,而我们如何准确移动到b的位置呢,这就要借助next数组

next数组构建

思路:

1.我们构建两个指针,一个用于到当前字符串位置最长公共前后缀长度,一个用于遍历整体串
2.整体思路就是,从第一个字符串开始不断往后添加,注意next数组记录的是从第1个字符到当前字符公共前后缀的长度
3.新增一个字符无非就两种情况,一种和当前公共前后缀的最后一个字符串相等,这时候我们只需要向后继续遍历即可,另外一种情况,就是不相等,当当前子串和当前公共前后缀不相等时,我们就需要去缩短公共前后缀的长度,依次进行匹配,直至匹配上或者到0为止

代码实现:

function build_next(str) {
  // 寻找子串中最长公共前后缀
  const next = [0]
  let prefix = 0, //最长公共前后缀长度
    i = 1 //因为1个字符串不存在公共前后缀,因此需要从第2个字符既i=1开始
  while (i < str.length) {
    if (str[prefix] == str[i]) {
      prefix++
      next.push(prefix)
      i++
    } else {
      if (prefix == 0) {
        next.push(0)
        i++
      } else {
        console.log(str[i], '=>', prefix)
        prefix = next[prefix - 1]
      }
    }
  }
  return next
}