数据结构学习笔记--串的模式匹配

原创 2007年09月26日 20:23:00

所谓串的模式匹配,简单说就是指给定一个主串T(text)和一个子串P(pattern),求在T中第一次出现P的位置索引。例如T=timercrack likes to sleepP=likes”,PT中的位置索引为12

首先,让我们来看一种朴素的匹配算法,设T[n]存放主串,P[m]存放子串(模式串),则算法可以这样描述:

//朴素的模式匹配算法
int Index(string T, string P, int pos) {
   
//返回模式串P在主串T第pos个字符之后的位置。
   
//其中1<=pos<=T.length
   i = pos; j = 1;
   
while (i <= T.length && j <= P.length)
       
if (T[i] == P[j]) {
          
++i;
          
++j;
       }
 else {
          i 
= i–j + 2;
          j 
= 1
       }

   
//匹配成功返回位序,否则返回-1
   retun j > T.length ? i–T.length : -1;
}
//Index

 

每一轮匹配j最多加到m,最多匹配n轮,所以算法的时间复杂度是O(m*n)。匹配的过程十分清晰,这里关键要注意当某次失配时(即T[i]P[j]),i会退回到本次匹配开始的下一个字符处,重新和T[1]开始比较。这导致在某些情况下算法的效率非常低,例如:P=00000001,T=000000000...0001,每次匹配到P[8]T[i]不相等时,i要回退j-2=6个字符,并重新和P[1]进行比较。

因此我们想,在每次失配时能不能不让i回退,而只让模式串向右“滑动一段距离”呢?举个例子:

如上图,在第二趟匹配失配时i回到了4位置,与P[1]重新比较。而实际上T[4]P[1]T[5]P[1]T[6]P[1]这三次比较是多余的,因为在第二趟匹配时我们已经知道它们是“bca”。因为模式串的首字符是a,刚好与T[6]相同,所以可保持i不变,让j=2再与T[i]比较。形象说就是让模式串相对于主串“向右滑动”3个字符。正如上图中最后的“第三趟匹配改”所做的那样。

本着这个思想我们来讨论一下如何改进这个算法。通过前面的简单介绍我们知道,在某些情况下当某一次匹配失败时,可以将模式串向右滑动一段距离。那么,为什么可以这样呢?换句话说,每次可以滑动多远呢?下面我们来分析一下:设若某一次T[i]P[j]时,假设可以保持i

不变,让P向右滑动一段距离,设此时j=k,程序将继续比较T[i]P[j]是否相等。那么必然有如下等式(1):

式(1)说明位置k之前的k-1个字符已经与主串i之前的k-1个字符相等,而目前已有P串的前j-1个字符与主串匹配成功,因为k<j,显然有:

由(1)(2)式可得出:

等式(3)就是P可以滑动的依据,当P向右滑动至j=k时,P之前的k-1个字符必与主串Ti向前数k-1个字符相等。也就是说能滑动的前提是模式串中存在两个相等的子串,并且:其中一个子串必须P首开始;二:两个子串在P中可以部分重叠。如下所示:

为了便于实现,我们定义一维数组next[],并规定next[j](1jm)表示当T[i]P[j]时,在模式串中须重新与T[i]相比较的位置(next[j]=k)。例如对于模式串P=abaabcac”,其next数组的值为:

我们规定next[1]=0,用来表示数组的起始值,这个规定是随意的,如果乐意完全可以改成别的值(设成0是为了算法的简洁,这在后面将会看到)。

好了,我们先假设有某种方法计算next[]数组的值,那么匹配可以这样进行:当T[i]P[j]时,令j=next[j]再继续与T[i]进行比较。若仍不相等再令j=next[j],重复上述过程,直到退至某一next[k]T[i]=P[next[k]];或退到j=next[1]=0,说明模式串的首字符与T[i]失配,则将i++,继续下一轮的匹配。下面给出这种改进的匹配算法:

//改进的模式匹配算法
int Index(string T, string P, int pos) {
   
//利用数组next[]求模式串P在主串T第pos个字符之后的位置。
   
//其中1<=pos<=T.length
   i = pos; j = 1;
   
while (i <= T.length && j <= P.length)
       
if (j == 0 || T[i] == P[j]) {
          
++i;
          
++j;
       }
 else j = next[j];  //模式串向右滑动
   
//匹配成功返回位序,否则返回-1
   retun j > T.length ? i – T.length : -1;
}
//Index


该算法最初是在1968年由D.E.Knuth和他的学生J.H.MorrisV.R.Pratt首先发现的,简称为KMP算法,取了这三个人名字的首字母。这时,或许你突然明白了AVL 树为什么叫AVL,或者Bellman-Ford为什么中间是一杠不是一个点。有时一个东西有七八个人研究过,那怎么命名呢?通常这个东西干脆就不用人名字命名了,免得发生争议,比如“3x+1问题”......扯远了。

