算法设计与分析——串匹配问题(Java)

【问题】给定两个字符串 S 和 T,在主串 S 中查找子串 T 的过程称为串匹配( stringmatching,也称模式匹配),T 称为模式。在文本处理系统、操作系统、编译系统、数据库系统以及 Internet 信息检索系统中,串匹配是使用最频繁的操作。

    串匹配问题具有下面两个明显的特征:

        (1)问题的输入规模很大,常常需要在大量信息中进行匹配,因此,算法的一次执行时间不容忽视;

        (2)匹配操作经常被调用,执行频率高,因此,算法改进所取得的效益因积累往往比表面上看起来要大得多。

【想法 1】应用蛮力法解决串匹配问题的过程是:从主串 S 的第一个字符开始和模式 T 的第一个字符进行比较,若相等,则继续比较二者的后续字符;若不相等,则从主串 S 的第二个字符开始和模式 T 的第一个字符进行比较,重复上述过程,若 T 中的字符全部比较完毕,则说明本趟匹配成功:若 S 中的字符全部比较完毕,则匹配失败。这个算法称为朴素的模式匹配算法,简称BF算法,如下图所示。  

设主串 S="abcabcacb",模式 T="abcac",BF 算法的匹配过程如下图所示。 

【算法 1】设字符数组 S 存放主串,字符数组 T 存放模式,BF算法用伪代码描述如下。

算法:串匹配算法 BF

输入:主串 S,模式T

输出:T 在 S 中的位置

1.初始化主串比较的开始位置 index=0;

2.在串 S 和串 T 中设置比较的起始下标 i=0,j=0;

3.重复下述操作,直到 S 或 T 的所有字符均比较完毕:

    3.1 如果 S[ i ] 等于T[ j ],则继续比较 S 和 T 的下一对字符;

    3.2 否则,下一趟匹配的开始位置 index++,回溯下标 i=index,j=0;

4.如果 T 中所有字符均比较完,则返回匹配的开始位置 index;否则返回 0;

【算法分析 1】设主串 S 长度为 n,模式 T 长度为 m,在匹配成功的情况下,考虑最坏情况,即每趟不成功的匹配都发生在模式 T 的最后一个字符。
例如:S= "aaaaaaaaaaab"
           T="aaab"
设匹配成功发生在 si 处,则在 i-1 趟不成功的匹配中共比较了 (i-1)*m 次,第 i 趟成功的匹配共比较了 m 次,所以总共比较了 i*m 次,因此平均比较次数是:

一般情况下,m<<n,因此最坏情况下的时间复杂性是 O(n*m)。 

【算法实现 1】BF 算法用JAVA语言描述如下:

public class BruteForce {
    static int BF(char S[], char T[])
    {
        int index = 0;                            //主串从下标0开始第一趟匹配
        int i = 0, j = 0;                           //设置比较的起始下标
        while ((S[i] != '\0') && (T[j] != '\0'))
        {
            if (S[i] == T[j]) {i++; j++;}
            else {index++; i = index; j = 0; }          //i和j分别回溯
        }
        if (T[j] == '\0') return index + 1;      //返回本趟匹配的开始位置(不是下标)
        else return 0;
    }
    public static void main(String args[]){
        String S = "abcabcabcaccb\0";
        char[] s=S.toCharArray();
        String T = "abcacc\0";
        char[] t=T.toCharArray();
        int index=BF(s,t);
        System.out.println("T在S中的位置是:"+index);
    }
}

运行结果如下 1:

 

