KMP算法(字符串匹配)

前端精髓

共 2307字,需浏览 5分钟

 · 2022-02-14


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


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



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

AAAAAABCAAAB


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


当字符串的索引为 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) 的。这太慢了!



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


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


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



一个基本事实是,当 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;  }}


浏览 34
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报