1. 引言

字符串匹配是极为常见的一种模式匹配。简单地说,就是判断主串\(T\)中是否出现该模式串\(P\),即\(P\)为\(T\)的子串。特别地,定义主串为\(T[0 \dots n-1]\),模式串为\(P[0 \dots p-1]\),则主串与模式串的长度各为\(n\)与\(p\)。

暴力匹配

暴力匹配方法的思想非常朴素:

  1. 依次从主串的首字符开始,与模式串逐一进行匹配;
  2. 遇到失配时,则移到主串的第二个字符,将其与模式串首字符比较,逐一进行匹配;
  3. 重复上述步骤,直至能匹配上,或剩下主串的长度不足以进行匹配。

下图给出了暴力匹配的例子,主串T="ababcabcacbab",模式串P="abcac",第一次匹配:

第二次匹配:

第三次匹配:

C代码实现:

int brute_force_match(char *t, char *p) {
int i, j, tem;
int tlen = strlen(t), plen = strlen(p);
for(i = 0, j = 0; i <= tlen - plen; i++, j = 0) {
tem = i;
while(t[tem] == p[j] & j < plen) {
tem++;
j++;
}
// matched
if(j == plen) {
return i;
}
}
// [p] is not a substring of [t]
return -1;
}

时间复杂度i在主串移动次数(外层的for循环)有\(n-p\)次,在失配时j移动次数最多有\(p-1\)次(最坏情况下);因此,复杂度为\(O(n*p)\)。


我们仔细观察暴力匹配方法,发现:失配后下一次匹配,

  • 主串的起始位置 = 上一轮匹配的起始位置 + 1;
  • 模式串的起始位置 = 首字符P[0]

如此未能利用已经匹配上的字符的信息,造成了重复匹配。举个例子,比如:第一次匹配失败时,主串、模式串失配位置的字符分别为 ac,下一次匹配时主串、模式串的起始位置分别为T[1]P[0];而在模式串中c之前是ab,未有重复字符结构,因此T[1]P[0]肯定不能匹配上,这样造成了重复匹配。直观上,下一次的匹配应从T[2]P[0]开始。

2. KMP算法

KMP思想

根据暴力方法的缺点,而引出KMP算法的思想。首先,一般化匹配失败,如下图所示:

在暴力匹配方法中,下一次匹配开始时,主串指针会回溯到i+1,模式串指针会回退到0。那么,如果不让主串指针发生回溯,模式串的指针应回退到哪个位置才能保证正确匹配呢?首先,我们从上图中可以得到已匹配上的字符:

\[T[i \dots i+j-1] = P[0 \dots j-1]
\]

KMP算法思想便是利用已经匹配上的字符信息,使得模式串的指针回退的字符位置能将主串与模式串已经匹配上的字符结构重新对齐。当有重复字符结构时,下一次匹配如下图所示:

从图中可以看出,下一次匹配开始时,主串指针在失配位置i+j,模式串指针回退到m+1;模式串的重复字符结构:

\begin{equation}

T[i+j-m-1 \dots i+j-1] = P[j-m-1 \dots j-1] = P[0 \dots m]

\label{eq:overlap}

\end{equation}

且有

\[T[i+j] \neq P[j] \neq P[m+1]
\]

那么应如何选取\(m\)值呢?假定有满足式子\eqref{eq:overlap}的两个值\(m_1 > m_2\),如下图所示:

如果选取\(m=m_2\),则会丢失\(m=m_1\)的这一种字符匹配情况。由数学归纳法容易知道,应取所有满足式子\eqref{eq:overlap}中最大的\(m\)值。


KMP算法中每一次的匹配,

  • 主串的起始位置 = 上一轮匹配的失配位置;
  • 模式串的起始位置 = 重复字符结构的下一位字符(无重复字符结构,则模式串的首字符)

模式串P="abcac"匹配主串T="ababcabcacbab"的KMP过程如下图:

部分匹配函数

根据上面的讨论,我们定义部分匹配函数(Partial Match,在数据结构书[2]称之为失配函数):

