您的位置:首页 > 其它

【算法导论】字符串匹配

2015-08-26 14:29 232 查看
在编辑文本程序过程中,我们经常需要在文本中找到某个模式的所有出现位置。典型情况是,一段正在被编辑的文本构成一个文件,而所要搜寻的模式是用户正在输入的特定的关键字。有效地解决这个问题的算法叫做字符串匹配算法,该算法能够极大提高编辑文本程序的响应效率。在其他很多应用中,字符串匹配算法用于在DNA序列中搜寻特定的序列。在网络搜索引擎中也需要用这种方法来找到所要查询的网页地址。

字符串匹配问题的形式化定义如下:

假定文本是一个长度为n的数组T[1..n],而模式是一个长度为m的数组P[1..m],其中m<=n,进一步假设P和T的元素都是来自一个有限字母集S的字符。例如S = {0, 1}或者S={a, b, ..., z}。字符数组P和T通常称为字符串。

如果0<=s<=n-m,并且T[s+1..s+m] ==P[1..m](即如果T[s+j] = P[j],其中1<=j<=m),那么称模式P在文本T中出现,且偏移为s(或者等价的,模式P在文本T中出现的位置是以s+1开始的)。如果P在T中以偏移s出现,那么称s是有效偏移;否则,称它为无效偏移。

字符串匹配问题就是找到所有的有效偏移,使用在该有效偏移下,所给的模式P出现在给定的文本T中。

后缀重叠引理1:

假设字符串x, y, z满足:x是z的后缀字符串,y也是z的后缀字符串。如果|x|<=|y|,那么,x是y的后缀字符串;如果|x|>=|y|,那么,y是x的后缀字符串;如果|x|==|y|,那么x = y。

下面要讨论的算法的复杂度:



1. 朴素字符串匹配算法

朴素字符串匹配算法是通过一个循环找到所有有效偏移,该循环对n-m+1个可能的s值进行检测,看是否满足条件P[1..m] = T[s+1..s+m]。

NAIVE-STRING-MATCHER(T, P)
1. n = T.length
2. m = P.length
3. for s = 0 to n-m
4.     if P[1..m] == T[s+1..s+m]
5.         print "Pattern occurs with shift" s


2. Rabin-Karp算法

在实际应用中,Rabin和Karp所提出的字符串匹配算法能够较好地运行,并且还可以从中归纳出相差问题的其他算法,比如二维模式匹配。虽然Rabin-Karp算法最坏运行时间与朴素匹配算法相同。但基于一些假设,在平均情况下,它的运行时间还是比较好的。

为了便于说明,假设S = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},这样每个字符都是十进制数字。(在通常情况下,可以假定每个字符都是以d为基数表示的数字,其中d=|S|)我们可以用长度为k的十进制数来表示由k个连续的字符组成的字符串。因此,字符串"31415"对应着十进制数31415。

给定一个模式P[1..m],假设p表示其相应的十进制值。类似地,给定文本T[1..m],假设t[s]表示长度为m的子字符串T[s+1..s+m]所对应的十进制值,其中s = 0, 1, 2, ..., n-m。当然,只有在T[s+1..s+m]=P[1..m]时,t[s]=p。如果能在时间O(m)内计算出p值,并在总时间O(n-m+1)内计算出所有的t[s]值,那么通过比较p和每一个t[s]值,就能在O(m)+O(n-m+1)=O(n)时间内找到所有的有效偏移s。

我们可以运用霍纳法则在时间O(m)内计算出p:

p = P[m] + 10(P[m-1] + 10(P[m-2] + ... + 10(P[2]+P[1])...))

类似地,也可以地O(m)的时间内根据T[1..m]计算出t[0]的值。

为了在时间O(n-m)内计算出剩余的值t[1], t[2], ... , t[n-m],我们需要在常数时间内根据t[s]计算出t[s+1],因为:

t[s+1] = 10(t[s] - 10^(m-1)T[s+1]) + T[s+m+1]

如果能预先计算出常数10^(m-1),则每次执行上式时的算术运算次数为常数。因此,可以在O(m)内计算出p,在时间O(n-m+1)内计算出所有t[0], t[1], t[2], ... , t[n-m]。因而可以在O(m)的预处理时间和O(n-m+1)的匹配时间找到所有模式P[1..m]在文本T[1..n]中出现的位置。