书上只是简单说这个程序的时间复杂度是O(n),并没有解释,但为什么是O(n)呢?其实主要的争议在于,每次是执行if语句体还是执行else语句使得while循环的执行次数出现了不确定因素。我们将用时间复杂度的摊还分析中的主要策略来分析它,简单地说就是通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。KMP的时间复杂度分析可谓摊还分析的典型。我们从上述程序的j 值入手。每一次执行第六句if语句体都会使j1(但不能大于m),而另外的改变j值的地方只有第六行一处(else j=next[j])。每一次执行这句j都会减少;因此,整个过程中j最多加了m-11。于是,j最多只有m-1次减小的机会(j值减小的次数当然不能超过m,因为j永远是非负整数)。这告诉我们,else语句最多执行了m-1次。按照摊还分析的说法,平摊到每次while循环中后,一次while循环的时间复杂度为O(1)。整个过程显然是O(n)的。

上面的都清楚以后,现在让我们来讨论next数组值的求法。由等式(3)可知,next的值只与模式串P本身有关,与主串没有关系。由next的定义可知当j>1next[j]等于集合{k | 1<k<jk满足等式(3)}中的最大值,当集合为空时next[j]=1。所以可以使用一种“递推”的方法来求next,这种方法类似于我们使用数学归纳法证明定理,所不同的是在证明定理时我们已经知道定理的结果,而现在结果并不知道,需要我们步步推出它。

已知next[1]=0,假设next[j]=k,下面来推next[j+1]的值:由等式(3)可知next[j+1]的取值有两种情况。

第一种情况:P[j]=P[k],则P[k+1]就是当P[j+1]T[i]失配时须重新与T[i]相比较的字符,此时next[j+1]=k+1=next[j]+1,如下:

第二种情况:P[j]≠P[k],说明等式(4)不成立,此时应在“p1p2p3...pk”中寻找新的位置k=k’(1<k’<k),使之满足等式(4)。此时求k’的过程也变成了模式匹配的过程,只不过整个模式串P既是主串又是模式串。所以当P[j]≠P[k]时应有k=next[k],再比较此时的P[j]与P[k],若相等则转到第一种情况,此时next[j+1]=next[k]+1=next[next[j]]+1,若k仍不满足等式(4)则继续令k=next[k]……重复以上过程直到找到某个k’(1<k’<j)使得匹配成功或者k’=0,说明不存在这样的子串,此时next[j+1]=1。如下所示:

下面给出求next[]的算法,将会看到该算法形式上与KMP算法神似:

//预处理程序,求数组next[] 的值
void get_next(string P, int next[]) {
   
//求模式串P的next数组值,存入next[]中
   i = 1; next[1]=0; j=0;
   
while (i < P.length)
       
if (j == 0 || P[i] == P[j]) {
          
++i;
          
++j;
          next[i] 
= j;
       }
 else j = next[j];
}
//get_next


依照KMP算法时间复杂度的求法,我们同样可以求出该算法的时间复杂度为O(n)。使用KMP算法进行匹配时,一个模式串在求出它的next值后可与不同的主串进行模式匹配(预处理的好处)并且主串的指针i在匹配过程中不需要回退。该算法在某些情况下比朴素算法效率要高出许多,举个极端的例子:主串由一大堆的0组成,模式串也是一串0但结尾是一个1。尽管朴素算法的时间复杂度是O(m*n),但一般情况下它的复杂度近似为O(n),这是因为在某轮匹配时一般P[j]很快就会与T[i]失配,从而继续下一轮匹配,j不会取遍1m。所以这种算法仍被广泛地采用,例如标准C库中的字符串匹配函数strstr(),其采用的就是朴素匹配算法。

但上面这个求next[]数组值的算法在某些情况下尚有缺陷,每次对next[j]进行赋值时,并没有考虑P[j+1]=P[k+1]的情况。书上在这里给出的例子很能说明问题:

T[4]P[4]失配时,按照next[j]的值则还应该比较T[4]P[3]T[4]P[2]T[4]P[1]。但实际上P[4]=P[3]=P[2],所以这三次比较是不需要的,可以直接i++,继续进行T[5]P[1]的比较。也就是说,如果在求next[j+1]的过程中找到了一个k(1<k<j)P[j+1]=P[k+1],则说明当P[j+1]与主串失配P[k+1]不必再与主串比较了,而应直接和P[next[k+1]]进行比较,即此时的next[j+1]=next[k+1]。由此可得到如下求next的改进算法:

//预处理程序,求数组next[] 的值的改进算法
void get_next(string P, int next[]) {
   
//求模式串P的next数组值,存入next[]中
   i = 1; next[1]=0; j=0;
   
while (i < P.length)
       
if (j == 0 || P[i] == P[j]) {
           
++i;
          
++j;
          
if (P[i] != P[j])
              next[i] 
= j;
          
else 
              next[i] 
= next[j];
       }
 else j = next[j];
}
//get_next


