BM算法的个人理解

2019-07-14 11:53发布

抄来的BM算法思想 BM算法实际上包含两个并行的算法,坏字符算法和好后缀算法。 这两种算法的目的就是让模式串每次向右移动尽可能大的距离(j+=x,x尽可能的大)。 几个定义: 例主串和模式串如下: 主 串:   mahtavaatalomaisemaomalomailuun 模式串:  maisemaomaloma 好后缀:模式串中的aloma为“好后缀”。 坏字符:主串中的“t”为坏字符。   首先明确两个概念, 坏字符跳转BadSkip,好后缀移位GoodShift;(这个两个英文我觉得算比较好帮助理解的了) BM算法就是取坏字节跳转和好后缀移位中较大的进行偏移的…  
什么叫坏字符跳转呢? 先说明下,这个是一个空间换时间的简单算法. 建立一张包含所有目标字符串中字符的表,一般来说例如ASCII. 这样就有一个含有256个数据的数组了,其中特别的,数组的下标使用字符来标记的(0-255(字符的表示)). 这样是为了方便直接通过字符直接获取跳转量…特别的,当模式串中不存在此字符时,可以直接跳过剩下的所有字符的比较,Sunday算法就是基于这样的方式(但是它是匹配后面一个字符的) 上图:  1.bmp ~~~
2.bmp
  一般的理解,是各个字符相对最后一个字符的位置偏移位置. 如上图中P c的跳转对应的是c排除最后字符的b(其实可以看做0 )…从后往前数第一个c 的位置..(2) 特别的,b的跳转值应该为3而不是0.. a的跳转值为1而不是a… 公式如下: 3.bmp
实现代码可以自己写个了…    
下面介绍 好后缀移位 GoodShift. 这个是BM算法比较难的一点,但也是核心的部分. 好后缀移位用于从后往前匹配时已经部分匹配,但最近的一次不匹配的情况下的偏移量. 类似KMP算法.但是它是从后往前匹配的. (因为发现字符串整体是从前往后匹配,那么后面的字符必然会要取匹配,当发生前面匹配而后面不匹配的情况很多的时候,实为浪费)   模式字符串P[len], i(正方向计数 用下标表示的) 位置发生了不匹配,(当然,前面步骤中都是匹配的)  那么匹配的字符串为P[i+1 ~ len ],   那么往前(从i-1开始)寻找一个等于 P[ i+1~ len ]的子串 , (如果模式串没有匹配这个子串,那其实整个匹配的部分都可以跳过去的) (len-i-1)用来表示匹配的串长度 如果模式串中存在 一个P[ x-(len-i-1) ~ x ](x-m+i+1>0)(按照搜索方向得到的x值) 和 P[ i+1 ~ len ] 一致  那么检查P[ x-(len-i-1) ~ x ] 前面一个字符 P[x-(len-i-1)-1] P[i]是否相同.(这个和KMP的改进方式是一致的原理) 如果相同,则跳过此次子串,继续往前搜寻. 如果不同,则需要的偏移为 (总长度 减去 匹配的长度)i 再减去 (前面剩余的长度)   偏移量为 len – (len - i) – (x-(len-i-1))= len – x –1; 11.bmp 
第二种情况,如果没有找到这样一个子串,那么就用另外的一种方式:叫做 模式串前缀串中匹配串的最长后缀子串...看图好理解.   偏移量为 len – 2*x 222.bmp 
第三种情况,当然就是第二种情况也没发生后的结果…即整个串移过匹配串的位置.. 偏移量为 len 333.bmp

上代码: int* MakeShift(char* pStart,int pLen) { //为好后缀表申请pLen个int的空间 int *shift =(int*)malloc(pLen*sizeof(int)); int *pShiftCurrent = shift +pLen - 1;//方便给好后缀表进行赋值的指标 char *pCurrent = pStart + pLen - 1;//记录好后缀表边界位置的指标 char c; if(shift == NULL) { fprintf(stderr,"malloc failed!"); return 0; } c = *(pStart + pLen - 1);//保存模式串中最后一个字符,因为要反复用到它 *pShiftCurrent = 1;//以最后一个字符为边界时,确定移动1的距离 pCurrent--;//边界移动到倒数第二个字符(这句是我自己加上去的,因为我总觉得不加上去会有BUG,大家试试“abcdd”的情况,即末尾两位重复的情况) while(pShiftCurrent-- != shift)//该最外层循环完成给好后缀表中每一个单元进行赋值的工作 { char *p1 = pStart + pLen - 2, *p2,*p3; //该do...while循环完成以当前pCurrent所指的字符为边界时,要移动的距离 do{ while(p1 >= pStart && *p1-- != c){};//该空循环,寻找与最后一个字符c匹配的字符所指向的位置 //找到了这么一个与尾字符匹配的字符. p2 = pStart + pLen - 2; //p2 貌似指向已经匹配的子串的尾字符 p3 = p1; //p3指向找到的字符串的尾字符 //开始干活了...从后往前 连续比较... while( (p3 >= pStart && *p3-- == *p2--) && p2 >=pCurrent){};//该空循环,判断在边界内字符匹配到了什么位置 }while(p3 >= pStart && p2 >= pCurrent); //*pShiftCurrent *pShiftCurrent = shift + pLen - pShiftCurrent + p2 - p3;//保存好后缀表中,以pCurrent所在字符为边界时,要移动的位置 /* PS:在这里我要声明一句,*pShiftCurrent= (shift + pLen- pShiftCurrent) + p2 - p3; 大家看被我用括号括起来的部分,如果只需要计算字符串移动的距离,那么括号中的那部分是不需要的。 因为在字符串自左向右做匹配的时候,指标是一直向左移的,这里*pShiftCurrent保存的内容,实际是指标要移动距离,而不是字符串移动的距离。 我想SNORT是出于性能上的考虑,才这么做的。 */ pCurrent--;//边界继续向前移动 } return shift; }
未完待续...
后面介绍suff[]方法,求BmGc的过程...其实它本身的发展应该 类似 KMP中 next到nextval的过程吧...纯属猜测...