到目前为止,我们有意回避的一个问题是:p和t[s]的值可能太大,导致不能方便地对其进行操作。如果P包含m个字符,那么关于p(m数位长)上的每次算术运算需要“常数”时间这一假设就不合理了。幸运的是,我们可以很容易地解决这个问题。选取一个合适的模q来计算p和t[s]的模。我们可以在O(m)的时间内计算出模q的p值,并且可以在O(n-m+1)时间内计算出模q的所有t[s]值。如果选模q为一个素数,使得10q恰好满足一个计算机字长,那么可以用单精度算术运算执行所有必需的操作。在一般情况下,采用d进制的字母表{0,
1, ..., d-1}时,选取一个q值,使得dq在一个计算机字长内,然后调整如上递归式,使其能够对模q有效,式子变为:

t[s+1] = (d(t[s] - hT[s+1]) + T[s+m+1]) mod q

其中h = d^(m-1)(mod q)是一个具有m数位的文本窗口的高位数位上的数字“1”的值。

但是基于模q得到的结果并不完美:t[s]=p(mod q)并不能说明t[s]=p。但是另一方面,如果t[s] != p(mod q),那么可以断定t[s] != p,从而确定偏移s是无效的。因此可以把t[s]=p(mod
q)是否成立作为一种快速的启发式测试方法用于检测无效偏移s。任何满足t[s]=p(mod q)的偏移s都需要进一步检测,看s是真的有效还是仅仅是一个伪命中点。这项额外的测试可以通过检测条件P[1..m] = T[s+1..s+m]来完成,如果q足够大,那么这个伪命中点可以尽量少出现,从而使额外测试的代价降低。

下面的过程准确地描述了上面的思想。过程的输入是文本T,模式P,使用基数d(一种典型的取值为|S|)和素数q:

RABIN-KARP-MATCHER(T, P, d, q)
1. n = T.length
2. m = P.length
3. h = d^(m-1) mod q
4. p = 0
5. t[0] = 0
6. for i = 1 to m
7.     p = (d*p + P[i]) mod q
8.     t[0] = (d*t[0] + T[i]) mod q
9. for s = 0 to n-m
10.     if p == t[s]
11.         if P[1..m] == T[s+1..s+m]
12.         print "Pattern occurs with shift" s
13.     if s < n-m
14.         t[s+1] = (d*(t[s] - T[s+1]*h) + T[s+m+1]) mod q


Rabin-Karp算法的期望运行时间为O(n)+O(m(v+n/q))。

3. 利用用限自动机进行字符串匹配

有限自动机:

一个有限自动机M是一个5元组(Q, q[0], A, S
4000
, f),其中

Q是状态的有限集合。

q[0] 是Q的一个元素,q[0] 是初始状态。

A是Q的子集,A是一个特殊的接受状态的集合。

f是一个从Q*S到Q的函数,称为M的转移函数。

有限自动机开始于状态q[0],每次读入输入字符串的一个字符。如果有限自动机在状态q时读入了字符a,则它从状态q变为状态f(q, a)(进行了一次转移)。每当其当前状态q属于A时,就说自动机M接受了迄今为止所读入的字符串。没有被接受的输入称为被拒绝的输入。

有限自动机M引入一个函数g,称为终态函数,它是从S*到Q的函数,满足g(w)是M在扫描字符串w后终止时的状态。因此,当且仅当g(w)是A中的元素时,M接受字符串w。我们可以用转移函数递归定义g:

g(e) = q[0]    // e表示字符串为空

g(wa) = f(g(w), a)

字符串匹配自动机:

对于一个给定的模式P,我们可以在预处理阶段构造一个字符串匹配自动机,根据模式构造出相应的自动机后,再利用它来搜寻文本字符串。下图说明了用于匹配模式P=ababaca的有限自动机的构造过程。从现在开始,假定P是一个已知的固定模式,为了使说明简洁,在下面的符号中将不指出对P的依赖关系。



(a)一个字符串匹配自动机的状态转换图,它可以接受所有以字符串ababaca结尾的字符串。状态0是初始状态,状态7(被涂黑)是仅有的接受状态。从状态i到状态j,标有a的有向边表示f(i, a) = j。形成自动机“脊”的右向边,在图中加重了颜色,对应着模式和字符串之间的成功匹配。除了从状态7到1和2的边外,向左指向的边对面应着失败的匹配。一些表示匹配失败的边并没有标记出来;通常,如果状态i对某个a没有对应a的出边,则f(i, a) = 0。

