KMP字符串匹配算法

前文

字符串匹配,其实就是string的findIndex(pattern)方法。暴力法就是从string的每个点位开始匹配pattern,如果不一样就从下个点位开始匹配,最坏情况是O(m*n),比如每次都匹到最后一个字符才发现不一样。

kmp(是三个发明者的首字母)可以从头匹到尾不回头。这个算法有两个步骤,分别要遍历一遍string和pattern,所以复杂度是O(m+n)。

一、遍历string

假设现在的情况是,我匹配进pattern,但是匹到?和!的地方不一样了。假设恰好我就是知道(tip:这个是可以提前算出来的,下面说)此时pattern前面这一子串里有一节相同的前缀和后缀;由于我们是匹配到这的,string和pattern的后缀也一定是一样的,所以这个前缀和string的后缀也是一样的。那么接下来的匹配,我就可以把前缀搬过来,直接匹配?和前缀后的下一个字符了。这个就是kmp的原理。

那也许中间也有地方能匹配呢?这不会漏吗。比如下图中,抛开后缀不谈,恰好中间有一段前缀2相同,我们知道,之所以说它是前缀,就是因为后面有字符不一样,比如下图中的a和b,既然后面都不一样了,那这个前缀也就没用了。那假设a和b也恰好一样,那它又变成了一个前缀,后面总有不一样的。如果真后面都是一样的,它反而就变成一个后缀了,又回到了上面的情况,所以大可放心。

二、遍历pattern

那么要实现上面的关键就在于,对于pattern每个位置,我们都需要知道它前面子串的最长公共前后缀(的长度即可),即一个长度为n的数组。实话说,也可以暴力点,但为了线性复杂度,也依然是从头到尾遍历一遍。用两个指针i=1、j=0框一个子串:

  1. 一开始,j=0,如果ij指向的字符不相同,说明没有公共前后缀,只有i+1。

  2. 如果相同,那就是有一个公共字符,ij都+1,记录j为i前面子串的公共前后缀长度。

  3. 比较tricky的是,如果在j不为0的时候,匹配到不相同的(如下面两张图),该怎么办呢?第一想法可能是让j直接回到0重新匹配,如图1;但在图2里就不行,此时长度为6时不相同,并不能直接置为0(+1),实际是aa=2,也就是说,虽然这个长串不行,但可能这里边还有短串行,所以要缩小范围继续尝试。

    算法实际过程是倒查,把j放到j前一位的位置上,即j=dict[j-1],并且每次要重新比较j+1和i是否相同,不相同还要继续这个操作,除非j=0。比如这里要先把j放到dict[j-1]=dict[4]=2,比较b和a,不同;再放到dict[j-1]=dict[1]=1,此时a相同,j最终为1+1=2。

    我想了很久这个倒查是什么含义呢?视频里也没有讲。从结果倒推一下,这个倒查dict[j-1]其实是找了j-1(上一轮)的最大公共前后缀。我们的目的是,当前缀aabaab和后缀aabaaa不能匹配时,找到最大公共的前缀的前缀后缀的后缀。如果我们把它俩拆看看成aabaa+b和aabaa+a,我们要找的其实是最大公共的第一个aabaa的前缀和第二个aabaa的后缀(就是这里很tricky),而这就是上一轮的最大公共前后缀dict[j-1]。

    这个语言实在很难形容......举个例子,aabaa+b和aabaa+a,现在等号左边都相同,但跟着的字符不同;我们只能尝试缩小加号左边,再看看跟着的字符是否相同,只是缩小的时候,前缀的aabaa是往左缩的,后缀的aabaa是往右缩的,它们会变得不一样,但我们要找公共串嘛,肯定得是一样的,所以我们就希望它们缩到一个一样的时候......所以这就是找aabaa的最大公共前后缀了嘛!比如现在缩到aa,aa+b和aa+a还是不同,只能再缩,变成a+a和a+a,此时相同即可停止。

www.bilibili.com/video/BV18k...

www.youtube.com/watch?reloa...

code

js 复制代码
var strStr = function(haystack, needle) {
  const m = haystack.length
  const n = needle.length

  const dict = new Array(n).fill(0)
  let j = 0
  for (let i = 1; i < n; i++) {
    while(needle[i] !== needle[j] && j > 0) {
      j = dict[j - 1]
    }
    if (needle[i] === needle[j]) {
      j++
    }
    dict[i] = j
  }

  j = 0
  for (let i = 0; i < m; i++) {
    while(haystack[i] !== needle[j] && j > 0) {
      j = dict[j - 1]
    }
    if (haystack[i] === needle[j]) {
      j++
    }
    if (j === needle.length) {
      return i - needle.length + 1
    }
  }
  return -1
};

leetcode.cn/problems/fi...

最后代码长这样,看起来还是蛮简单的。

第一个循环是计算字典,这是几个case合并的结果,可以按case写出来再合并。第二个循环是遍历字符串,公共前后缀可能不止一对,先从最大的前缀开始查。

......

可以看到这两个循环其实是一样的东西,都是因为前缀可能不止一对,先从最大的开始倒查,最后如果相同则+1。这样说来,实在是太简单了,妙啊。

又试了一遍,已经可以轻松默写了......

相关推荐
_WndProc5 分钟前
C++ 日志输出
开发语言·c++·算法
m0_748247807 分钟前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
努力学习编程的伍大侠18 分钟前
基础排序算法
数据结构·c++·算法
ZJ_.35 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
XiaoLeisj1 小时前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
Jasmine_llq1 小时前
《 火星人 》
算法·青少年编程·c#
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
闻缺陷则喜何志丹1 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
还是大剑师兰特2 小时前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用