字符串模式匹配的BM算法

轉自:http://blog.csdn.net/zhoubl668/article/details/7321264

作者:zhoubl668


由於畢業設計(入侵檢測)的需要,這兩天仔細研究了BM模式匹配算法,稍有心得,特此記下。

 

  首先,先簡單說明一下有關BM算法的一些基本概念。

 

  BM算法是一種精確字符串匹配算法(區別於模糊匹配)。

 

  BM算法採用從右向左比較 的方法,同時應用到了兩種啓發式規則,即壞字符規則 和好後綴規則,來決定向右跳躍的距離。

 

  BM算法的基本流程: 設文本串T,模式串爲P。首先將T與P進行左對齊,然後進行從右向左比較 ,如下圖所示:

    

   若是某趟比較不匹配時,BM算法就採用兩條啓發式規則,即壞字符規則 和好後綴規則 ,來計算模式串向右移動的距離,直到整個匹配過程的結束。

         

    下面,來詳細介紹一下壞字符規則 和好後綴規則 。

    首先,詮釋一下壞字符和好後綴的概念。

    請看下圖:

    

     圖中,第一個不匹配的字符(紅色部分)爲壞字符,已匹配部分(綠色)爲好後綴。

    

    1)壞字符規則(Bad Character):

         在BM算法從右向左掃描的過程中,若發現某個字符x不匹配,則按如下兩種情況討論:

               i.  如果字符x在模式P中沒有出現,那麼從字符x開始的m個文本顯然不可能與P匹配成功,直接全部跳過該區域即可。

               ii. 如果x在模式P中出現,則以該字符進行對齊。

 

         用數學公式表示,設Skip(x)爲P右移的距離,m爲模式串P的長度,max(x)爲字符x在P中最右位置。

 

                     

 

         例1:

         下圖紅色部分,發生了一次不匹配。

 

             

        計算移動距離Skip(c) = 5 - 3 = 2,則P向右移動2位。

        移動後如下圖:

              

        

    2)好後綴規則(Good Suffix):

         若發現某個字符不匹配的同時,已有部分字符匹配成功,則按如下兩種情況討論:

              i.  如果在P中位置t處已匹配部分P'在P中的某位置t'也出現,且位置t'的前一個字符與位置t的前一個字符不相同,則將P右移使t'對應t方纔的所在的位置。

              ii. 如果在P中任何位置已匹配部分P'都沒有再出現,則找到與P'的後綴P''相同的P的最長前綴x,向右移動P,使x對應方纔P''後所在的位置。  

 

         用數學公式表示,設Shift(j)爲P右移的距離,m爲模式串P的長度,j 爲當前所匹配的字符位置,s爲t'與t的距離(以上情況i)或者x與P''的距離(以上情況ii)。

           

         以上過程有點抽象,所以我們繼續圖解。

         例2:

         下圖中,已匹配部分cab(綠色)在P中再沒出現。

         

         再看下圖,其後綴T'(藍色)與P中前綴P'(紅色)匹配,則將P'移動到T'的位置。

         

         移動後如下圖:

        

          

 

 

          自此,兩個規則講解完畢。

 

     在BM算法匹配的過程中,取SKip(x)與Shift(j)中的較大者作爲跳躍的距離。

    

     BM算法預處理時間複雜度爲O(m+s),空間複雜度爲O(s),s是與P, T相關的有限字符集長度,搜索階段時間複雜度爲O(m·n)。

    

       最好情況下的時間複雜度爲O(n/m),最壞情況下時間複雜度爲O(m·n)。



 BM算法的實現很複雜,但是高手程序員就是能寫出令人敬佩的代碼。


    當然,高手程序員不是指我自己,是SNORT2.7.0的設計者/實現者之一。  


    下面是SNORT2.7.0中提取出的代碼。

    我將以行注的形式進行講解

 /* 
    函數:int* MakeSkip(char *, int) 
    目的:根據壞字符規則做預處理,建立一張壞字符表 
    參數: 
        ptrn => 模式串P 
        PLen => 模式串P長度 
    返回: 
        int* - 壞字符表 
*/  
int* MakeSkip(char *ptrn, int pLen)  
{     
    int i;  
    //爲建立壞字符表,申請256個int的空間  
    /*PS:之所以要申請256個,是因爲一個字符是8位, 
      所以字符可能有2的8次方即256種不同情況*/  
    int *skip = (int*)malloc(256*sizeof(int));  
  
    if(skip == NULL)  
    {  
        fprintf(stderr, "malloc failed!");  
        return 0;  
    }     
  
    //初始化壞字符表,256個單元全部初始化爲pLen  
    for(i = 0; i < 256; i++)  
    {  
        *(skip+i) = pLen;  
    }  
  
    //給表中需要賦值的單元賦值,不在模式串中出現的字符就不用再賦值了  
    while(pLen != 0)  
    {  
        *(skip+(unsigned char)*ptrn++) = pLen--;  
    }  
  
    return skip;  
}  
  
  
/* 
    函數:int* MakeShift(char *, int) 
    目的:根據好後綴規則做預處理,建立一張好後綴表 
    參數: 
        ptrn => 模式串P 
        PLen => 模式串P長度 
    返回: 
        int* - 好後綴表 
*/  
int* MakeShift(char* ptrn,int pLen)  
{  
    //爲好後綴表申請pLen個int的空間  
    int *shift = (int*)malloc(pLen*sizeof(int));  
    int *sptr = shift + pLen - 1;//方便給好後綴表進行賦值的指標  
    char *pptr = ptrn + pLen - 1;//記錄好後綴表邊界位置的指標  
    char c;  
  
    if(shift == NULL)  
    {  
        fprintf(stderr,"malloc failed!");  
        return 0;  
    }  
  
    c = *(ptrn + pLen - 1);//保存模式串中最後一個字符,因爲要反覆用到它  
  
    *sptr = 1;//以最後一個字符爲邊界時,確定移動1的距離  
  
    pptr--;//邊界移動到倒數第二個字符(這句是我自己加上去的,因爲我總覺得不加上去會有BUG,大家試試“abcdd”的情況,即末尾兩位重複的情況)  
  
    while(sptr-- != shift)//該最外層循環完成給好後綴表中每一個單元進行賦值的工作  
    {  
        char *p1 = ptrn + pLen - 2, *p2,*p3;  
          
        //該do...while循環完成以當前pptr所指的字符爲邊界時,要移動的距離  
        do{  
            while(p1 >= ptrn && *p1-- != c);//該空循環,尋找與最後一個字符c匹配的字符所指向的位置  
              
            p2 = ptrn + pLen - 2;  
            p3 = p1;  
              
            while(p3 >= ptrn && *p3-- == *p2-- && p2 >= pptr);//該空循環,判斷在邊界內字符匹配到了什麼位置  
  
        }while(p3 >= ptrn && 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 *skip, int *shift)  
{  
    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 = skip[(unsigned char)buf[b_idx]];//根據壞字符規則計算跳躍的距離  
        shift_stride = shift[p_idx];//根據好後綴規則計算跳躍的距離  
        b_idx += (skip_stride > shift_stride) ? skip_stride : shift_stride;//取大者  
    }  
    return 0;  
} 


發佈了4 篇原創文章 · 獲贊 7 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章