字符串匹配算法——Boyer-Moore算法

字符串匹配算法很多,Boyer-Moore算法也不算是效率最高的算法,它常用于各种文本编辑器的”查找”功能(Ctrl+F)。
比较经典的字符串模式匹配算法还有:Horspool算法、Sunday算法、KR算法、AC自动机等。不多说,进入主题。

Boyer-Moore算法概率

  1. 假定字符串为”HERE IS A SIMPLE EXAMPLE”,搜索词为”EXAMPLE”。
    字符串匹配算法——Boyer-Moore算法_第1张图片

  2. 首先,”字符串”与”搜索词”头部对齐,从尾部开始比较。
    字符串匹配算法——Boyer-Moore算法_第2张图片
    这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符肯定不是要找的结果。
    我们看到,”S”与”E”不匹配。这时,”S”就被称为”坏字符”(bad character),即不匹配的字符。我们还发现,”S”不包含在搜索词”EXAMPLE”之中,这意味着可以把搜索词直接移到”S”的后一位。

  3. 依然从尾部开始比较,发现”P”与”E”不匹配,所以”P”是”坏字符”。但是,”P”包含在搜索词”EXAMPLE”之中。所以,将搜索词后移两位,两个”P”对齐。
    字符串匹配算法——Boyer-Moore算法_第3张图片

  4. 我们由此总结出”坏字符规则”:后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置
    字符串匹配算法——Boyer-Moore算法_第4张图片
    如果”坏字符”不包含在搜索词之中,则上一次出现位置为 -1。
      以”P”为例,它作为”坏字符”,出现在搜索词的第6位(从0开始编号),在搜索词中的上一次出现位置为4,所以后移 6 - 4 = 2位。再以前面第二步的”S”为例,它出现在第6位,上一次出现位置是 -1(即未出现),则整个搜索词后移 6 - (-1) = 7位。

  5. 依然从尾部开始比较,”E”与”E”匹配。
    字符串匹配算法——Boyer-Moore算法_第5张图片

  6. 比较前面一位,”LE”与”LE”匹配。
    字符串匹配算法——Boyer-Moore算法_第6张图片

  7. 比较前面一位,”PLE”与”PLE”匹配。
    字符串匹配算法——Boyer-Moore算法_第7张图片

  8. 比较前面一位,”MPLE”与”MPLE”匹配。我们把这种情况称为”好后缀”(good suffix),即所有尾部匹配的字符串。注意,”MPLE”、”PLE”、”LE”、”E”都是好后缀。
    字符串匹配算法——Boyer-Moore算法_第8张图片

  9. 比较前一位,发现”I”与”A”不匹配。所以,”I”是”坏字符”。
    字符串匹配算法——Boyer-Moore算法_第9张图片

  10. 根据”坏字符规则”,此时搜索词应该后移 2 - (-1)= 3 位。问题是,此时有没有更好的移法?
    字符串匹配算法——Boyer-Moore算法_第10张图片

  11. 我们知道,此时存在”好后缀”。所以,可以采用”好后缀规则”:后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置
    字符串匹配算法——Boyer-Moore算法_第11张图片
    计算时,位置的取值以”好后缀”的最后一个字符为准。如果”好后缀”在搜索词中没有重复出现,则它的上一次出现位置为 -1。
      所有的”好后缀”(MPLE、PLE、LE、E)之中,只有”E”在”EXAMPLE”之中出现两次,所以后移 6 - 0 = 6位。
      

  12. 可以看到,”坏字符规则”只能移3位,”好后缀规则”可以移6位。所以,Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。
      更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了。
    字符串匹配算法——Boyer-Moore算法_第12张图片

  13. 继续从尾部开始比较,”P”与”E”不匹配,因此”P”是”坏字符”。根据”坏字符规则”,后移 6 - 4 = 2位。
    字符串匹配算法——Boyer-Moore算法_第13张图片

  14. 从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据”好后缀规则”,后移 6 - 0 = 6位,即头部的”E”移到尾部的”E”的位置。
    字符串匹配算法——Boyer-Moore算法_第14张图片

Boyer-Moore算法实现

