轉自: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;
}