【想法 2】分析 BF 算法的执行过程,造成 BF 算法效率低的原因是回溯,即在某趟匹配失败后,对于主串 S 要回溯到本趟匹配开始字符的下一个字符,模式 T 要回溯到第一个字符,而这些回溯往往是不必要的。观察 BF 算法的执行过程,在第 1 趟匹配过程中,S[ 0 ] ~ S[ 3 ] 和 T[ 0 ] ~ T[ 3 ]匹配成功,S[ 4 ] ≠ T[ 4 ] 匹配失败,因此有了第 2 趟。因为在第 1 趟中有 S[ 1 ] = T[ 1 ],而 T[ 0 ] ≠ T[ 1 ],因此有 T[ 0 ] ≠ S[ 1 ],所以第 2 趟是不必要的,同理第 3 趟也是不必要的,可以直接到第 4 趟。进一步分析第 4 趟中的第一对字符 S[ 3 ] 和 T[ 0 ] 的比较是多余的,因为第 1 趟中已经比较了 S[ 3 ] 和 T[ 3 ],并且 S[ 3 ] = T[ 3 ],而 T[ 0 ] = T[ 3 ],因此必有 S[ 3 ] = T[ 0 ],因此第 4 趟比较可以从第二对字符 S[ 4 ] 和 T[ 1 ] 开始进行,这就是说,第 1 趟匹配失败后,下标 i 不回溯,而是将下标 j 回溯至第 2 个字符,从 T[ 1 ] 和 S[ 4 ] 开始进行比较。
        综上所述,希望某趟在 S[ i ] 和 T[ j ] 匹配失败后,下标 i 不回溯,下标 j 回溯至某个位置 k,从 T[ k ] 和 S[ i ] 开始进行比较。显然,关键问题是如何确定位置 k?
        观察部分匹配成功时的特征,某趟在 S[ i ] 和 T[ j ] 匹配失败后,下一趟比较从 S[ i ] 和 T[ k ]开始,则有 T[ 0 ] ~ T [ k - 1 ] = S[ i - k ] ~ S[ i - 1 ]成立,如下图 (a) 所示;在部分匹配成功时,有T [ j - k ] ~ T[ j - 1 ] = S [ i - k ] ~ S [ i - 1 ] 成立,如下图 (b) 所示。

由 T[ 0 ] ~ T[ k - 1 ] = S[ i - 1 ] 和 T[ j - k ] ~T[ j - 1 ] = S[ i - k ] ~S[ i - 1 ],可得:
                                            T[ 0 ] ~ T[ k - 1 ] = T[ j - k ] ~ T[ j - 1 ] 
