串的模式匹配算法
本章我们主要介绍一下串的模式匹配算法。
1. 求子串位置的定位函数
子串的定位操作通常称作串的模式匹配
(其中T称为模式串),是各种串处理系统中最重要的操作之一。下面我们给出一个采用定长顺序存储结构的子串定位算法:
在上面算法中,分别利用计数指针i
和j
指示主串S和模式串T中当前正待比较的字符位置。算法的基本思想是: 从主串S的第pos个字符起和模式的第一个字符比较之,若相等,则继续逐个比较后续字符; 否则从主串的下一个字符起再重新和模式的字符比较之。依次类推,直至模式T中的每个字符依次和主串S中的一个连续的字符序列相等,则称匹配成功,函数值为和模式T中第一个字符相等的字符在主串S中的序号;否则称匹配不成功,函数值为0。下图展示了模式T=’abcac’和主串S的匹配过程(pos=1)。
上述算法的匹配过程易于理解,且在某些应用场合,如文本编辑等,效率也较高,例如,在检查模式’STING’是否存在于下列主串中时:
‘A STRING SEARCHING EXAMPLE CONSISTING of SIMPLE TEXT’
上述算法中while循环次数(即进行单个字符比较的次数)为41,恰好为(Index + T[0]-1)+4, 这就是说,除了主串中呈黑色体的4个字符,每个字符比较了两次以外,其他字符均只和模式进行一次比较。在这种情况下,此算法的时间复杂度为O(n+m)
。其中,n和m分别为主串和模式的长度。然而,在有些情况下,该算法的效率却很低。例如,当模式串为’00000001’,而主串为’00000000000000000000000000000000000000000000000000001’时,由于模式中前7个字符均为’0’,主串中前52个字符均为0,每趟比较都在模式的最后一个字符出现不等,此时需将指针i回溯到i-6的位置上,并从模式的第一个字符开始重新比较,这个匹配过程中指针i需回溯45次,则while循环次数为46 * 8(index * m)。可见,上述算法在最坏情况下的时间复杂度为O(n*m)。这种情况在只有0、 1两种字符的文本串处理中经常出现,因为在主串中可能存在多个和模式串“部分匹配”的子串,因而引起指针i的多次回溯。01串
可以用在许多应用之中。比如一些计算机的图形显示就是把画面表示为一个01串,一页书就是一个几百万个0和1组成的串。在二进位计算机上实际处理的都是01串。一个字符的ASCII码也可以看成是8个二进位01串。包括汉字存储在计算机中处理时也是作为一个01串和其他的字符串一样看待。因此下面我们会介绍另一种较好的模式匹配算法。
2. 模式匹配的一种改进算法
这种改进算法是D.E.Knuth与V.R.Pratt和J.H.Morris同时发现的,因此人们称它为克努特-莫里斯-普拉特操作(简称KMP算法)。此算法可以在O(n+m)的时间数量级上完成串的模式匹配操作。其改进在于: 每当一趟匹配过程中出现字符比较不等时,不需要回溯i
指针,而是利用已经得到的部分匹配
的结果将模式向右“滑动”尽可能远的一段距离后,继续进行比较。下面我们先从具体的例子看起。
回顾我们上一节
的匹配过程示例,在第三趟的匹配中,当i=7、j=5字符比较不等时,又从i=4、j=1重新开始比较。然后,经仔细观察可发现,在i=4和j=1
, i=5和j=1
以及i=6和j=1
这3次比较都是不必进行的。因为从第三趟部分匹配的结果就可得出,主串中第4、5和6个字符必然是’b’、’c’和’a’(即模式串中第2、3和4个字符)。因为模式中的第一个字符是a,因此它无需再和这3个字符进行比较,而仅需将模式向右滑动3个字符的位置继续进行i=7、j=2时的字符比较即可。同理,在第一趟匹配中出现字符不等时,仅需将模式向右移动两个字符的位置继续进行i=3,j=1时的字符比较。由此,在整个匹配过程中,i指针没有回溯。如下图所示为改进算法的匹配过程示例
:
现在讨论一般情况。假设主串为’S1S2…Sn’, 模式串为’P1P2…Pm’, 从上面分析可知,为了实现改进算法,需要解决下述问题: 当匹配过程中产生“失配”(及Si != Pj)时,模式串“向右滑动”可行的距离多远,换句话说,当主串中第i个字符与模式中第j个字符“失配”(即比较不等)时,主串中第i个字符(i指针不回溯)应与模式中哪个字符再比较?
假设此时应与模式中第k(k<j)个字符继续比较,则模式中前k-1个字符的子串必须满足如下关系式(4-2),且不可能存在k’>k满足如下关系式(4-2):
而已经得到的部分匹配
的结果是:
由上式(4-2)和式(4-3)推得下列等式:
反之,若模式串中存才满足式(4-4)的两个子串,则当匹配过程中,主串中第i个字符与模式中第j个字符比较不相等时,仅需将模式向右滑动至模式中第k个字符和主串中第i个字符对齐,此时,模式中头k-1个字符的子串'P1P2...Pk-1'
必定与主串中第i个字符之前长度为k-1的子串'Si-k+1Si-k+2...Si-1'
相等,由此,匹配仅需从模式中第k个字符与主串中第i个字符比较起继续进行。
若令next[j]=k
,则next[j]表明当模式中第j个字符与主串中相应字符“失配”时,在模式中需重新和主串中该字符进行比较的字符的位置。由此,可引出模式串的next函数的定义:
由此定义可推出下列模式串的next函数值:
在求得模式的next函数之后,匹配可如下进行: 假设以指针i和j分别指示主串和模式中正待比较的字符,令i的初值为pos,j的初值为1。若在匹配过程中si=pj
,则i和j分别增1;否则,i不变,而j退到next[j]的位置再比较,若相等,则指针各自增1,否则j再退到下一个next值的位置,依次类推,直至下列两种可能:
-
1) j退到某个next值(next[next[…next[j]..]])时字符比较相等,则指针各自增1,继续进行匹配;
-
2) j退到值为0(即模式的第一个字符失配),此时需将模式继续向右滑动一个位置,即从主串的下一个字符
Si+1
起和模式重新开始匹配。
如下图是上述匹配过程的一个例子(模式为’abaabcac’):
2.1 KMP算法
KMP算法如下所示,它在形式上与开头介绍的直接定位函数类似
。不同之处仅在于:当匹配过程中产生失配
时,指针i不变,指针j退回到next[j]所指示的位置上重新进行比较,并且当指针j退至零时,指针i和指针j需同时增1.即若主串的第i个字符和模式的第1个字符不等,应从主串的第i+1个字符起重新进行匹配。
2.2 求next函数
KMP算法
是在已知模式串的next函数值的基础上执行的,那么,如何求得模式串的next函数值呢? 从上述讨论可见,此函数值仅取决于模式串本身而和相匹配的主串无关。我们可从分析其定义出发用递推的方法求得next函数值。
由定义得知,
next[1] = 0 (4-6)
设next[j] = k,这表明在模式串中存在下列关系:
'P1P2..Pk-1' = 'Pj-k+1Pj-k+2...Pj-1' (4-7)
其中k为满足1<k<j
的某个值,并且不可能存在k'>k
满足等式(4-7)。此时next[j+1]=?
可能有两种情况:
1) 情形1: Pk=Pj
若Pk=Pj
,则表明在模式串中
'P1P2...Pk = 'Pj-k+1Pj-k+2...Pj' (4-8)
并且不可能存在k'>k
满足上式,这也就是说next[j+1] = k+1,即:
next[j+1] = next[j] + 1 (4-9)
2) 情形2: Pk!=Pj
若Pk!=Pj
,则表明在模式串中
'P1P2...Pk' != 'Pj-k+1Pj-k+2...Pj'
此时可把求next函数值的问题看成是一个模式匹配的问题。整个模式串既是主串又是模式串,而当前在匹配的过程中,已有P1=Pj-k+1
, P2=Pj-k+2
,…, Pk-1=Pj-1
,则当Pj!=Pk
时应将模式向右滑动至以模式中的第next[k]
个字符和主串中的第j个字符相比较。若next[k]=k'
,且Pj=Pk'
,则说明在主串中第j+1个字符之前存在一个长度为k'
(即next[k])的最长子串,和模式串中从首字母起长度为k'
的子串相等,即:
也就是说next[j+1] = k’+1,即:
next[j+1] = next[k] + 1 (4-11)
同理,若Pj!=Pk'
,则将模式继续向右滑动直至将模式中第next[k’]个字符和Pj对齐, …, 依次类推,直至Pj和模式中某个字符匹配成功或者不存在任何k'
(1<k’<j)满足等式(4-10),则:
next[j] = 1 (4-12)
例如,下图所示的模式串,以求得前6个字符的next函数值,现求next[7],因为next[6]=3,又P6!=P3,则需比较P6和P1(因为next[3]=1),这相当于将子串模式向右滑动。由于P6 != P1,而且next[1] = 0,所以next[7]=1,而因P7=P1,则next[8]=2。
下面根据上述分析所得结果(式(4-6)、(4-9)、(4-11)和(4-12)),仿照KMP算法
,可得到求next函数值的算法:
2.3 kmp算法时间复杂度
上面求next的算法时间复杂度为O(m)。通常,模式串的长度m
比主串的长度n
要小的多,因此,对整个匹配算法来说,所增加的这点时间是值得的。最后要说明两点:
1) 虽然串的直接模式匹配算法Index()
的时间复杂度是O(n*m)
,但在一般情况下,其实际的执行时间近似于O(n+m)
,因此至今仍被采用。KMP算法仅当模式与主串之间存在"部分匹配"
的情况下才显得比直接模式匹配Index()
快得多。但是KMP算法的最大特点是指示主串的指针不需要回溯,整个匹配过程中,对主串仅需从头至尾扫描一遍,这对处理从外设输入的庞大文件很有效,可以边读入边匹配,而无需回头重读。
2)前面定义的next函数在某些情况下尚由缺陷。例如模式'aaaab'
在和主串'aaabaaaab'
匹配时,当i=4、j=4时,S.ch[4] != T.ch[4],由next[j]的指示还需进行i=4、j=3, i=4、j=2和i=4、j=1这三次比较。
实际上,因为模式中第1、2、3个字符和第4个字符都相等,因此不需要再和主串中第4个字符相比较,而可以将模式一气向右滑动4个字符的位置直接进行i=5、j=1时的字符比较。这就是说,若按上述定义得到next[j]=k
,而模式中Pj=Pk
,则当主串中字符Si
和Pj
比较不相等时,不需要再和Pk进行比较,而直接和Pnext[k]
进行比较,换句话说,此时的next[j]
应和next[k]
相同。由此可得计算next函数修正值的算法如下。此时匹配算法不变:
参看如下图示:
[参考]