zoukankan      html  css  js  c++  java
  • 字符串匹配算法——Boyer-Moore算法

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

    Boyer-Moore算法概率

    1. 假定字符串为”HERE IS A SIMPLE EXAMPLE”,搜索词为”EXAMPLE”。
      这里写图片描述

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

    3. 依然从尾部开始比较,发现”P”与”E”不匹配,所以”P”是”坏字符”。但是,”P”包含在搜索词”EXAMPLE”之中。所以,将搜索词后移两位,两个”P”对齐。
      这里写图片描述

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

    5. 依然从尾部开始比较,”E”与”E”匹配。
      这里写图片描述

    6. 比较前面一位,”LE”与”LE”匹配。
      这里写图片描述

    7. 比较前面一位,”PLE”与”PLE”匹配。
      这里写图片描述

    8. 比较前面一位,”MPLE”与”MPLE”匹配。我们把这种情况称为”好后缀”(good suffix),即所有尾部匹配的字符串。注意,”MPLE”、”PLE”、”LE”、”E”都是好后缀。
      这里写图片描述

    9. 比较前一位,发现”I”与”A”不匹配。所以,”I”是”坏字符”。
      这里写图片描述

    10. 根据”坏字符规则”,此时搜索词应该后移 2 - (-1)= 3 位。问题是,此时有没有更好的移法?
      这里写图片描述

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

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

    13. 继续从尾部开始比较,”P”与”E”不匹配,因此”P”是”坏字符”。根据”坏字符规则”,后移 6 - 4 = 2位。
      这里写图片描述

    14. 从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据”好后缀规则”,后移 6 - 0 = 6位,即头部的”E”移到尾部的”E”的位置。
      这里写图片描述

    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/

    生命不止,奋斗不息!
  • 相关阅读:
    PostgreSQL 语法
    Linux 上安装 PostgreSQL
    Gitlab基础知识介绍
    Grafana 入门知识介绍
    数据卷容器
    Docker网络详解——原理篇
    Docker网络详细理解-容器网络互通
    搭建Elasitc stack集群需要注意的日志问题
    创建Elasticsearch集群并为它们配置TLS安全通信
    Elastic:为Elastic Docker部署设置安全
  • 原文地址:https://www.cnblogs.com/huzongzhe/p/6735159.html
Copyright © 2011-2022 走看看