KMP算法(字符串匹配)

本文探讨了如何通过KMP算法(Knuth-Morris-Pratt)改进字符串匹配的暴力搜索,利用部分匹配值减少不必要的字符比较,从而将复杂度降低。通过理解前缀和后缀的概念,以及 nxt 数组的构建,实现高效字符串查找。
摘要由CSDN通过智能技术生成

a0e29592dad95272008bf4877502386d.png

字符串匹配是常见的算法题,就有一个字符串判断里面是否包含另一个字符串。

举例来说,有一个字符串"AAAAAABC"(主串),我想知道,里面是否包含另一个字符串"AAAB"(模式串)?对主串和模式串做匹配。

708e3db1ef691cc3a796ed2e3b6c9ce3.png

首先,字符串 "AAAAAABC" 的第一个字符与搜索词 "AAAB" 的第一个字符,进行比较。

AAAAAABC
AAAB

字符串有一个字符与搜索词的第一个字符相同,接着比较字符串和搜索词的下一个字符,还是相同。直到字符串有一个字符,与搜索词对应的字符不相同为止。

当字符串的索引为 3 的时候发现不相等,这时,最自然的反应是,将搜索词整体后移一位,再从头逐个比较。

AAAAAABC
 AAAB

基于这个想法我们可以得到以下的程序:

function bf(ts, ps) {
  let t = ts;
  let p = ps;
  let i = 0; // 主串的位置
  let j = 0; // 模式串的位置
  while (i < t.length && j < p.length) {
    if (t[i] === p[j]) { // 当两个字符串相同,就比较下一个
      i++;
      j++;
    } else {
      i = i - j + 1; // 一旦不匹配,i后退
      j = 0; // j归0
    }
  }
  if (j === p.length) {
    return i - j
  } else {
    return -1;
  }
}


console.log(bf('AAAAAABC', 'AAAB'))

上面的程序是没有问题的,但不够好!这是暴力解法复杂度 O(nm) 的。这太慢了!


d3a48de2bb20a666ee62e652cfee6c80.png

我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。因此,我们考虑降低比较的趟数。

跳过不可能成功的字符串比较

有些趟字符串比较是有可能会成功的;有些则毫无可能。而如果我们跳过那些绝不可能成功的字符串比较,则可以希望复杂度降低到能接受的范围。

54df608677946e93fa193ace4f4c67e8.png

一个基本事实是,当 d 不匹配时,你其实知道前面五个字符是"abcab"。如果是人为来寻找的话,肯定不会再把 i 移动到索引为1,我们会直接移动到索引为3!就可以来到第二个"ab"的位置。

所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道指针要移动到哪?

移动位数 = 已匹配的字符数 - 对应的部分匹配值

首先,要了解两个概念:"前缀"和"后缀"。"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

"abcab"的前缀为[a, ab, abc, abca],后缀为[bcab, cab, ab, b],共有元素为"ab",长度为2;

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"abcab"之中有两个"ab",那么它的"部分匹配值"就是2("ab"的长度)。搜索词移动的时候,第一个"ab"向后移动到索引为3(字符串长度5-部分匹配值2),就可以来到第二个"ab"的位置。

根据nxt数组加快字符串匹配。

function getNext(p) {
  let nxt = [];
  nxt.push(0); // next[0] 必然是0
  let x = 1; // 因此 nxt[1] 开始求
  let now = 0;
  while (x < p.length) {
    if (p[now] === p[x]) { // 如果 p[now] == p[x] ,则可以向右扩展一位
      now += 1
      x += 1
      nxt.push(now)
    } else if (now) {
      now = nxt[now - 1] // 缩小 now,改成 nxt[now - 1]
    } else {
      nxt.push(0) // now 已经为0,无法再缩小了, 故 nxt[x] = 0
      x += 1
    }
  }
  return nxt
}


console.log(getNext('abcab'))
// [ 0, 0, 0, 1, 2 ]

根据nxt数组移动标尺。

function bf(ts, ps)
  let t = ts;
  let p = ps;
  let i = 0; // 主串的位置
  let j = 0; // 模式串的位置
  let nxt = getNext(ps)
  while (i < t.length && j < p.length) {
    if (t[i] === p[j]) { // 当两个字符串相同,就比较下一个
      i++;
      j++;
    } else {
      // 失配了
      if (j) {
        j = nxt[j - 1] // 根据nxt数组移动标尺
      } else {
        i++; // ps[0]失配了,直接把标尺往右移动一位
      }
    }
  }
  if (j === p.length) {
    return i - j
  } else {
    return -1;
  }
}

5fbd27821d9d400f308699c6a185e59f.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值