总结一下,KMP模式匹配算法由两部分组成,主算法的时间复杂度是O(n),求next的是O(m) (预处理过程),总时间复杂度是O(m+n)。该算法使用了预处理的方法来提高搜索速度,事实上,许多模式匹配算法都巧妙地运用了预处理,从而可以在线性的时间里解决字符串的匹配。每种算法都各具特点,比如BM(Boyer Moore)算法的查找是跳跃式进行的、ASS(Alpha Skip Search)具有亚线性的时间复杂度,还有后缀树,自动机等等。实际选用哪种算法应该具体情况具体分析,其他算法以后学到再说,呵呵~

部分内容及参考源自一下链接:

《Knuth-Morris_Pratt string matching》http://www.ics.uci.edu/~eppstein/161/960227.html

《KMP算法详解》http://www.matrix67.com/blog/article.asp?id=146

数据结构---串的模式匹配算法介绍

前言 The years teach much which the days never knew. Time:2017/2/19 Name:Willam 1、介绍 对于文本程序来说...
  • qq_35644234
  • qq_35644234
  • 2017年02月21日 19:26
  • 2214

数据结构例程——串的模式匹配(KMP算法)

问题:串的模式匹配 KMP算法:#include #include "sqString.h" void GetNext(SqString t,int next[]) /*由模式串t求出next值...
  • sxhelijian
  • sxhelijian
  • 2015年10月07日 15:50
  • 2476

《数据结构》严蔚敏版(java解)——第四章 串01 串的模式匹配KMP算法

概念:KMP字符串模式匹配通俗点说就是一种在一个字符串中定位另一个串的高效算法。简单匹配算法的时间复杂度为O(m*n);KMP匹配算法。可以证明它的时间复杂度为O(m+n)。 特点:相比一般简单的字...
  • wang1472jian1110
  • wang1472jian1110
  • 2017年03月09日 16:48
  • 678

数据结构——串的朴素模式和KMP匹配算法

一、朴素模式假设我们要从主串S=”goodgoogle"中找到子串T=“google"的位置,步骤如下:i表示主串的当前位置下标,j表示子串的当前位置下标,如上图在第一轮比较(i=1开始)中j=4和i...
  • lxq_xsyu
  • lxq_xsyu
  • 2014年12月07日 14:34
  • 3586

数据结构Java实现——③串--->串的模式匹配:Brute-Force算法和 KMP算法

KMP模式匹配算法 在Brute-Force算法的基础上进行了改进,改进后的算法,可以在匹配失败后,无需修改记录主串当前比较的字符的下标 i;而且,记录子串当前比较的字符的下标 j也不用清零。而是在...
  • u011446177
  • u011446177
  • 2015年01月18日 10:47
  • 901

C语言实现串的基本模式匹配

前言本程序是数据结构上机实验内容,参考《数据结构(C语言版)》(清华大学出版社)中串操作部分的伪代码实现。题目要求设计一个程序实现串的基本模式匹配。所谓模式匹配,就是判断某个串是否是另一个已知串的子串...
  • lgj123xj
  • lgj123xj
  • 2017年05月24日 14:45
  • 1091

数据结构——串(朴素的模式匹配算法、KMP模式匹配算法)

提示:以下内容不适合零基础人员,仅供笔者复习之用。 概要: 串是由零个或多个字符组成的有限序列,又名叫字符串。 一、串的比较     给定两个串,s = "a1a2.....an",t="b1...
  • daijin888888
  • daijin888888
  • 2017年04月20日 16:03
  • 1097

串模式匹配算法的几种代码实现

c语言中字符串处理的库函数还是比较全的,c++里的string类就更不用说了。有些处理函数虽然已封装为库函数,直接调用即可,但是正在学习数据结构,还是想敲些代码,以便总结+巩固。 首先,总结一下常用的...
  • ppalive_
  • ppalive_
  • 2015年07月31日 12:05
  • 1561

串的应用--模式匹配算法

子串的定位操作通常称为串的模式匹配,是串中最重要的操作之一。朴素的模式匹配算法,简单来说,就是对主串的每个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,知...
  • dengpei187
  • dengpei187
  • 2016年07月12日 10:23
  • 1642

数据结构(11)--串的模式匹配算法之BF、KMP算法

参考书籍:数据结构(C语言版)严蔚敏吴伟民编著清华大学出版社 1.串的存储 1.1定长顺序存储 串的定长顺序存储(静态数组):     #define  MAXSTRLEN  255  // 用户...
  • u010366748
  • u010366748
  • 2016年02月23日 13:35
  • 1842
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:数据结构学习笔记--串的模式匹配
举报原因:
原因补充:

(最多只允许输入30个字)