算法导论 chapter32 字符串匹配

在一个文本文件中查找指定模式的问题,就是一个字符串匹配问题,形式化定义:文本是一个长为n的数组T[1..n],模式是一个长为m的数组P[1..m],其中m\leq n,进一步假设P和T的元素都是来自一个有限字符集\Sigma的字符,例如\Sigma ​​​​​​= {0, 1}={0, 1}或者\Sigma = {a, b,...,z}。字符数组P和T就是我们所说的字符串。

0\leq s \leq n - m,并且T[s + 1..s + m] = P[1..m],那么说模式P在文本T中出现,并且偏移是s,这里s是数组下标。若P在T中以偏移s出现,称s是有效偏移,否则是无效偏移。字符串匹配就是找到所有有效偏移的过程,使得在这个有效偏移下,所给的模式P出现在文本T中。

除了朴素算法,每一种字符串匹配算法都基于模式进行预处理,然后找到所有的有效偏移,第二步成为匹配。每个算法的匹配时间和预处理时间如下:

算法                              预处理时间                              匹配时间

朴素算法                       0                                              O((n-m+1)m)

Rabin-Karp                   \Theta (m)                                       O((n-m+1)m)

有限自动机算法            O(m|\Sigma |)                                  \Theta (n)

KMP                             \Theta (m)                                        \Theta (n)

1.朴素字符串匹配算法

通过一个循环找到所有的有效偏移,该循环对n-m+1个可能的s值进行检测,看是否满足条件T[s + 1..s + m] = P[1..m],伪代码如下:

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行隐含一个对模式的遍历,因此时间界是O((n-m+1)m),当m=\left \lfloor n/2 \right \rfloor,时间是

2.Rabin-Karp算法

将字符串看作数字,字符串匹配的过程就是一个判断数字是否相等的过程。比如,假设\Sigma ={0,1,2,...,9},这样每个字符都是十进制数字,通常情况下,假定每个字符都是以d为基数表示的数字,其中d=|\Sigma |,比如字符有127个,那么d就是127,数字从0到126表示。下面都用十进制数字为例,可以用长度为k的十进制数字表示由k个字符表示的连续字符组成的数字,比如“31415”对应十进制数字31415。

算法描述:给定一个模式P[1..m],在T中每次取m位组成一个数字,判断这个数字和P是否相等。

时间:计算P的时间是\Theta (m),方法是使用霍纳法则;计算T中每个数字值的时间是\Theta (n-m),方法是使用如下公式:

t_{s+1}=10(t_{s}-10^{m-1}T[s+1])+T[s+m+1]

含义是,比如模式是5位数字31415,T中连续6位数字是314152,那么上次取到的数字是31415,下次应该取14152,将31415的最高位减掉,剩余4位乘10,然后加最低位的2,得到14152。

这样,此算法就是一个\Theta (m)的准备时间和一个\Theta (n-m)的匹配时间,但是这个时间忽略的P可能很大的问题,这个时间在P上的每次算数运算是常数这个假设不成立。解决此问题的方法是使用两个数对第三个数同模。

模运算:选择一个合适的模q,要求q是一个素数并且10q满足一个计算机字长,那么可以用单精度算数运算执行必须的计算。

模运算的时间:计算P的时间还是\Theta (m),用模替换原来的公式,如下:

t_{s+1}=(d(t_{s}-T[s+1]h)+T[s+m+1])modq

其中h\equiv d^{m-1}(modq)

原理是利用(a * b)modq = (amodq * bmodq)modq,(a + b)modq = (amodq + bmodq)modq

这样,比较数字变成了比较模,但是有一个问题,t_{s}\equiv p(modq)不能说明t_{s}=p,但是另一方面,若t_{s}\not\equiv p(modq),那么可以断定t_{s}\neq p,因此这项测试可以用来判定无效偏移,任何满足条件的偏移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算法的预处理时间是\Theta (m),最坏情况下匹配时间是\Theta ((n-m+1)m),原因是对于每个有效偏移进行显式验证,在实际应用中,可以期望有效偏移的数量少一些,期望匹配时间是O(n)。

3.有限自动机算法

有限自动机M是一个五元组(Q,q_{0},A,\Sigma ,\delta ),其中:

Q是状态的集合

q_{0}\in Q是初始状态

A\subseteq Q是一个特殊的接受状态的集合

\Sigma是有限输入字母表

\delta是一个Q\times \Sigma到Q的函数,称为M的转移函数

处理流程:有限自动机开始于状态q_{0},每次读取输入字符串的下一个字符。当有限自动机在状态q读取了一个字符a,那么它的状态从q变成\delta (q,a),即发生了一次状态变化。每当其状态属于A的时候,称自动机M接受了迄今为止读入的字符串。没有被接受的输入称为被拒绝的输入。

终态函数:有限自动机引入一个终态函数\phi,是从\Sigma ^{*}到Q的函数,满足\phi (w)是M在扫描字符串w后终止时的状态。当且仅当\phi (w)\in A时,M接受字符串w,可以用转移函数递归定义\phi,如下:

\phi (\varepsilon )=q_{0}

\phi (wa)=\delta (\phi (w),a), w\in \Sigma ^{*},a\in \Sigma

字符串匹配自动机:对于一个给定的模式P,可以在预处理阶段构造一个字符串匹配自动机,定义一个辅助函数\sigma

\sigma (x)=max(k:P_{k}\sqsupset x),即x的后缀P的最长前缀的长度。给定模式P[1..m],相应的字符串匹配自动机定义如下:

1.状态集合Q为{0, 1, ..., m},开始状态q_{0}为0,只有状态m是接受状态

2.对任意状态和字符a,状态转移函数定义为:

\delta (q,a)=\sigma (P_{q}a),其中P_{q}是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

这是一个时间为\Theta (n)的匹配,但是没有包含计算转移函数函数需要的预处理时间,先讨论这个函数的正确性。

引理32.2 (后缀函数不等式):对任意字符串x和字符a,\sigma (xa)\leq \sigma (x)+1

证明:显然

引理32.3 (后缀函数递归引理):对任意x和字符串a,若q=\sigma (x)\sigma (xa)=\sigma (P_{q}a)

证明:显然

定理32.4 如果\phi是字符串匹配自动机关于给定模式P的终态函数,T[1..n]是输入文本,则对i=0,1...,\phi (T_{i})=\sigma (T_{i})

证明:显然

由上面的定理可以证明使用自动机匹配字符串的正确性,接下来核心问题就是如何计算状态转移函数,伪码如下:

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。这个函数的时间界是O({m_{}}^{3}|\Sigma| )

4.KMP算法

此算法无需计算转移函数\delta,匹配时间是\Theta (n),只用到辅助函数\pi,它在\Theta (m)时间内根据模式预先计算出来,存储在数组\pi [1..m]中。对任意状态a = 0, 1, ..., m和任意字符a\in \Sigma\pi [q]的值包含了与a无关但是计算\delta (q,a)时需要的信息。由于数组\pi只有m个元素,而\delta\Theta (m|\Sigma |)个值,所以通过预先计算\pi而不是\delta,可以使计算时间减少一个\Sigma因子。

关于模式的前缀函数:前缀函数\pi包含模式与其自身偏移进行匹配的信息,可以用来在朴素字符串匹配算法中避免对无用偏移进行检测,也可以避免在字符串匹配自动机中,对整个转移函数\delta的预先计算。

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值