【数据结构与算法基础】模式匹配问题与KMP算法

前言

数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。

也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。

此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。

欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。


这一篇,我们来讨论一个问题——字符串的模式匹配问题,同时介绍一种经典算法KMP算法。

说起KMP算法,博主还真是有不少话想说。

咱就先不说博主这是第五次才把他学会,就这个算法名字而言,它就经常被戏称为 看毛片 算法。但其实他是三位发明该算法的大佬(Knuth,Morris,Pratt)名字的简写。

这个算法简洁、高效、且优美。通过定义一个巧妙地函数,完美的解决了一个字符串的基础难题:模式匹配。那么就让我们先从这个问题入手吧。

模式匹配问题

模式匹配问题很好理解。

给定两个字符串:模式串P和文本串s,求出p在s中出现的位置

举个例子:

给 定 文 本 串 S : q w e r a b c d a b c r e w q 给 定 模 式 串 P : a b c 问 P 在 S 中 所 有 的 出 现 位 置 给定文本串S:qwerabcdabcrewq\\ 给定模式串P:abc\\ 问P在S中所有的出现位置 S:qwerabcdabcrewqP:abcPS

对于这个例子,一眼看去就能找到所有答案;

在这里插入图片描述

两个答案分别是4和8;

但是计算机可没有“一眼看去”的技能,他只能通过算法来得到这个问题的答案。

朴素的模式匹配算法

用算法来实现模式匹配的任务,一个最朴素的想法,是将模式串逐位的放在文本串上进行验证。

也就是这样:

在这里插入图片描述

这个算法实现起来不难,两重循环,分别固定模式串起始位置,验证匹配。

//C
void match(char * p,char * s){
   
	int lp = strlen(p);
	int ls = strlen(s);
	for(int i = 0;i < ls - lp + 1;i++){
   
		for(int j = 0;j < lp;j++){
   
			if(s[i + j] != p[j])
				break;
			if(j == lp - 1)
				printf("%d ",i);
		}
	}
}

运行:
在这里插入图片描述
答案没有问题。

但是朴素算法的弊端就在于其复杂度太高,从刚才的算法实现中不难看出其复杂度为 O(nm) 其中n为模式串的长度,m为文本串的长度。

这个复杂度伴随着文本串和模式串的增长而成平方增加。这种复杂度是不能接受的!!!

究其原因就在于它在执行过程中进行了太多次没有意义的跳转和比较,有些位置的比较实际上是不需要的

比如,对于如下匹配:

文 本 串 : a b a b a b a a c 文本串:abababaac abababaac
模 式 串 : a b a a 模式串:abaa abaa

当第四个字符失配时,按照朴素算法的思路,应该放弃将模式串与文本串第一位对应,转为对应第二位再进行判定,但实际上,模式串并不需要在第二位上面继续比较,而可以直接去第三位的位置进行这个过程,因为那里是以a开头的。

要改进这个思路,就需要让模式串失配时,在保证算法正确性的基础上,让模式串尽可能多的向后移动,进而提高效率。这就需要利用已有的信息或者模式串本身的特点。

为了解决这个问题,下面我们需要引入一个函数,它能体现字符串本身的特性,并且能够很好地指导该字符串在匹配过程中失配时的行为。

KMP算法

对朴素算法改进的讨论

(该过程是博主本人对引出算法的过渡讨论,不影响后续对失败函数的叙述,可以选择观看。当然,同博主一起讨论这部分内容可能会帮助理解算法

遵从上面的指导思想,我们想要在模式串失配时可以尽可能快速的移动到下一个合适的地方进行后续匹配。

在刚才的例子中,我们找到了文本串中下一个同模式串开头的相同的地方。但不能总是使用这个策略,因为这个策略仍旧要比较所有的后续字符,本质上与普速算法没有区别。

不妨设:
模 式 串 P : p 0 p 1 p 2 . . . p m 模式串P:p_0p_1p_2...p_m P:p0p1p2...pm
文 本 串 S : s 0 s 1 s 2 . . . . s n 文本串S:s_0s_1s_2....s_n S:s0s1s2....sn

进一步想,如果可以,直接跳到下一个与头两个元素相同的位置,将会比跳到下一个与首元素相同的位置更优,会再少比较一次。

. . . s k s k + 1 s k + 2 . . . s n ...s_ks_{k+1}s_{k+2}...s_n ...sksk+1sk+2...sn
       p 0 p 1 . . . p m \;\;\;p_0p_1...p_m p0p1...pm
( s k = p 0 , s k + 1 = p 1 ) (s_k = p_0,s_{k+1}=p_1) (sk=p0,sk+1=p1)

那么三个呢,四个呢?肯定更优。甚至于如果直接能匹配m个,那任务就完成了2333。再仔细想,这样的想法本质上是希望模式串能够跳转到匹配其最长前缀的地方。

虽然想法是好的,但是问题就在于无从得到在文本串中出现前缀的所有地方,一来模式串中的前缀有m个,二来前缀也是字符串,匹配前缀本身又是个规模更小的模式串匹配问题,这导致问题不但没有简单,反而更复杂了。

所以对于找到下一个匹配某个前缀的位置的问题,我们需要换个思路。

注意到在朴素算法匹配的过程中,未被扫描到的文本串内容与模式串的匹配关系是未知的,而被匹配过的内容却是已知的。

举个例子:
在这里插入图片描述

在当前匹配中,前三个位置是匹配成功的,在模式串第四位失配了。此时在文本串中4之前的位置匹配结束,我们不关心,7之后的内容未经匹配,匹配结果未知。但是在4到6之间是已经同模式串匹配过的地方,匹配关系是明确的。

如果能在已经知道匹配关系的区间,找到下一个模式串的前缀,并且以失配的前一位为结尾的位置,就可以直接跳过去了。

用一张图表示:
在这里插入图片描述

稍加思索不难发现一个问题,这个位置只和模式串的字符排列特点决定的,与文本串无关。换句话说,是模式串本身的某个前缀中,其某个真前缀与对应长度的后缀内容相同。

即:
对 于 模 式 串 P : p 0 p 1 p 2 . . . p m 对于模式串P:p_0p_1p_2...p_m P:p0p1p2...pm
任 取 0 < j ≤ m , 可 能 存 在 0 ≤ k < j , 满 足 任取 0 < j≤ m,可能存在0≤k<j,满足 0<jm,0k<j
p 0

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值