数据结构与算法-字符串匹配KMP算法【六】

[数据结构与算法] 字符串的匹配算法(KMP算法)

标签: 实现strStr KMP算法


上一篇文章概述了一下BF算法以及缺点,这篇文章来说明一下KMP算法,对BF算法的优化。

如果你还不知道BF算法是什么: 点击我 先了解匹配字符串的暴力匹配法,然后再看KMP,感觉会更清晰一点。

KMP算法

这个算法,是对BF算法的一个优化,为什么说优化呢,KMP算法当两个字符不相等的时候,i不需要回溯,只需要j回溯,j回溯到什么位置呢?先不看这个问题,先看kmp算法的例子。

假设给定的str是a b a b c a a b c a c b c b
给定的pattern是a b c a c

推导过程手写了一份:

第一步:

i指向当前str的索引,j指向pattern的索引,从0开始相等,然后i++,j++。当i==2 j==2遇到不相等的情况:
kmp-01

第二步:
kmp-02
第三步:

kmp-03

为什么会是这样:

kmp-04
kmp-05

总结,实际上最大前后缀,咱们可以细细想一下上面的例子。

此时str 是a b c x y z a b c x y z a b c d c d
pattern是 a b c x y z a b c d

比较到最后一个字符 x 和 d是不等的。这时候说明,pattern中字符d以前 的字符已经完全和str中的 x字符以前的字符完全匹配。这时候我们只需要关注pattern中j的位置的变化,也就是求d字符的索引j的下一个位置,也就是next表,也即使说这个d字符对应的j要移动到的位置的关系表。 dj的索引,如果和对应 的i值不相等,那么应该跳回到j以前的字符的最大公共前缀,也就是next表中的 next[j]的值。

BF算法是这样:

	const strStr = (str, pattern) =>  {
		let i = 0, j = 0
		while (i < str.length && j < pattern.length ) {
			if (str[i] == pattern[j]) {
				i++
				j++
			} else {
				i = i - j + 1 // 回溯到当前比较的起点的下一个位置
				j = 0
			}
		}
		if (j == pattern.length) return i - j
		return -1
	}

利用KMP算法的话,i 不需要变化,j回溯,代码会是下面这样:

	const strStr = (str, pattern) =>  {
		let i = 0, j = 0
		while (i < str.length && j < pattern.length ) {
			if (str[i] == pattern[j] || j == -1) { 
			// 为什么j==-1为判定条件之一,先忽略,往后面看
				i++
				j++
			} else {
				// 优化之前
				// i = i - j + 1 // 回溯到当前比较的起点的下一个位置
				// j = 0
				
				// 优化之后	
				j = next[j] // next表存储着这个位置的下个回溯位置
			}
		}
		if (j == pattern.length) return i - j
		return -1
	}

那么问题来了,怎么求这个next表,也就是对应j位置的下一个回溯位置,代码如下:

function getPrefixTable (pattern) {
  
  let prefix = []
  prefix[0] = 0
  let len = 0; // 最大前后缀的相等的数目 【包含当前字母的字符串的最大相等前后缀的长度】
  let i = 1; // i 从pattern的第二个字母开始比较,为什么,因为第一个字母比较不相等j不需要回溯,i和j都往后移动一个即可。
  while (i < pattern.length) {
    /**
     *  https://www.youtube.com/watch?v=3IFxpozBs2I  7:59s
     *  ABABCA [这个字符] 的prefix值是啥,这个字符前面的最大重复前后缀为1 len = 1
     *  如果这个字符 等于 pattern[1] 那么必然有两个相同的前后缀
     *  所以每个位置的相同的前后缀是和前面一个字符的相同最大前后缀长度相关联的 
     * 
     */
    if (pattern[i] == pattern[len]) {
      len++
      prefix[i] = len
      i++;
    } else {
      // len 可能等于 -1 
      // WRONG
      // len = prefix[len - 1]

      if (len > 0) {
        len = prefix[len - 1]
      } else {
        //  let t = 'ABABCABAA' i = 1 ; len = 0  程序进入的时候就死循环
        // 所以
        prefix[i] = len 
        i++;
        // prefix[i] = 0 都可以
      }
    }
  }
  console.log(prefix)
  return moveTable(prefix)
}


// j对应的next表的值,求的是 pattern[0] 到 pattern[j - 1] 的最大公共字符串。
// 没有调用moveTable之前,是包含了自己的字符
function moveTable(prefix) {
  let j = prefix.length;
  for (let i = j - 1; i > 0; i--) {
    prefix[i] = prefix[i - 1]
  }
  prefix[0] = -1
  return prefix
}

let next =  getPrefixTable(pattern)

好的我知道你肯定一脸懵逼,这个推导过程while循环内的,if语句判定相等的过程是容易理解的,不相等的情况有点迷糊,直接看这个油管up主黄浩杰的讲解 ,楼主也是看了很多视频和文章,感觉就这个最容易理解点。

资料:

王卓老师-KMP算法
油管up主黄浩杰的讲解

王卓老师的讲解bp算法是好理解,KMP的算法原理也好理解【很关键,知道怎么跳过无意义的比较】,但是next表也就是j回溯位置的数组求解就有点懵,而且王老师计算是索引1开始,数组一般是0开始,需要转换一下。

建议先看王老师的原理,然后看油管黄浩杰的 next表的求解原理。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值