在一个文本文件中查找指定模式的问题,就是一个字符串匹配问题,形式化定义:文本是一个长为n的数组T[1..n],模式是一个长为m的数组P[1..m],其中,进一步假设P和T的元素都是来自一个有限字符集
的字符,例如
={0, 1}或者
。字符数组P和T就是我们所说的字符串。
若,并且
,那么说模式P在文本T中出现,并且偏移是s,这里s是数组下标。若P在T中以偏移s出现,称s是有效偏移,否则是无效偏移。字符串匹配就是找到所有有效偏移的过程,使得在这个有效偏移下,所给的模式P出现在文本T中。
除了朴素算法,每一种字符串匹配算法都基于模式进行预处理,然后找到所有的有效偏移,第二步成为匹配。每个算法的匹配时间和预处理时间如下:
算法 预处理时间 匹配时间
朴素算法 0
Rabin-Karp
有限自动机算法
KMP
1.朴素字符串匹配算法
通过一个循环找到所有的有效偏移,该循环对个可能的s值进行检测,看是否满足条件
,伪代码如下:
n = T.length
m = P.length
for s = 0 to n - m
if P[1..m] == T[s + 1..s + m]
print "Pattern occurs with shift"s
其中第三行是一个循环,第4行隐含一个对模式的遍历,因此时间界是,当
,时间是
2.Rabin-Karp算法
将字符串看作数字,字符串匹配的过程就是一个判断数字是否相等的过程。比如,假设,这样每个字符都是十进制数字,通常情况下,假定每个字符都是以d为基数表示的数字,其中
,比如字符有127个,那么d就是127,数字从0到126表示。下面都用十进制数字为例,可以用长度为k的十进制数字表示由k个字符表示的连续字符组成的数字,比如“31415”对应十进制数字31415。
算法描述:给定一个模式P[1..m],在T中每次取m位组成一个数字,判断这个数字和P是否相等。
时间:计算P的时间是,方法是使用霍纳法则;计算T中每个数字值的时间是
,方法是使用如下公式:
含义是,比如模式是5位数字31415,T中连续6位数字是314152,那么上次取到的数字是31415,下次应该取14152,将31415的最高位减掉,剩余4位乘10,然后加最低位的2,得到14152。
这样,此算法就是一个的准备时间和一个
的匹配时间,但是这个时间忽略的P可能很大的问题,这个时间在P上的每次算数运算是常数这个假设不成立。解决此问题的方法是使用两个数对第三个数同模。
模运算:选择一个合适的模q,要求q是一个素数并且10q满足一个计算机字长,那么可以用单精度算数运算执行必须的计算。
模运算的时间:计算P的时间还是,用模替换原来的公式,如下:
其中
原理是利用(a * b)modq = (amodq * bmodq)modq,(a + b)modq = (amodq + bmodq)modq
这样,比较数字变成了比较模,但是有一个问题,不能说明
,但是另一方面,若
,那么可以断定
,因此这项测试可以用来判定无效偏移,任何满足条件的偏移s都需要进一步检测,判断是否是伪命中点。进一步检测的方法就是直接比较原来的数字。伪命中点足够少,因此时间可以忽略。伪码如下:
rabin-karp-matcher(T, P, d, q)
n = T.length
m = P.length
h = 10 ^ (m - 1)modq
p = 0
t0 = 0
/* preprocessing */
for i = 1 to m
p = (dp + P[i])modq //计算模式的模q值
t0 = (dt0 + T[i])modq //计算T前m位的模q值
/* matching */
for s = 0 to n - m
if p == t //模相等,可能是伪命中点,需要进一步检查
if P[1..m] == T[s + 1, s + m]
print "Pattern occures with shift" s
if s < n - m //若是最后一次,不用更新t了,节省一次计算
使用公式计算下一个m位数字的值
结论:rabin-karp算法的预处理时间是,最坏情况下匹配时间是
,原因是对于每个有效偏移进行显式验证,在实际应用中,可以期望有效偏移的数量少一些,期望匹配时间是O(n)。
3.有限自动机算法
有限自动机M是一个五元组,其中:
Q是状态的集合
是初始状态
是一个特殊的接受状态的集合
是有限输入字母表
是一个
到Q的函数,称为M的转移函数
处理流程:有限自动机开始于状态,每次读取输入字符串的下一个字符。当有限自动机在状态q读取了一个字符a,那么它的状态从q变成
,即发生了一次状态变化。每当其状态属于A的时候,称自动机M接受了迄今为止读入的字符串。没有被接受的输入称为被拒绝的输入。
终态函数:有限自动机引入一个终态函数,是从
到Q的函数,满足
是M在扫描字符串w后终止时的状态。当且仅当
时,M接受字符串w,可以用转移函数递归定义
,如下:
,
字符串匹配自动机:对于一个给定的模式P,可以在预处理阶段构造一个字符串匹配自动机,定义一个辅助函数:
,即x的后缀P的最长前缀的长度。给定模式P[1..m],相应的字符串匹配自动机定义如下:
1.状态集合Q为{0, 1, ..., m},开始状态为0,只有状态m是接受状态
2.对任意状态和字符a,状态转移函数定义为:
,其中
是P中前q个字符组成的字符串
自动机转移函数为func,在输入文本T[1..n]中,寻找长度为m的模式P的出现位置,如同对于m长模式的任意字符串匹配自动机,状态集Q为{0, 1, ..., m},初始状态为0,唯一接受状态为m,伪码如下:
finite-automaton-matcher(T, func, m)
n = T.length
q = 0
for i = 1 to n
q = func(q, T[i])
if q == m
print "Pattern occurs with shift" i - m
这是一个时间为的匹配,但是没有包含计算转移函数函数需要的预处理时间,先讨论这个函数的正确性。
引理32.2 (后缀函数不等式):对任意字符串x和字符a,
证明:显然
引理32.3 (后缀函数递归引理):对任意x和字符串a,若则
证明:显然
定理32.4 如果是字符串匹配自动机关于给定模式P的终态函数,T[1..n]是输入文本,则对
证明:显然
由上面的定理可以证明使用自动机匹配字符串的正确性,接下来核心问题就是如何计算状态转移函数,伪码如下:
compute-transition-function(P, set)
m = P.length
for q = 0 to m
for each character a in set
k = min(m + 1, q + 2)
repeat
k = k - 1
until
Pk is suffic of Pqa
func(q, a) = k
return func
思想是两层循环,第一层遍历所有状态,第二层遍历所有字符,即求所有状态下,遇到每个字符后,状态转移函数应该给出的值,k最大的可能值是min(m, q + 1),因为m是上限,每次加一个字符状态最多加1。先k = k - 1所以初始多了1。这个函数的时间界是。
4.KMP算法
此算法无需计算转移函数,匹配时间是
,只用到辅助函数
,它在
时间内根据模式预先计算出来,存储在数组
中。对任意状态a = 0, 1, ..., m和任意字符
,
的值包含了与a无关但是计算
时需要的信息。由于数组
只有m个元素,而
有
个值,所以通过预先计算
而不是
,可以使计算时间减少一个
因子。
关于模式的前缀函数:前缀函数包含模式与其自身偏移进行匹配的信息,可以用来在朴素字符串匹配算法中避免对无用偏移进行检测,也可以避免在字符串匹配自动机中,对整个转移函数
的预先计算。
KMP算法回答了如下问题:假设模式字符P[1..q]与文本字符T[s+1..s+q]匹配,s2是最小的下次偏移量,s2 > s,那么对于某些k < q,满足P[1..k] = T[s2+1..s2+k]的最小偏移s2是多少,其中s2 + k = s + q。
解决这个问题需要的预处理可以用模式与自身进行比较来计算。伪代码如下:
KMP-matcher(T, P)
n = T.length
m = P.length
π = compute-prefix-function(P)
q = 0
for i = 1 to n
while q > 0 and P[q + 1] != T[i]
q = π[q]
if P[q + 1] == T[i]
q = q + 1
if q == m
print "Pattern occurs with shift" i - m
q = π[q]
compute-prefix-function(P)
m = P.length
let π[1..m] be a new array
π[1] = 0
k = 0
for q = 2 to m
while k > 0 and P[k + 1] != P[q]
k = π[k]
if P[k + 1] == P[q]
k = k + 1
π[q] = k
return π
计算π在compute-prefix-function中完成,其循环的意思是,每次k是对于q已经满足的,看下一个字符加入后是否满足,若满足,那么π[q] = k + 1,否则迭代k,直到找到一个值或者0,继续找下一个字符。
完整C代码如下:
#ifdef _cplusplus
extern "C" {
#endif
#include <stdio.h>
#include <string.h>
/* 朴素字符串匹配 */
void func1(char *T, char *P)
{
int tlength, plength;
int i, j;
tlength = strlen(T);
plength = strlen(P);
for (i = 0; i < tlength - plength + 1; i++)
{
for (j = 0; j < plength; j++)
{
if (T[i + j] != P[j])
{
break;
}
}
if (j == plength)
printf("match, s is %d\n", i);
}
}
/* Rabin-Karp算法 */
/* 辅助函数,递归求幂 */
int func2_pow(int d, int m)
{
int res;
if (1 == m)
return d;
res = func2_pow(d, m / 2);
res *= res;
if (m & 0x00000001)
return res * d;
else
return res;
}
void func2(char *T, int Tlength, char * P, int Plength)
{
int m, n, h;
int p = 0, t = 0;
int i, j;
m = Plength;
n = Tlength;
h = func2_pow(10, m - 1) % 13;
for (i = 0; i < m; i++)
{
p = (10 * p + P[i]) % 13;
t = (10 * t + T[i]) % 13;
}
for (i = 0; i <= n - m; i++)
{
if (p == t)
{
for (j = 0; j < m; j++)
{
if (P[j] != T[i + j])
break;
}
if (j == m)
printf("match, s is %d\n", i);
}
if (i != n - m)
{
/* C语言求模和数学求模有差异,可能是负数,要处理 */
t = (10 *(t - T[i] * h) + T[i + m]) % 13;
if (t < 0)
t += 13;
}
}
}
/* 有限自动机算法 */
int transfer[255][255] = {0};
int Min(int a, int b)
{
return a < b ? a : b;
}
void func3_preprocess(char *P, char * Set)
{
int m, characterlength;
int q, k, i, j;
char a;
int kindex;
m = strlen(P);
characterlength = strlen(Set);
for (q = 0; q < m; q++)
{
for (i = 0; i < characterlength; i++)
{
a = Set[i];
k = Min(m, q + 1);
while (k > 0)
{
kindex = k - 1;
if (P[kindex] == a)
{
for (j = kindex - 1; j >= 0; j--)
{
if (P[j] != P[q - kindex + j])
{
break;
}
}
if (-1 == j)
break;
}
k--;
}
transfer[q][a] = k;
}
}
}
void func3()
{
char *T = "aababacaabababacaaaeababaca";
int n, i, q, m;
char * P = "ababaca";
char *Set = "abcdefghijklmnopqrstuvwxyz";
func3_preprocess(P, Set);
n = strlen(T);
m = strlen(P);
q = 0;
for (i = 0; i < n; i++)
{
q = transfer[q][T[i]];
if (q == m)
printf("match, s is %d\n", i - m + 1);
}
}
/* KMP算法 */
char pi[64] = {0};
void func4_preprocess(char *P)
{
int m, k = 0;
int q;
m = strlen(P);
for (q = 2; q <= m; q++)
{
while (k > 0 && P[q - 1] != P[k])
{
k = pi[k];
}
if (P[q - 1] == P[k])
k++;
pi[q] = k;
}
}
void func4()
{
char *T = "aababacaabababacaaaeababaca";
char *P = "ababaca";
int m, n, q = 0;
int i;
func4_preprocess(P);
m = strlen(P);
n = strlen(T);
for (i = 0; i < n; i++)
{
while (q > 0 && P[q] != T[i])
{
q = pi[q];
}
if (P[q] == T[i])
q++;
if (q == m)
{
printf("match, s is %d\n", i - m + 1);
q = pi[q];
}
}
}
void main()
{
char T1[] = "ababa";
char P1[] = "aba";
char T2[] = {6, 3, 1, 4, 1, 5, 2};
char P2[] = {3, 1, 4, 1, 5};
//func1(T1, P1);
//func2(T2, 7, P2, 5);
//func3();
func4();
}
#ifdef _cplusplus
}
#endif