程序设计方法与实践-时空权衡

什么是时空权衡?

时空权衡是算法设计中的一个众所周知的问题,也就是对算法的空间和时间效率做出权衡,它大概有分两种形式:

  • 对输入的部分数据或者全部数据作预处理,然后对于获得额外信息储存起来,从而加快后面问题的求解速度——加强输入
  • 使用额外的空间来储存已有的信息,使达到加快问题求解的目的——预构造

输入增强

  • 计数排序
  • 字符串匹配

计数排序

思路:

对于待排序列表中的每一个元素,算出比该元素小的元素的数量,并将这些数据记录在一张表中。

伪码描述:

Comparison ( A[0,2……n-1] ){
    for(int i=0; i<n; i++) Count[i]=0;  //Count用来记录多少元素比他它小,一定要初始化!
    for(int i=0; i<n; i++){
        for(int j=i+1; j<n; j++){
            if(A[j]<a[i]) Count[i]++;
            else Count[j]++;
        }
    }
    for(int i=0; i<n; i++){
        S[Count[i]]=A[i];  //这里很有意思,Count存的也正好,该元素在有序表中的下标
    }
    return S;
}

过程详解: 

分析总结:

如果只从这个角度看,似乎这个算法并没有多么高效。不难分析,它的时间复杂度为\Theta (n^{2}),并不比蛮力法要好,但是在一些特殊情况下,计数排序是有一定优势的。

例如,如果待排序列表中的元素都来自于某个数列,那么此时使用计数排序将会高效很多:

如图,待排序列表中有11、12、13,它们都在数列[11,12,13]中,那么我通过计数排序的思想,先去统计11,12,13分别出现了多少次,然后将这个数据存在一张表中,而也代表了这个元素在有序表中最后一次出现的下标:

DistributionCounting (A[0,1……],U,L){  //U是元素最大值,L是最小值
    for(int i=0; i<U-L; i++)  D[i]=0;
    for(int j=0; j<n; j++){
        D[A[j]-L]++;
    }
    for(int j=1; j<U-L; j++) D[j]=D[j-1]+D[j];  //这个好好理解,实现了数组的复用
    for(int i=n-1; i>=0; i--){
        j=A[i]-L;
        S[D[j]-1]=A[i];
        D[j]--;
    }
    return S;
}

此时的时间的复杂度是\Theta (n)的,也就是线性的,这是因为这个算法充分利用了数组的自然属性。

Boyer-Moore(BM算法) 

我们先回想一下字符匹配的暴力算法,它的最坏时间复杂度是\Theta (mn),而最效率度是\Theta (n)也就是线性的。接着让我们来了解一下BM算法。

Hoespool算法

首先在讲解BM算法之前,我们先了解一下BM算法的简化版——Horspool算法:

  • 与蛮力法相比较,Horspool算法在匹配方式上的区别就在于,蛮力法是从模式串的左边向右边匹配,而Horspool算法是从模式串的右边向左边进行匹配的
  • 实现Horspool算法的关键就在于建立一个“坏”字符表,表中储存的是,当字符串当前匹配失败是时,字符串应该移动多少的长度。

//要注意的是这个表是以文本串中可能出现的字符为索引的

  • 对于每一个字符可以创建下面的字符移动公式:

t(c)=m

t(c)=模式串中前m-1个字符的最后一个c,到模式串的最后一个字符的距离

过程详解:

文本串:S[ ]

模式串:BARBER

  1. 对于文本串中的所有字符,除了字符为E,B,R,A的单元格分别填上1,2,3,4,其他所有没有在模式串中出现的字符的单元格全部是6(模式串的长度)。
  2. 然后建立“坏”字符表,从左到右扫描模式串表,对前m-1个字符的第i个字符在文本串中对应的单元格的值改为m-1-i(就是该字符到最右端的距离)。(要注意的是应该重复扫描m-1次)

伪码描述:

HorsopoolMatching (S[0..m-1],T[0..n-1]){
    ShiftTable(P[0..m-1]);
    i=m-1;
    while(i<n){
        k=0;
        while(k<m && S[m-1-k]==T[i-k])
            k++;
        if(k==m)
            return i-(m-1);
        else
            i=i+Table[T[i]];
    }
    return -1;
}

建立移动表的函数如下:

ShiftTable(P[0..m-1]) {
    //创建一个以文本串字符为索引的Table数组,并将其全部初始化为m
    for(int j=0; j<m-2; j++){
        Table[P[j]]=m-1-j;
    }
    return Table;
}

下面是一个匹配的具体过程,方便大家理解:

可以看到显示建立了一个移动字符表,现将文本串与模式串对齐,并从模式串右边开始匹配,A-R,匹配失败,去查字符移动表,A(4),即将模式串向右移动四个单位,后面的过程以此类推。

PS:当匹配失败时,要查表的字符并不是当前匹配失败的字符,而应该是模式串末尾对应文本串中字符的移动表值!!!

BM算法

值得注意的是,BM算法在匹配失败时,模式串移动的距离d应该由两个数值决定。

第一个数值d1:当字符匹配失败时,已经有k(0<=k<m)个字符匹配成功,那么BM算法与Horspool算法的处理不同。BM算法的处理是将模式串向右移动t(c)-k个单位。

第二个数值d2:构建一个好后缀移动表

k代表的是,模式串的末尾k位,把其称作好的后缀。例如,k=1时,后缀为B,而模式串中有3个这样的后缀,d2会等于将从右往左数第二个后缀与移动到右往左数第一个后缀的距离,2。又如,k=2时,后缀为AB,模式串中有2个后缀AB,d2会等于将倒数第一个AB移动倒数第二个AB的距离,4。但要注意的是,当这一个后缀只在模式串中出现了一次时,就需要观察这个后缀的子集,例如,k=3时,后缀为BAC,但是模式串中只有一个BAB,此时我们去观察它的子集,发现子集AB存在,所以此时d2=将从右往左数第二个AB与移动到右往左数第一个AB的距离

PS:而此时的要查表的字符就是匹配失败的那个字符!!!

总结一下:就是

下面详细讲解一个过程,先给出文本串,模式串:

然后建立”坏“字符表,以及好后缀移动表

 然后进行字符串匹配:

 以上是讲解了关于是时空权衡的两个经典算法,计数排序、BM算法,其中BM算法的过程可能比较难以理解,需要大家反复琢磨思考,其实并不难,只是细节东西比较多。

加油!!!加油!!!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值