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。这样说来,实在是太简单了,妙啊。

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

相关推荐
阿史大杯茶36 分钟前
Codeforces Round 976 (Div. 2 ABCDE题)视频讲解
数据结构·c++·算法
LluckyYH1 小时前
代码随想录Day 58|拓扑排序、dijkstra算法精讲,题目:软件构建、参加科学大会
算法·深度优先·动态规划·软件构建·图论·dfs
万叶学编程1 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
转调1 小时前
每日一练:地下城游戏
开发语言·c++·算法·leetcode
不穿格子衬衫2 小时前
常用排序算法(下)
c语言·开发语言·数据结构·算法·排序算法·八大排序
wdxylb2 小时前
使用C++的OpenSSL 库实现 AES 加密和解密文件
开发语言·c++·算法
aqua35357423582 小时前
蓝桥杯-财务管理
java·c语言·数据结构·算法
CV金科2 小时前
蓝桥杯—STM32G431RBT6(IIC通信--EEPROM(AT24C02)存储器进行通信)
stm32·单片机·嵌入式硬件·算法·蓝桥杯
sewinger2 小时前
区间合并算法详解
算法