(b)对应的转移函数f和模式字符串P = ababaca。模式和输入之间的成功匹配被标上了阴影。

(c)自动机在文本T=abababacaba上的操作。在处理了前缀T[i]之后,在每个文本字符T[i]下面,给出了它在自动机内的状态f(T[i])。自动机找到该模式的一个出现,以位置9结尾。

为了详细说明与给定模式P[1..m]对应的字符串匹配自动机,首先定义一个辅助函数h,称为对应P的后缀函数。函数h是一个从S*到{0, 1, 2, ..., m}上的映射,满足h(x)是x的后缀P的最长前缀的长度:

h(x) = max {k:x是p[k]的后缀}

因为空字符串p[0] = e是每一个字符串的后缀,所以后缀函数h是良定义的。

给定模式P[1..m],其相应的字符串匹配自动机定义如下:

状态集合Q为{0, 1, 2, ..., m}。开始状态q[0]是0状态,并且只有状态m是唯一被接受的状态。

对任意的状态q和字符a,转移函数f定义如下f(q, a) = h(p[q]a)。`

为了清楚说明字符串匹配自动机的操作过程,我们给出一个简单而有效的程序,用来模拟这样一个自动机(用它的转移函数f来表示),在输入文本T[1..n]中,寻找长度为m的模式P的出现位置。如果对于m长模式的任意字符串匹配自动机,状态信Q为{0, 1, 2, ..., m},初始状态为0,唯一的接受状态是m。

FINITE-AUTOMATON-MATCHER(T, f, m)
1. n = T.length
2. q = 0
3. for i = 1 to n
4.     q = f(q, T[i])
5.     if (q == m)
6.         print "Pattern occurs with shift" i-m
引理2. 后缀函数不等式

对任意字符串x和字符a,h(xa)<=h(x)+1。

引理3. 后缀函数递归定理

对任意字符串x和字符a,若q = h(x),则h(xa)=h(p[q]a)。

定理:

如果g是字符串匹配自动机关于给定模式P的终态函数,T[1..n]是自动机的输入文本,则对i = 0, 1, ..., n, g(T[i]) = h(T[i])。

计算转移函数:

下面的过程根据一个给定模式P[1..m]来计算转移函数f

COMPUTE-TRANSITION-FUNCTION(P, S)
1. m = P.length
2. for q = 0 to m
3.     for each charater a in S
4.         k = min(m+1, q+2)
5.     repeat
6.         k = k-1
7.     until p[q]a is the postfix of p[k]
8.     f(q, a) = k
9. return f


4. Knuth-Morris-Pratt算法

现在介绍一种由Knuth、Morris和Pratt三个人设计的一个线性时间字符串匹配算法。这个算法无需计算转移函数f,匹配时间为O(n),只用到辅助函数π,它在O(m)时间内根据模式预先计算出来,并且存储在数组π[1..m]中。数组π使得我们可以按需要“即时”有效地计算转移函数f。粗略地说,对任意状态q = 0, 1, 2, ..., m和任意字符a,π[q]的值包含了与a无关但在计算f(q, a)时需要的信息。

下面给出Knuth-Morris-Pratt匹配算法的伪代码KMP-MATCHER过程。我们将看到,其大部分都是在模仿FINITE-AUTOMATON-MATCHER。KMP-MATCHER调用了一个辅助程序COMPUTE-PREFIX-FUNCTION来计算π。

KMP-MATCHER(T, P)
1. n = T.length
2. m = P.length
3. π = COMPUTE-PREFIX-FUNCTION(P)
4. q = 0
5. for i = 1 to n
6.     while q > 0 and P[q+1] != T[i]
7.         q = π[q]
8.     if P[q+1] == T[i]
9.         q = q+1
10.     if q == m
11.         print "Pattern occurs with shift" i-m
12.     q = π[q]

COMPUTE-PREFIX-FUNCTION(P)
1. m = P.length
2. let π[1..m] be a new array
3. π[1] = 0
4. k = 0
5. for q = 2 to m
6.     while k > 0 and P[k+1] != P[q]
7.         k = π[k]
8.     if P[k+1] == P[q]
9.         k = k+1
10. return π
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: