快速字符串匹配一: 看毛片算法(KMP)
前言##
由于需要做一个快速匹配敏感关键词的服务,为了提供一个高效,准确,低能耗的关键词匹配服务,我进行了漫长的探索。这里把过程记录成系列博客,供大家参考。
在一开始,接收到快速敏感词匹配时,我就想到了 KMP 翻译过来叫“看毛片“的算法,因为大学的时候就学过它。听说到它的效率非常高。把原本字符串匹配效率 O(n*m) 缩短到了O(n+m),把✖️变成了➕,真是了不得。
每次我回顾 KMP 算法时,都会发现自己是个小白,或者每次回顾时,都发现上次因为回顾而写的总结居然是错的!所以为了学习快速字符串匹配,并再次温故 KMP ,所以我决定使用 KMP 算法试一试。如果以后在面试的时候,可以将KMP 完整的写出来,那岂不是很牛逼?
孔子说过的“温故而知新” 真的是很有道理的,经过这次回顾,我觉得是时候为此写一篇全新的博客了,因为这次的理解肯定是正确的!
KMP 快是因为啥呢?是因为利用了字符串
公共前后缀的特性,加快了匹配速度,但是转念一想,敏感关键词公共前后缀相等的情况可是很少的呀。那还有必要用KMP 吗?
当然有必要了,所谓技多不压身,了解掌握一种算法准没坏处,而且还可以比较 KMP 和 C# 中String.Contains()
的效率,开拓自己的眼界。
KMP##
以前在学习 KMP 的时候,我也看了网上很多博客,关于这个算法讲解的博客是非常多的,而且讲解的都很细致。奈何我每看过一次,就会忘记一次。所以这次温故,我是完全在纸上画画,自己理解的。毕竟自己的思路不容易忘,而别人的思路总是很容易忘的。并且,理解一个算法,得找到适合自己的角度。
因此我理解 KMP 算法的角度,就是 字符串前缀和后缀,在我的脑子里,用前缀和后缀去理解 KMP 是很容易的。
公共前后缀长度##
前缀和后缀,很容易理解,就是一个字符串的前半部分和后半部分。比如字符串 a b c x y a b c
的前缀有
a
a b
a b c
等等,后缀有
c
b c
a b c
等等。
那么公共前后缀
的意思就是,前缀和后缀相等。在上面这个例子中,公共前后缀 就是 a b c
,长度为3。请注意,公共前后缀 和 回文串是不一样的哦。
a b c x y c b a
的公共前后缀,只是a
,而不是a b c
。
原始的字符串匹配##
了解完 公共前后缀后。暂且放在一旁,去了解一下,原始的字符串匹配。
首先我们把 待匹配的字符串叫做 文本字符串
,匹配的字符串叫做匹配字符串
,比如我们要在 a b c x y a b c x y a
中匹配 a b c x y a b c y
是否存在。
于是 文本字符串 S 就是 :a b c x y a b c x y a
**匹配字符串 ** P 就是: a b c x y a b c y
从肉眼看出来,匹配一定是失败的,因为 匹配字符串 最后一个字母 y
不匹配。
那么原始的字符串匹配过程就是 暴力的一位一位去比。首先,从第一位开始比较:
0 1 2 3 4 5 6 7 8 9 10
↓
S a b c x y a b c x y a
P a b c x y a b c y
↑
第一位,相同比较第二位,一直比到第 8 位
0 1 2 3 4 5 6 7 8 9 10
↓
S a b c x y a b c x y a
P a b c x y a b c y
↑
发现不相同,匹配失败,于是把 匹配字符串 向右移动一位。
0 1 2 3 4 5 6 7 8 9 10
↓
S a b c x y a b c x y a
P a b c x y a b c y
↑
继续重复上面的过程,直到 文本字符串全部遍历完。 这种方法的效率最差的时候是 O( n*m ) ,就是那种每次都是最后一个字符匹配不了的情况。
快速移动##
有没有更快的方法呢? 肯定是有的。但是不着急,我们还是按照上面的步骤,继续走下去。
当 匹配字符串 一直向右移动,移动到第 5 位的时候,终于发现首字母是匹配的情况了。,如下
0 1 2 3 4 5 6 7 8 9 10
↓
S a b c x y a b c x y a
P a b c x y a b c y
↑
其实我们发现,从 文本字符串 第一位之后的 b c x y
其实都没必要匹配的,因为它们和 匹配字符串首字母都不一样,如果可以直接跳过就好了。
那么有什么依据可以直接跳过吗?当然有,之前的 公共前后缀 就发挥作用了。
a b c x y a b c y
中的子串 a b c x y a b c
的公共前后缀是 a b c
,
当一开始,我们发现第 8 位不匹配时,
0 1 2 3 4 5 6 7 8 9 10
↓
S a b c x y a b c x y a
P a b c x y a b c y
↑
我们可以直接将 匹配字符串向右移到第五位,然后再从第 8 位继续进行判断
0 1 2 3 4 5 6 7 8 9 10
| ↓
S a b c x y a b c x y a
P a b c x y a b c y
| ↑
为什么呢?
因为a b c
= a b c
啊,在0 - 7 位的字符串中,它有公共前后缀a b c
,所以我们可以把匹配字符串直接移到 公共后缀的起始位置,也就是 第 5位。
因为前面都不用去看,是一定不匹配的!,只有在第五位开始匹配,才有可能成功。
移动的结果,起始就是将一个字符串的前缀部分,移到和后缀部分对齐。这是成功匹配的前提。你可以想象成 :匹配字符串的子串一直在找自己的后缀,然后靠上去,去匹配。
如下
后缀
a b c x y a b c
a b c x y a b c
前缀
那么这样移动之后,咱们就可以接着 第 8 位 继续往下匹配,而不用从头再来了。所以这种方法下,文本字符串只遍历一次,它不会倒退的。
这就是我所理解的 KMP 算法的核心思想。** KMP 就是利用字符串的前缀和后缀做文章**
具体过程##
KMP 算法的物理核心思想理解了,接下来就是代码实现了。如果保存 匹配字符串的公共前后缀信息,以及它的子串的公共前后缀信息呢?一旦匹配不成功,我怎么确定匹配字符串的子串移动多少位,恰好靠上后缀呢?
第一个问题,用一个数组就可以维护,这是大家都耳熟能详的Next数组
Next 数组,Next[i] 表示的是 从 0 开始到 i 结束的子串 的最长公共前后缀的长度 ,咱们举个栗子就很好理解了。比如下面的字符串 s :
a b a b c a b
Next [ 0 ] => a
,只有一个字符,前缀和后缀的概念这里就不存在了,所以 Next [ 0 ] = 0
Next [ 1 ] => a b
,前缀 a
不等于后缀 b
,所以也是 0,Next[ 1 ] = 0
Next [ 2 ] => a b a
,前缀 a
等于后缀b
,但是前缀a b
不等于后缀b a
,所以 Next[ 2 ] = 1
Next [ 3 ] => a b a b
,前缀 a b
等于后缀 a b
,所以Next[ 3 ] = 3
经过上面的栗子,大概就可以知道 Next 数组是干嘛的了吧。回到之前的匹配字符串 P:
P a b c x y a b c y
它的 Next 数组是啥呢?看着字符串算法一下就可以得出了
Next[9]= { 0 , 0 , 0 , 0 , 0 , 1 , 2 , 3, 0 }
当我们匹配到第8位,也就是最后一个字符的时候,发现不匹配了
0 1 2 3 4 5 6 7 8 9 10
↓
S a b c x y a b c x y a
P a b c x y a b c y
↑
于是我们可以直接将 匹配字符串 向右移动 5位,
0 1 2 3 4 5 6 7 8 9 10
| ↓
S a b c x y a b c x y a
P a b c x y a b c y
| ↑
0 1 2 3 4 5 6 7 8
这个过程其实就是,当 S [ 8 ] != P [ 8 ] 时 ,S [ 8 ] 直接继续和 P [ 3 ] 进行比较,依据就是 Next [ 7 ] 的值是 3
因为子串 P[ 0-7 ] 的最大公共前后缀长度是 3,所以S[ 8 ] 只要和 公共前缀的下一个字符P[ Next[ 7 ] ] (Next[ i ] 同样也是公共前缀的下一个字符的下标,这很好理解)进行比较,也就是 P[ 3 ],这么做的的原因是 P[ 0 ],P[ 1 ],P[ 2 ] 和 S[ 5 ] ,S[ 6 ],S[ 7 ] 是公共前后缀,它们都是一样的!
以上,就是经典 KMP 算法的全部过程。
代码实现##
先是要求 Next[] 数组,怎么求呢?很简单,咱们利用动态规划的思想。Next[ i ]的值要么是在已有最长公共前后缀的字符串基础上 +1 ,要么子串一个符合的都没有,自己另起炉灶。
Next[ i ] 的值有两种情况:
- Next [ i - 1 ]不为 0,说明子串 中有公共前后缀,那我就去字符串中公共前缀的下一个字符串 P[ Next [ i-1 ] ],如果P[ i ] == P [ Next [ i - 1] ],那么公共前后缀长度就+1 也就是 Next [ i ] = Next[ i -1 ]+1。那如果不相等呢?那就去找 P [ Next [ i-1 ] ] 的 Next 值,重复上面的过程,有点递归的意思。其实这个过程就是在找字符串里的公共前缀,看看有没有符合条件的(即P [ i ] == P[Next [ k] ]),没有的话,就在前缀里再去找前缀,直到找到为止,或者发现已经没用公共前缀了,那就跳出来。
- 发现子串没有符合条件,让自己+1的,于是只能从自己开始,看看P[ i ] == P[ 0 ] 如果相等,那就是1 ,如果不相等,那就只能是0 了。
代码实现如下,理解了其实还是很简单的,随时都能手写出来,也不会忘记。
void getNext(string str)
{
next[0]=0;
for(int i=1;i<str.length();i++)
{
int k=next[i-1];
while(k!=0&&str[i]!=str[k])
{
k=next[k-1];
}
if(str[i]==str[k])
next[i]=k+1;
else
next[i]=0;
}
}
这就是我对Next 数组的理解,我觉得这样理解,我能记得住。
还有一种很精简版的Next 数组实现,我不打算贴出来,乱我心志,我就用我能理解,能看懂的代码。
Next 数组求出来,就是字符串匹配了。也很简单哦。
int KMP(string content,string str)
{
getNext(str);
int i=0,j=0;
while(i<content.length()&&j<str.length())
{
if (j==0 ||content[i]==str[j])
{
if(content[i]==str[j])
j++;
i++;
}
else
{
j=next[j-1];
}
}
if(j>=str.length())
{
return i-str.length();
}
else
return -1;
}
j = next [j-1]
就是我上面所有的,移动的过程,其他的也很好理解的。
然后可以用KMP 去通过LeetCode 的一道题目,以检测自己写的代码是否正确:https://leetcode.com/problems/implement-strstr/
总结
KMP 算法就介绍到这里了,关于KMP 还有很多升级的版本。
字符串快速匹配,第一弹,看毛片。回顾一下,感觉以后应该都不会忘记了吧。
开头说的 把 KMP 和C#的 String.Contains 进行PK ,要留到下一篇博文里。下一篇博文将对 字符串的匹配的性能来个大排序,并且见识一下微软的黑科技。
快速字符串匹配一: 看毛片算法(KMP)的更多相关文章
- KMP算法再解 (看毛片算法真是人如其名,哦不,法如其名。)
KMP算法主要解决字符串匹配问题,其中失配数组next很关键: 看毛片算法真是人如其名,哦不,法如其名. 看了这篇博客,转载过来看一波: 原博客地址:https://blog.csdn.net/sta ...
- SDUT OJ 数据结构实验之串一:KMP简单应用 && 浅谈对看毛片算法的理解
数据结构实验之串一:KMP简单应用 Time Limit: 1000 ms Memory Limit: 65536 KiB Submit Statistic Discuss Problem Descr ...
- kmp//呵呵!看毛片算法
以前刚学的时候迷迷糊糊的,一看就懵圈,前几天捡起来的时候 发现还不会 于是研究了两天,自尊心严重受挫,今天的时候 突然一道灵光迸发,居然 感觉好像懂了,于是又琢磨起来 终于 我懂了 呵呵! ...
- KMP算法 字符串匹配(看猫片)
前言 此篇笔记根据自己的理解和练习心得来解释算法,只代表个人观点,如有不足请指出(我刚学QWQ) 浅谈字符串匹配 设想一个场景,假设你是一个净化网络语言环境的管理员,每天需要翻阅大量的文章和帖子来查找 ...
- 关于KMP算法理解(快速字符串匹配)
参考:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html 2016-08- ...
- KMP快速字符串匹配
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现.KMP算法的关键是利用匹配失败后的信息,从错误中吸取经验,尽量减少模式串与主串的匹配次数以 ...
- Boyer-Moore(BM)算法,文本查找,字符串匹配问题
KMP算法的时间复杂度是O(m + n),而Boyer-Moore算法的时间复杂度是O(n/m).文本查找中“ctrl + f”一般就是采用的BM算法. Boyer-Moore算法的关键点: 从右遍历 ...
- Sunday算法解决字符串匹配问题
概述 提起字符串匹配可能更多人会想到KMP算法,该算法时间复杂度为O(m+n),而且也是我们在学习数据结构过程中最早接触到的比较好的算法.但KMP算法需要在模式字符串有关联的情况下,也即模式字符串前后 ...
- [小专题]另一种字符串匹配的思路——Shift-And算法
吐槽:前两天打组队赛遇到一个字符串的题考了这个(见:http://acm.hdu.edu.cn/showproblem.php?pid=5972 ) 当时写了个KMP瞎搞然后TLE了(害),赛后去查了 ...
随机推荐
- MediatR一个.net中简单好用的中介者模式实现方案
MediatRGit地址:https://github.com/jbogard/MediatR 1.安装妞盖特包 一般来说只需要安装一个MediatR就行了,.net core程序需要再安装一个Med ...
- C++模版的用法
模板是实现代码重用机制的一种工具,实质就是实现类型参数化,即把类型定义为参数. C++提供两种模板:函数模板,类模板 函数模板 template <typename T> T myMax( ...
- [Vue 牛刀小试]:第十六章 - 针对传统后端开发人员的前端项目框架搭建
一.前言 在之前学习 Vue 基础知识点的文章中,我们还是采用传统的方式,通过在 html 页面上引用 vue.js 这个文件,从而将 Vue 引入到我们的项目开发中.伴随着 Node.js 的出现, ...
- Jmeter实时监控+SpringBoot接口性能实战
性能测试 Jmeter实时监控+SpringBoot接口性能实战 自动化 SpringBoot Java Jmeter实时监控+SpringBoot接口性能实战 一.实验目的及实验环境 1.1.实验目 ...
- c++学习书籍推荐《超越C++标准库:Boost库导论》下载
<超越C++标准库Boost库导论>不仅介绍了Boost库的功能.使用方法及注意事项,而且还深入讨论了Boost库的设计理念.解决问题的思想和技巧以及待处理的问题.因此,本书是一本了解Bo ...
- Balking设计模式
import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayLi ...
- while 循环,运算符,字符串的格式化练习
1.判断下列逻辑语句的结果,一定要自己先分析 1)1 > 1 or 3 < 4 or 4 > 5 and 2 > 1 and 9 > 8 or 7 < 6 Ture ...
- RabbitMQ(一):RabbitMQ快速入门
RabbitMQ是目前非常热门的一款消息中间件,不管是互联网大厂还是中小企业都在大量使用.作为一名合格的开发者,有必要对RabbitMQ有所了解,本文是RabbitMQ快速入门文章. RabbitMQ ...
- 【CYH-01】小奔的国庆练习赛:赛后标程
前排鸣谢@找寻 大佬 emm-由于头一次举办公开赛所以--准备不是很充分,所以说题解也没有备好,在这里表示歉意. 欢迎大家来发布题解,在此我们可以提供AC代码,供大家参考. T1 解析:这一题可能栈溢 ...
- 题解 P5016 【龙虎斗】
首先祝各位大佬noip有个好成绩吧 当时比赛有个大数据,蒟蒻我暴力居然过了,好激动 这题一定要注意开long long (那个大数据就是我开long long才过的) 还有刚开始应设置答案为m(见解析 ...