\[f(j) = \left\{ {\matrix{
{\max \{ m \} } & P[0\dots m]=P[{j-m}\dots {j}],0\le m < j \cr
{-1} & else \cr
} } \right.
\]

其表示字符串\(P[0 \dots j]\)的前缀与后缀完全匹配的最大长度,也表示了模式串中重复字符结构信息。KMP中大名鼎鼎的next[j]函数表示对于模式串失配位置j+1,下一轮匹配时模式串的起始位置(即对齐于主串的失配位置);则

\[next[j] = f(j)+1
\]

如何计算部分匹配函数呢?首先来看一个例子,模式串P="ababababca"的部分匹配函数与next函数如下:

j 0 1 2 3 4 5 6 7 8 9
P[j] a b a b a b a b c a
f(j) -1 -1 0 1 2 3 4 5 -1 0
next[j] 0 0 1 2 3 4 5 6 0 1

模式串的f(j)满足\(P[0 \dots f(j)]=P[j-f(j) \dots j]\),在计算f(j+1)分为两类情况:

  • 若\(P[j+1]=P[f(j)+1]\),则有\(P[0 \dots f(j)+1]=P[j-f(j) \dots j+1]\),因此f(j+1)=f(j)+1
  • 若\(P[j+1] \neq P[f(j)+1]\),则要从\(P[0 \dots f(j)]\)中找出满足P[f(j+1)]=P[j+1]f(j+1),从而得到\(P[0 \dots f(j+1)]=P[j+1-f(j+1) \dots j+1]\)

其中,根据f(j)的定义有:

\[P[j]=P[f(j)]=P[f(f(j))]=\cdots = P[f^k(j)]
\]

其中,\(f^k(j)=f(f^{k-1}(j))\)。通过上面的例子可知,函数\(f^k(j)\)是随着\(k\)递减的,并最后收敛于-1。此外,P[j]p[j+1]相邻;因此若存在P[f(j+1)]=P[j+1],则必有

\[f(j+1)=f^k(j)+1
\]

为了求满足条件的最大的f(j+1),因\(f^k(j)\)是随着\(k\)递减的,故应为满足上式的最小\(k\)值。

综上,部分匹配函数的计算公式如下:

\[f(j) = \left\{ {\matrix{
{f^k(j-1)+1} & \min \limits_{k} P[f^k(j-1)+1]=P[j] \cr
{-1} & else \cr
} } \right.
\]

代码实现

部分匹配函数(失配函数)的C实现代码:

int *fail(char *p) {
int len = strlen(p);
int *f = (int *) malloc(len * sizeof(int));
f[0] = -1;
int i, j;
for(j = 1; j < len; j++) {
for(i = f[j-1]; ; i = f[i]) {
if(p[j] == p[i+1]) {
f[j] = i + 1;
break;
}
else if(i == -1) {
f[j] = -1;
break;
}
}
}
return f;
}

KMP的C实现代码:

int kmp(char *t, char *p) {
int *f = fail(p);
int i, j;
for(i = 0, j = 0; i < strlen(t) && j < strlen(p); ) {
if(t[i] == p[j]) {
i++;
j++;
}
else if(j == 0)
i++;
else
j = f[j-1] + 1;
}
return j == strlen(p) ? i - strlen(p) : -1;
}

时间复杂度fail函数的复杂度为\(O(p)\),kmp函数的复杂度为\(O(n)\),所以整个KMP算法的复杂度为\(O(n+p)\)。

3. 参考资料

[1] dekai, Lecture 16: String Matching.

[2] E. Horowitz, S. Sahni, S. A. Freed, 《Fundamentals of Data Structures in C》.

[3] Jake Boxer, The Knuth-Morris-Pratt Algorithm in my own words.

【模式匹配】KMP算法的来龙去脉的更多相关文章

  1. 字符串模式匹配KMP算法

    一篇不错的博客:http://www.cnblogs.com/dolphin0520/archive/2011/08/24/2151846.html KMP字符串模式匹配通俗点说就是一种在一个字符串中 ...

  2. KMP算法的来龙去脉

    1. 引言 字符串匹配是极为常见的一种模式匹配.简单地说,就是判断主串TT中是否出现该模式串PP,即PP为TT的子串.特别地,定义主串为T[0-n−1]T[0-n−1],模式串为P[0-p−1]P[0 ...

  3. 字符串模式匹配——KMP算法

    KMP算法匹配字符串 朴素匹配算法   字符串的模式匹配的方法刚开始是朴素匹配算法,也就是经常说的暴力匹配,说白了就是用子串去和父串一个一个匹配,从父串的第一个字符开始匹配,如果匹配到某一个失配了,就 ...

  4. 模式匹配KMP算法

    关于KMP算法的原理网上有很详细的解释,我试着总结理解一下: KMP算法是什么 以这张图片为例子 匹配到j=5时失效了,BF算法里我们会使i=1,j=0,再看s的第i位开始能不能匹配,而KMP算法接下 ...

  5. 模式匹配-KMP算法

    /***字符串匹配算法***/ #include<cstring> #include<iostream> using namespace std; #define OK 1 # ...

  6. 数据结构4.3_字符串模式匹配——KMP算法详解

    next数组表示字符串前后缀匹配的最大长度.是KMP算法的精髓所在.可以起到决定模式字符串右移多少长度以达到跳跃式匹配的高效模式. 以下是对next数组的解释: 如何求next数组: 相关链接:按顺序 ...

  7. 深入理解KMP算法之续篇

    前言: 纠结于KMP已经两天了,相较于本人之前博客中提到的几篇博文,本人感觉这篇文章更清楚地说明了KMP算法的来龙去脉. http://www.cnblogs.com/goagent/archive/ ...

  8. 字符串模式匹配之KMP算法图解与 next 数组原理和实现方案

    之前说到,朴素的匹配,每趟比较,都要回溯主串的指针,费事.则 KMP 就是对朴素匹配的一种改进.正好复习一下. KMP 算法其改进思想在于: 每当一趟匹配过程中出现字符比较不相等时,不需要回溯主串的 ...

  9. 串的模式匹配和KMP算法

    在对字符串的操作中,我们经常要用到子串的查找功能,我们称子串为模式串,模式串在主串中的查找过程我们成为模式匹配,KMP算法就是一个高效的模式匹配算法.KMP算法是蛮力算法的一种改进,下面我们先来介绍蛮 ...

随机推荐

  1. Win10 无法完全关机问题

    Win10是重新安装的.开机运行时间长了或者跑的东西多了.关机,键盘灯还是亮的,要强制按电源键关机才行. 问题解决:从网上搜以为是显卡驱动问题,上官网更新最新驱动,结果还是关不了机.偶然间搜到是Int ...

  2. 微信公共平台开发-(.net实现)2--ACCESSTOKEN值获得

    成功的走出第一步后,我们紧接着趁热打铁开始下一步: 成为了开发者之后微信平台会给您AppId和AppSecret,在订阅号中是没有的,所以因该申请一下服务号, 若没有请注意上一篇http://www. ...

  3. DOM扩展札记

    Selector API HTML5 DOM扩展 Element Traversal规范 Selector API 众多JavaScript库中,最常用的一个功能就是根据css选择符选择与某个模式匹配 ...

  4. 使用Location对象查询字符串参数

    location是BOM中最有用的对象之一: 1.它提供了与当前窗口中加载的文档有关的信息: 2.他还提供了一些导航功能. location对象的属性有: hash, host, hostname, ...

  5. SVM-非线性支持向量机及SMO算法

    SVM-非线性支持向量机及SMO算法 如果您想体验更好的阅读:请戳这里littlefish.top 线性不可分情况 线性可分问题的支持向量机学习方法,对线性不可分训练数据是不适用的,为了满足函数间隔大 ...

  6. 如何为编程爱好者设计一款好玩的智能硬件(七)——LCD1602点阵字符型液晶显示模块驱动封装(上)

    当前进展: 一.我的构想:如何为编程爱好者设计一款好玩的智能硬件(一)——即插即用.积木化.功能重组的智能硬件模块构想 二.别人家的孩子:如何为编程爱好者设计一款好玩的智能硬件(二)——别人是如何设计 ...

  7. 获取MySQL服务提供的sakila数据库(Example Databases)

    关于这个数据库也就是样例数据库,数据库,数据库,最可怕的就是没有数据了,对吧?没有数据你学个什么呀. 可是,没有数据,咱会自己insert,那只能适用于初学者.对于数据库的优化方面的学习,还是有大数据 ...

  8. VUE 意淫笔记

    caihg Vue.js 递归组件实现树形菜单 最近看了 Vue.js 的递归组件,实现了一个最基本的树形菜单. 项目结构: main.js 作为入口,很简单: 1 2 3 4 5 6 7 8 9 i ...

  9. Redis总结笔记(二):C#连接Redis简单例子

    转载于:http://www.itxuexiwang.com/a/shujukujishu/redis/2016/0216/113.html?1455860686 注:C#在调用Redis是不要使用S ...

  10. Redis学习笔记~分布式的Pub/Sub模式

    回到目录 redis的客户端有很多,这次用它的pub/sub发布与订阅我选择了StackExchange.Redis,发布与订阅大家应该很清楚了,首先一个订阅者,订阅一个服务,服务执行一些处理程序(可 ...