上式说明,模式中的每一个字符 T[ j ] 都对应一个 k 值,这个 k 值仅依赖于模式本身,与主串无关,且 T[ 0 ] ~ T[ k - 1] 是 T[ 0 ] ~ T[ j - 1 ] 的真前缀,T [ j - k ] ~ T[ j - 1] 是 T[ 0 ] ~ T[ j - 1 ] 的真后缀,k 是 T[ 0 ] ~ T[ j - 1 ] 的真前缀和真后缀相等的最大子串的长度。用 next[ j ] 表示 T[ j ] 对应的 k 值 ( 0<= j < m ,其定义如下:

        设模式 T="ababc",根据 next[ j ] 的定义,计算过程如下:

        j = 0 时,next[ 0 ] = -1

        j = 1 时,next[ 1 ] = 0

        j = 2 时,T[ 0 ] ≠ T[ 1 ],则next[ 2 ] = 0

        j = 3 时,T[ 0 ] T [1 ] ≠ T[ 1 ] T[ 2 ],T[ 0 ] = T[ 2 ],则next[ 3 ] = 1

        j = 4 时,T[ 0 ] T[ 1 ] T[ 2 ] ≠ T[ 1 ] T[ 2 ] T[ 3 ],T[ 0 ] T[ 1 ] = T[ 2 ] T[ 3 ],则next[ 4 ] = 2

       设主串S="ababaababcb",模式T="ababc" ,模式 T 的 next 值为 {-1,0,0,1,2},改进的串匹配算法(称为KMP算法)的匹配过程如下图所示。

 【算法 2】在求得了模式 T 的 next 值后,KMP 算法用伪代码描述如下。

算法:串匹配算法KMP

输入:主串 S,模式 T

输出:T 在 S 中的位置

1.在串 S 和串 T 中分别设置比较的起始下标 i = o,j = 0;

2.重复下述操作,直到 S 或 T 的所有字符均比较完毕:

    2.1 如果S[ i ] 等于 T[ j ],则继续比较 S 和 T 的下一对字符;

    2.2 否则,将下标 j 回溯到 next[ j ] 位置,即 j = next[ j ];

    2.3 如果 j 等于-1,则将下标 i 和 j 分别加 1,准备下一趟比较;

3.如果 T 中所有字符均比较完毕,则返回本趟匹配的开始位置;否则返回0;

【算法分析2】在求得模式 T 的 next 值后,KMP 算法只需将主串扫描一遍,设主串的长度为 n,则 KMP 算法的时间复杂性是 O(n) 。
【算法实现2】可以用蛮力法求得模式 T 的 next 值,KMP 算法用JAVA语言描述如下:

public class KMPSF {

    int KMP(char S[],char T[])
    {
        int i = 0, j = 0;
        int [] next=new int[80];
        next[0]=-1;
        GetNext(T,next);
        while (S[i] != '\0' && T[j] != '\0')
        {
            if(S[i] == T[j])
            {
                i++; j++;
            }
            else
            {
                j = next[j];
                if (j == -1) {i++; j++;}
            }
        }
        if(T[j] == '\0') return i - T.length + 1;
        else return 0;
    }

    void GetNext(char T[ ], int next[ ])
    {
        int j = 0, k = -1;
        next[0] = -1;
        while (T[j] != '\0')                           //直到字符串末尾
        {
            if (k == -1) {                           //无相同子串
                next[++j] = 0; k = 0;
            }else if (T[j] == T[k]) {                //确定next[j+1]的值
                k++;
                next[++j] = k;
            } else k = next[k];          //取T[0]...T[j]的下一个相等子串的长度
        }
    }

    public static void main(String args[]){
        char S[]={'a','b','a','b','c','a','b','c','a','c','b','a','b','\0'};
        char T[]={'a','b','c','a','c','\0'};
        KMPSF kmpsf=new KMPSF();
        int index= kmpsf.KMP(S,T);
        System.out.println("T在S中的位置是:"+index);
    }
}

运行结果如下 2:

 

from:算法设计与分析(第2版)——王红梅 胡明 编著——清华大学出版社

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
利用Java编写的几种经典问题算法: 1.设a[0:n-1]是一个有n个元素的数组,k(0<=k<=n-1)是一个非负整数。 试设计一个算法将子数组a[0:k]与a[k+1,n-1]换位。要求算法在最坏情况下耗时O(n),且只用到O(1)的辅助空间。 2.在一个圆形操场的四周摆放着n堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。试设计一个算法,计算出将n堆石子合并成一堆的最小得分和最大得分,并分析算法的计算复杂性。 3.设磁盘上有n个文件f1,f2,…,fn,每个文件占用磁盘上的1个磁道。这n个文件的检索概率分别是p1,p2,…,pn,且 =1。磁头从当前磁道移到被检信息磁道所需的时间可用这2个磁道之间的径向距离来度量。如果文件fi存放在第i道上,1≦i≦n,则检索这n个文件的期望时间是对于所有的i<j,time+=pi*pj*d(i,j) 。其中d(i,j)是第i道与第j道之间的径向距离|i-j|。磁盘文件的最优存储问题要求确定这n个文件在磁盘上的存储位置,使期望检索时间达到最小。试设计一个解决此问题的算法,并分析算法的正确性与计算复杂度。 4.最小重量机器设计问题。设某一机器由n个部件组成,每一种部件可以从m个不同的供应商处购得。设wij是从供应商j处购得的部件i的重量,cij是相应的价格。试设计一个算法,给出总价格不超过c的最小重量机器设计。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值