/*
    函数:int* MakeBadCharTable(char *, int)
    目的:根据坏字符规则做预处理,建立一张坏字符表
    参数:
        p => 模式串P
        PLen => 模式串P长度
    返回:
        int* - 坏字符表
*/
int* MakeBadCharTable(char *p, int pLen)
{   
    int i;
    //为建立坏字符表,申请256int的空间
    /*之所以要申请256个,是因为一个字符是8位,所以字符可能有28次方即256种不同情况*/
    int *badchar = (int*)malloc(256*sizeof(int));

    if(badchar == NULL){
        printf("malloc failed!");
        return 0;
    }   

    //初始化坏字符表,256个单元全部初始化为pLen,没有在模式串出现的字符距离为pLen。
    for(i = 0; i < 256; i++){
        *(badchar+i) = -1;
    }

    //给表中需要赋值的单元赋值,不在模式串中出现的字符就不用再赋值了
    //以数组小标为字符键,以值为字符坏字符位置
    while(pLen != 0){
        *(badchar+(unsigned char)*p++) = pLen--;
    }

    return badchar;
}


/*
    函数:int* MakeGoodShiftTable(char *, int)
    目的:根据好后缀规则做预处理,建立一张好后缀表
    参数:
        p => 模式串P
        PLen => 模式串P长度
    返回:
        int* - 好后缀表
*/
int* MakeGoodShiftTable(char* p,int pLen){
    //为好后缀表申请pLen个int的空间
    int *shift = (int*)malloc(pLen*sizeof(int));
    int *sptr = shift + pLen - 1;//方便给好后缀表进行赋值的指标
    char *pptr = p + pLen - 1;//记录好后缀表边界位置的指标
    char c;

    if(shift == NULL){
        fprintf(stderr,"malloc failed!");
        return 0;
    }

    c = *(p + pLen - 1);//保存模式串中最后一个字符,因为要反复用到它

    *sptr = 1;//以最后一个字符为边界时,确定移动1的距离

    pptr--;//边界移动到倒数第二个字符(这句是我自己加上去的,因为我总觉得不加上去会有BUG,大家试试“abcdd”的情况,即末尾两位重复的情况)

    while(sptr-- != shift){//该最外层循环完成给好后缀表中每一个单元进行赋值的工作 
        char *p1 = p + pLen - 2, *p2,*p3;

        //do...while循环完成以当前pptr所指的字符为边界时,要移动的距离
        do{
            while(p1 >= p && *p1-- != c);//该空循环,寻找与最后一个字符c匹配的字符所指向的位置

            p2 = p + pLen - 2;
            p3 = p1;

            while(p3 >= p && *p3-- == *p2-- && p2 >= pptr);//该空循环,判断在边界内字符匹配到了什么位置

        }while(p3 >= p && p2 >= pptr);

        *sptr = shift + pLen - sptr + p2 - p3;//保存好后缀表中,以pptr所在字符为边界时,要移动的位置
        /*
          PS:在这里我要声明一句,*sptr = (shift + pLen - sptr) + p2 - p3;
             大家看被我用括号括起来的部分,如果只需要计算字符串移动的距离,那么括号中的那部分是不需要的。
             因为在字符串自左向右做匹配的时候,指标是一直向左移的,这里*sptr保存的内容,实际是指标要移动
             距离,而不是字符串移动的距离。我想SNORT是出于性能上的考虑,才这么做的。          
        */

        pptr--;//边界继续向前移动
    }
    return shift;
}


/*
    函数:int* BMSearch(char *, int , char *, int, int *, int *)
    目的:判断文本串T中是否包含模式串P
    参数:
        buf => 文本串T
        blen => 文本串T长度
        ptrn => 模式串P
        PLen => 模式串P长度
        skip => 坏字符表
        shift => 好后缀表
    返回:
        int - 1表示成功(文本串包含模式串),0表示失败(文本串不包含模式串)。
*/
int BMSearch(char *buf, int blen, char *ptrn, int plen, int *badchar, int *goodshift){
    int b_idx = plen;  
    if (plen == 0)
        return 1;
    while (b_idx <= blen){//计算字符串是否匹配到了尽头
        int p_idx = plen, skip_stride, shift_stride;
        while (buf[--b_idx] == ptrn[--p_idx]){//开始匹配
            if (b_idx < 0)
                return 0;
            if (p_idx == 0){   
                return 1;
            }
        }
        skip_stride = badchar[(unsigned char)buf[b_idx]];//根据坏字符规则计算跳跃的距离
        shift_stride = goodshift[p_idx];//根据好后缀规则计算跳跃的距离
        b_idx += (skip_stride > shift_stride) ? skip_stride : shift_stride;//取大者
    }
    return 0;
}

这个算法理解起来,比KMP容易多了,但是整体还是很复杂。效率看似不比KMP高多少,但是在实际应用的数据中,效率却高很多。
最后,概念和代码分别来自互联网,这里我只是做了一个比较全面的总结。我想重点在于理解算法的思路吧。
http://kb.cnblogs.com/page/176945/

你可能感兴趣的:(数据结构)