KMP算法学习笔记

KMP算法学习笔记

Knuth-Morris-Pratt算法(即KMP算法)是一个用于字符串匹配(求给定串中是否包含另外一个串)的算法,由暴力匹配法优化而来。

暴力匹配法:
本文将被匹配的串称为主串,去匹配主串的串叫模式串。

  1. 将模式串第一位逐个与主串中的字符比对。
  2. .如果匹配,则遍历模式串中的每个字符与主串对应的字符比对。
  3. 如果有某一位比对失败,则主串与模式串都回退到第一位,重复步骤1,直到都比对成功或者主串已经遍历完毕。

但这样的过程有一个问题,那就是一旦比对失败,主串与模式串都要回退到第一位,抹除掉之前的匹配结果。如果字符串很长,这个回退过程就会十分浪费时间且毫无意义,而且之前的匹配结果,可以帮助
快速的跳过某些根本没有必要的比较。基于这样的理念,KMP算法诞生了。

KMP算法: 利⽤已经部分匹配这个有效信息,保持主串的下标不回退, 通过修改模式串的下标,
让模式串尽可能往后移动最多的距离。而整个KMP的重点就在于当某⼀个字符与主串不匹配时, 如何知道模式串下标要移动到哪⼉。

当匹配失败时, j要移动的下⼀个位置p,存在着这样的性质:模式串最前⾯的p-1个字符和j之前的最后p-1个字符是⼀样的

在这里插入图片描述
因此,有必要给模式串配一个数组用于储存每一个元素在匹配失败时应当回退到的位置,即数组next(好像专业一点的称呼是叫部分匹配表(Partial Match Table,简称PMT),汗,反正应该是一个东西吧,下面统一叫数组next)。
那么next数组的值该如何确定呢?首先贴一下前缀后缀的概念(直接贴图了)
在这里插入图片描述
当不匹配时,j应当跳到的位置是:模式串中当前位置之前的子串前缀和后缀中长度最长的共有元素的长度+1

举个例子:
如果模式串为abcabb,当j = 6时, 发⽣不匹配, 那么j应该跳到的位置是”abcab”的前缀和后缀最⻓的共有元素的⻓度+1.
对于 ”abcab”, 我们有
前缀: a, ab, abc, abca
后缀: bcab, cab, ab, b
最⻓共有元素是 ab, ⻓度为2.
则位置6上的next值为2+1=3

以此类推,可以推出模式串每个元素的next值,当模式串匹配错误时,就可以通过访问数组next来知晓自己应当回退的位置。
KMP算法:

find_subString_kmp(s, t, next[])
	n = s.length, m = t.length;
	j = 1, i = 1;
	while(i ≤ n && j ≤ m)
		if(j == 0 || s[i] == t[j])
		i++,j++;
	else
		j = next[j];
 if(j == m + 1)
	return i - j; 
 return -1;

如何求next[] 数组:

get_next(t)
	m = t.length;
 	next[1] = 0;
 	j = 0, i = 1;
 	while(i ≤ m)
 		if(j == 0 || t[i] == t[j])
 			next[++i] = ++j;
 		else
 			j = next[j]; 
 	return next;

求next数组的这段代码十分微言大义……网上有很多各种版本的讲解,不过我看起来都感觉有点复杂+理解不能……在这里写上我自己的思考过程吧。
我通常喜欢假装自己是机器来按顺序运行代码和循环。与此同时的是,不要忘记next[]数组的含义:模式串中当前位置之前的子串前缀和后缀中长度最长的共有元素的长度+1.
该段代码的数组都是从1开始用的,为了防止乱!!!

  • 假设模式串为abcabb,则m为该字符串的长度,为6,同时将模式串第一个字符对应的next值设成了0,这是理所当然的,毕竟如果第一个字符都没对上,也退无可退了,根本用不上next值。
  • 接下来需要搞明白i和j的嵌套关系和具体含义。i指向的是模式串每一位的下标,需要i来给每个字符赋对应的next值,而j则指向是每个当前位置之前的子串每一位的下标,意在判断next的值该怎么赋,即子串里最长的前缀和后缀共有元素的最大长度。
    即:i的范围是整个模式串,j的范围是i所在位置之前的子串。
  • 最后,需要搞明白ifelse的含义,当然这都是在我通过后面的一步步推演+思考后得出来的……
  • 这里的if条件有两个,j==0和t[i]==t[j],先解释t[i]==t[j]吧。这个条件用来判断有没有最长共有元素。因为j的活动范围是i所在位置之前的子串,且从头开始,所以一定可以保证j是从子串最左边,i是从子串最右边开始的,通过t[i]和t[j]的比对,即子串的第一位和最后一位,判断是否可能存在共有元素。根据前面说的前缀后缀的定义,前后缀想找共有元素,肯定需要从第一和最后的元素开始比较,只有他俩相同了,才可能存在更长的共有元素。如果不相等,就可以直接走else让i继续往后挪位了。
  • 如果首部和尾部的元素相等了,即满足t[i]==t[j],则执行if分支,将i和j都往右挪一位,同时把i刚才位置的共有元素最大长度记入next数组里(由于一切都是从最左边开始的,此时一定可以记录到准确的next值),随后进行新一轮比较,直到t[i]和t[j]不再相等,或者模式串已经遍历完了。

暂时就想到这么多,再写下去怕是要前言不搭后语了,等有新感悟再整理措辞吧。

下面放一下我自己推演的整个算法过程,真是费时费力的笨办法……
字符串为abcabb。

  1. i=1,j=0符合if条件,于是next[2]=1,i=2,j=1.
    这里加重一下,++i是先加再赋值,i++是先赋值再加,我总是浑忘掉,惭愧……

  2. i=2,j=1,这里需要看t[i]和t[j]是否相等,咱们的字符串第一个是a,第二个是b,即t[2]=b,t[1]=a,很显然不相等,于是走向else分支,j=next[1]=0,next[1]是最开始赋过的值。

  3. i=2,j=0,j又等于0,于是走if分支,next[3]=1,i=3,j=1.

  4. i=3,j=1,比较t[i]和t[j],t[3]=c,t[1]=a,不相等,于是走else,j=next[1]=0.

  5. i=3,j=0,j又等于0,走if分支,next[4]=1,i=4,j=1.

  6. i=4,j=1,比较t[i]和t[j],t[4]=a,t[1]=a,居然相等了,于是走if分支,next[5]=2.
    细心的小伙伴这时候应该能看出来了,i=4情况下的子串为abca,出现了共有元素a,长度为1,根据前面的规律,长度+1,1+1=2,而2正是next[5]的值(每次我看代码的时候都有这种柳暗花明的妙感……可能这就是算法的魅力吧,虽然我总怀疑我学习的方式有问题,哈哈哈)。

  7. i=5,j=2,比较t[i]和t[j],t[5]=b,t[2]=b,又一次相等了,走if分支,next[6]=3。其实next数组的赋值到这已经结束了,这里同样可以发现,i=5情况下的子串是abcab,共有元素是ab,长度为2,2+1=3,next[6]的值是3.

短短几行代码,就蕴含这么多东西,我是真的服了。怪不得这算法要三个人才能发明出来(笑)
先记录到这里吧,如果你看到了,希望能帮到你理解这个算法!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值