BM算法代码深入剖析

BM(Boyer-Moore)算法:是一种高效的字符串匹配算法,性能是相当之高,是KMP的几倍之多。

术语

在123456abc789中找abc

主串:123456abc789为主串

模式串:abc为字串

BM算法思想

通过增加匹配失败后一次移动的字符数,减少无效的匹配次数,从而增加匹配效率。

如何在增加移动字符数

BM算法充分利用了模式串的不变特性,和在发生不匹配时,已经匹配了的字符串及已知的不匹配字符串

abcacabcbc
   abcbc  

                                                                                     图1

这里引入两个概念

1、坏字符:图1红色字体a就是不匹配的坏字符

2、好后缀:图1的绿色字体bc就是好后缀

BM算法原理

坏字符规则

根据坏字符和模式串,BM算法是如何一次移动多位的,如何增加效率的

我们试着逐步移动,将模式串向右移动一位

abcacabcbc
    abcbc 

                                                                                              图2

 

其他的我们先不考虑,我们分析我们已知的坏字符,与移位后的对应模式串上的b是不匹配的,我们继续右移一位

abcacabcbc
     abcbc

                                                                                                           图3

 

移位后主串上坏字符,与移位后的模式串对应位置的字符是匹配的。

从上面分析我们可知,在匹配出现坏字符时,我们需要右移,而模式串中与当前坏字符位置(即a)可匹配的位置,可以自模式串右边向左边逐一比较获取,这样我们就一次移动到模式串与坏字符匹配的位置了。

而逐一向前对比结果是增加了模式串向前移动的字符数,但是还是需要对比O(m)次,性能并没有提高,而模式串内容是固定的,我们可以把模式串中每个字符在模式串中的位置通过hash表缓存起来,

hash算法:

哈希函数:我们可以直接取字符的ascii码值

所以将字符的ascii码值作为数组下标,对应的在模式串中的位置作为值,ascii是一个字符大小,所以申请255大小的数组bc,数组的初始化代码如下:

    private void generateBC() {
        //所有bc初始化为-1
        for (int i = 0; i < bc.length; i++) {
            bc[i] = -1;
        }

        for (int i = 0; i < b.length; i++) {
            // 将字串i处内容的ascii码值作为缓存下标,其下标作为缓存的值
            int ascii = b[i];
            bc[ascii] = i;
        }
    }

如此上面就不要逐一比较了,直接使用bc[a]=0就能找到最近的匹配位置。

                                                                                   图4

根据上面的图坏字符=a,s=2,模式串中对应匹配x=bc[a]=0,

向右移动字符数=S-X=2-0=2,

坏字符缺点:

                                                                                   图5

如果匹配串如上图,坏字符在模式串中的匹配位置在右边即X=bc[a]=3, 移动字符数=S-X=2-3=-1,

所以要配合好后缀一起使用

好后缀规则:

好后缀的规则比较复杂一些,但基本上和坏字符原理类似。

abcacbbcbc
  abcdbc  

                                                                                               图6


匹配好的(绿色字体)bc是好后缀,记作{u},拿bc到模式串中查找,如果找到另一个跟{u}匹配的字串{u*},我们就可以将模式串移动到{u*}的起始位置。

                                                                            图7

如图7:移动字符数=S-GS+1

我们再来看看图6应该向右移动多少:3-1+1=3,移动后结果为

abcacbbcbc 
     abcdbc

                                                                          图8

 

另一种情形,如果好的后缀在模式串中没有找到另一个匹配字符串,是否就可以直接将模式串滑动到{u}后面,我们来看看下面这种情形

abcacbcdbc
   cdbc   

                                                                          图9

上图9,如果在模式串中没有找到对应的,直接将模式串启动到bc之后,将会错过匹配串。

abcacbcdbc  
   cdbc     
       cdbc移动到{u}之后
      cdbc 错过的匹配处

                                                                        图10

上图就是出现滑动过度情形,所以处理好的后缀的时候,不仅要看完整的后缀,而且要看后缀的子后缀是否存在跟模式串的前缀子串匹配。

后缀子串及后缀字串字串是否存在匹配前缀字串的缓存表达:

好后缀与坏字符类似,也是使用了缓存数组,这里suffix针对不同长度的后缀,在模式串中对应的匹配索引,及是否存在匹配的前缀字串prefix。

cdbcdb
      
 后缀子串长度suffixprefix 
 b12TRUE 
 db21FALSE 
 cdb30TRUE 
 bcdb4-1FALSE 
 dbcdb5-1FALSE 

模式串前缀缓存处理

     public void generateGs() {
        // 所有suffix初始化位-1,所有prefix初始化为prefix
        for (int i = 0; i < b.length-1; i++) {
            suffix[i]=-1;
            prefix[i] = false;
        }

        int m = b.length;
        for (int i = 0; i < m-1; i++) {
            int j = i;
            int k = 0;
            //两端从长度为1对比,每次符合条件刷新suffix,如此,suffix匹配会是最靠近右端
            while (j >= 0 && b[m-1-k] == b[j]) {
                j--;
                k++;
                suffix[k] = j+1;
            }
            if (j == -1) {
                prefix[k] = true;
            }
        }
    }

用cbdcbd这个模式串分析;

  1. i = 0, j=0,k=0
    1. b[m-1-k]==b[j] => b[5]==b[0] ? => c==d? 不等, j==-1? 不等,结束
  2. i=1
    1. j=1,k=0 b[m-k-1]==b[j] => b[5]==b[1] =>  d==b?不等,j==-1?不等,结束
  3. i=2
    1. j=2,k=0 b[m-k-1]==b[j] => b[5]==b[2] => d==d?等,suffix[1]=j=2
    2. j=1,k=1 b[m-k-1]==b[j] => b[4]==b[1] => b==b?等, suffix[2]=j=1
    3. j=0,k=2 b[m-k-1]==b[j] => b[3]==b[0] => c==c?等, suffix[3]=j=0
    4. j=-1,k=3 => prefix[3]=true
  4. i=3
    1. j=3,k=0 => b[5]==b[3] => d==c>不等结束
  5. i=4
    1. j=4,k=0 => b[5]==b[4] => d==b,不等结束

从上面代码分析可看出,该部分代码是从位置0,逐步与后缀比对,找到最靠右的与不同长度后缀相同的索引,且如果完全匹配,则prefix为true

BM算法代码实现

 public int bm(String str) {
        char[] a = str.toCharArray();
        generateBC();
        generateGs();

        //print();
        int i = 0;
        int n = a.length;
        int m = b.length;
        while (i <= n - m) {
            // 计算坏字符
            int j;
            // 当前对比的是i位开始,与模式串从右自左的对比
            for (j = m-1; j>=0; j--) {
                if (a[i+j] != b[j]) {
                    break;
                }
            }

            // 如果全部比完说明完全匹配
            if (j < 0) {
                return i;
            }
            
            int x = j - bc[a[i+j]];
            int y = 0;
            if (j < m-1) {
                y = moveByGS(j, m);
            }
            i += x > y ? x : y;
        }
        return -1;
    }

    /**
     * 计算好后缀
     * @param j
     * @param m
     * @return
     */
    private int moveByGS(int j, int m) {
        // 好后缀长度
        int k = m - j - 1;
        //模式串存在一个好后缀匹配的子串,返回其索引
        if (suffix[k]!=-1) {
            return j-suffix[k]+1;
        }
        
        //如果没有找到,则看好后缀子后缀在模式串是否有前缀字串匹配
        //使用j+2的原因是因为j为坏字符,j+1是好后缀的起始位置,j+2是好后缀的第一个子后缀,子后缀的位置不超过m-1
        for (int r = j+2; r <= m-1; r++) {
            if (prefix[m-r]) {
                return r;
            }
        }

        return 0;
    }

使用下面数据模拟运行上面代码

fbcbbcacbcb
cbcb       

n=11,m=4

  1. i=0
    1. j=m-1=3,a[i+j]=b[j] => a[3]=b[3] =>b=b?继续
    2. j=2,a[2]==b[2] => a[2]=b[2]=> c=c?继续
    3. j=1,b==b继续
    4. j=0, f!=b
    5. 坏字符移动距离x=s-bc[f]=0-(-1)=1
    6. 好后缀moveByGS
      1. k=m-j-1 => k = 4-0 -1 = 3
      2. suffix[3] = -1
      3. 子串中不存在与好后缀完全匹配的子串{u*}
      4. 继续寻找好后缀的子后缀,模式串中是否存在前缀与子后缀匹配的
      5. r=2, prefix[4-2]=prefix[2]=true,所以返回2
    7. i+=Max(坏字符右移,好后缀右移)=> MAX(-1, 2)=2,i=2
    8. fbcbbcacbcb
        cbcb     
  2. i=2
    1. j=3,c!=b
    2. 坏字符移动距离x=j-bc[a[i+j]]=3-bc[a[5]]=3-bc[c]=3-0=3
    3. j=m-1,不存在好后缀
    4. i += MAX(坏字符右移,好后缀右移)=MAX(3,0)=3, i=5
    5. fbcbbcacbcb
           cbcb  
  3. i=5
    1. j=3, b==b,继续
    2. j=2, c==c
    3. j=1, a!=b
    4. x = j-bc[a[i+j]=>3-bc[a]=>1-(-1)=2
    5. y=moveByGS,好后缀=cb
      1. 好后缀长度=m-j-1=4-1-1=2
      2. suffix[2]=0,移动字符数=j-suffix[2]+1=1-0+1=2
    6. i+=MAX(2,2)=2=>i=7
    7. fbcbbcacbcb
             cbcb
  4. i=7
    1. 字符串完全匹配

以上是BM算法代码的单步分析,相信如果前面基本原理没有太理解,通过代码的逐步分析也会对BM算法有着更深一层的理解。

BM算法我们分析的差不多了,从BM算法我们是否可以看出一些程序设计的思想

1、 程序中存在大量量不是很大的重复数据查找,我们可是使用hash算法,使O(n)的时间复杂度一下就到了O(1),即热门数据缓存,

2、合理范围内数据可选择好的散列表,减小查询时间复杂度

BM算法就是充分分析出重复的数据操作,与合理利用已处理过的信息

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值