字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法一网打尽
本文内容框架:
§1 Boyer-Moore算法
§2 Horspool算法
§3 Sunday算法
§4 KMP算算法
§5 KR算法
§6 AC自动机
§7 小结
§1 Boyer-Moore(BM)算法
Boyer-Moore算法原理
Boyer-Moore算法是一种基于后缀匹配的模式串匹配算法,后缀匹配就是模式串从右到左开始比较,但模式串的移动还是从左到右的。字符串匹配的关键就是模式串的如何移动才是最高效的,Boyer-Moore为了做到这点定义了两个规则:坏字符规则和好后缀规则,下面图解给出定义:
下面分别针对利用坏字符规则和好后缀规则移动模式串进行介绍:
坏字符规则
1.如果坏字符没有出现在模式字符中,则直接将模式串移动到坏字符的下一个字符:
(坏字符c,没有出现模式串P中,直接将P移动c的下一个位置)
2.如果坏字符出现在模式串中,则将模式串最靠近好后缀的坏字符(当然这个实现就有点繁琐)与母串的坏字符对齐:
(注:如果模式串P是babababab,则是将第二个b与母串的b对齐)
好后缀规则
好后缀规则分三种情况
1.模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠靠近好后缀的子串对齐。
2.模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。
其实,1和2都可以看成模式串还含有好后缀串(好后缀子串也是好后缀)。
3.模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。
Boyer-Moore算法步骤
1.对模式子串进行预处理
Boyer-Moore算法实现必须对模式串进行预处理,得到坏字符规则和好后缀规则移动的映射表,下面代码中MakeSkip是建立坏字符规则移动的映射表,MakeShift是建立好后缀规则的移动映射表。
MakeSkip是构造数组skip[],skip[k]表示字符k距离模式串末尾的距离。
MakeShfit是构造数组shfit[],shfit[k]表示模式串的以k为边界的后缀子串的最靠近的模式子串(或最前缀子串)到模式子串末尾的距离,例如:abcab,shfit[3]=3和shfit[2]=3(即都是第一个b到末尾的距离),k=2时,后缀子串为cab,这时只有最长前缀ab,shfit[2]=3。
2.从b_idx开始查找,得到坏字符和好后缀,得到最大移动距离,移动b_idx,直至b_idx到达母串的末尾。
Boyer-Moore算法实现
╔
/* 函数: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,没有在模式串出现的字符距离为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; }
╝②
算法的时间复杂度最差(匹配不上)是O(n×m),最好是O(n),其中n为母串的长度,m为模式串的长度。BM算法时间复杂度最好是O(n/(m+1))
§2 Horspool算法
horspool算法将主串中匹配窗口的最后一个字符跟模式串中的最后一个字符比较。如果相等,继续从后向前对主串和模式串进行比较,直到完全相等或者在某个字符处不匹配为止(如下图中的α与σ失配) 。如果不匹配,则根据主串匹配窗口中的最后一个字符β在模式串中的下一个出现位置将窗口向右移动。
Horspool算法相对于Boyer-Moore算法改进了坏字符规则,Boyer-Moore算法只是将模式串P中从当前未匹配位置向右第一个坏字符与母串的坏字符(未匹配的字符)对齐进行再次匹配,Horspool算法是以当前匹配窗口中母串的最末尾的一个字符和模式串最靠近它的字符对齐,下图中β是当前匹配窗口的母串最后一个字符,将其与模式串左边最靠近的β对齐移动。
Horspool算法预处理
为了实现模式串的移动,必须先记录每一个字符串在模式串中距离最右边的距离:
Horspool算法实现
╔
/* * implementation of Horspool * Author:Horspool * Coder: Cobbliu */ #define WORD 26 int horspool(char *T, int lenT, char *P, int lenP) { int d[WORD]; int i, pos, j; for(i = 0; i != WORD; i++) d[i] = lenP; for(i = 0; i != (lenP-1); i++) d[P[i]-'A'] = lenP-i-1; pos = 0; while(pos < (lenT-lenP)){ j = lenP-1; while(j >= 0 && T[pos+j]==P[j]) //matching j--; if(j == -1) return pos; else //not matched pos += d[T[pos+lenP-1]-'A']; } return -1; }
Horspool算法时间复杂度
假设主串的长度为n,模式串的长度为m,那么Horspool算法最坏情况下的时间复杂度是O(mn),但平均情况下它的时间复杂度是O(n)。
╝④
§3 Sunday算法
Sunday算法思想跟BM算法很相似,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。如果该字符没有在匹配串中出现则直接跳过,即移动步长= 匹配串长度+1;否则,同BM算法一样其移动步长=匹配串中最右端的该字符到末尾的距离+1。
Sunday算法实现(不废话直接上代码)
╔
#include <iostream> #include <cstring> using namespace std; int sunday(const char* src, const char* des) { int len_s = strlen(src); int len_d = strlen(des); int next[26] = {0}; for (int j = 0; j < 26; ++j) next[j] = len_d + 1; for (int j = 0; j < len_d; ++j) next[des[j] - 'a'] = len_d - j; //记录字符到最右段的最短距离+1 //例如:des = "abcedfb" //next = {7 1 5 4 3 2 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8} int pos = 0; while (pos < (len_s - len_d + 1)) //末端对齐 { int i = pos; int j; for (j = 0; j < len_d; ++j, ++i) { if (src[i] != des[j]) { pos += next[src[pos + len_d] - 'a']; //不等于就跳跃,跳跃是核心 break; } } if ( j == len_d ) return pos; } return -1; } int main() { char src[]="abcdacdaahfacabcdabcdeaa"; char des[]="abcde"; cout<<sunday(src,des)<<endl; return 0; }
╝⑤
Boyer-Moore、Horspool、Sunday算法小结
Boyer-Moore、Horspool、Sunday算法都是基于后缀数组的匹配算法,区别在于移动的方式不一样(好像网上有些都没有说的Boyer-Moore算法的好后缀规则,有可能是优化方法吧,没有去深究,抱歉)。下面给出三种方法的对比:
╔
|
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(a) Boyer-Moore | (b) Horspool | (c) Sunday | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
In this example, t0, ..., t4 = a b c a b is the current text window that is compared with the pattern. Its suffix a b has matched, but the comparison c-a causes a mismatch. The bad-character heuristics of the Boyer-Moore algorithm (a) uses the "bad" text character c to determine the shift distance. The Horspool algorithm (b) uses the rightmost character b of the current text window. The Sunday algorithm (c) uses the character directly right of the text window, namely d in this example. Since d does not occur in the pattern at all, the pattern can be shifted past this position.
╝⑥
§4 Knuth-Morris-Pratt(KMP)算法
KMP算法是一种高效的前缀匹配算法,在传统蛮力(BF)匹配算法的基础上改进的地方在于每次移动的距离不是1可以是更大,没有进行回溯,BF算法的时间复杂度是O(m*n),而KMP算法的时间复杂度是O(m+n)。
假设执行第i+1趟匹配时,如果比较模式串P中的第j个字符时不匹配,也就是有
T[i,i+1,...,i+j-1]=P[0,1,...,j-1],T[i+j]≠P[j] (打不了下标,就有数组的形式给出字符串) (1)
BF算法下一趟是从目标的第i+1位置开始与模式串比较。如果匹配成功则有
T[i+1,i+2,...,i+m]=P[0,1,...m-1] (2)
如果模式串P有如下特征
P[0,1,...j-2]=P[1,2,...j-1] (3)
由(1)可知
T[i+1,i+2,...,i+j+1]=P[1,2,...j-1] (4)
由(3)(4)可知
T[i+1,i+2,...,i+j+1]≠P[0,1,...j-2] (5)
故由
T[i+1,i+2,....,i+m]≠P[0,1,...m-1]
所以第i+2趟是匹配可以不需要进行,因为一定不能匹配。
类似可以推得
P[0,1,...k-1]=P[j-k-1,j-k,...j-1]
这时才有
P[0,1,...k-1]=P[j-k-1,j-k,...j-1]=T[i+j-k,i+j-k+1,i+j-1]
模式串P从当前位置直接向右移动 j-k 位置,使模式串P的第 k 个字符P[k]与目标串T中的第i+j个字符对齐开始比较(前面 k 个已经匹配)。
造成BF算法效率低的主要原因是在算法执行过程中有回溯,而这些回溯是可以避免的。KMP算法的关键是在匹配失败时,确定下一次匹配的位置,设next[j]=k,表示当模式串P中第j个字符与母串T相应字符不匹配时,模式串P中应当由第K个字符与目标串中刚不匹配的字符对齐继续进行比较。
例如,模式串P="abaabcac",其对应的next[j]如下:
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
t[i] |
a |
b |
d |
a |
b |
c |
d |
e |
next[i] |
-1 |
0 |
0 |
0 |
1 |
2 |
0 |
0 |
next数组构造
╔ -1, j=0;
next[j]= ║max{k| 0<k<j 且 P[0,1,...,k-1]=P[j-k,j-k+1,..j-1}
╚ 0, 其他情况
next数组求解是一个递推过程,
设next[j]=k,则有
P[0,1,...k-1]=P[j-k,j-k+1,...,j-1]
next[j]= ╔ max{k| 0<k<j 且 P[0,1,...,k]=P[j-k,j-k+1,..j-1}
╚ 0, 其他情况
如果P[k]=P[j],有 next[j+1]=next[j]+1=k+1。
如果P[k]≠P[j],有 P[0,1,...,k]≠P[j-k,j-k+1,...j],
假设next[j+1]=h+1,则有下式成立
P[0,1,...h]=P[j-h+1,j-k+1,...j] P[h]=P[j]
又因为
P[0,1,...h-1]=P[j-h,j-k+1,...j-1]=P[k-h,k-h+1,k-1] (next[k]=h的情况)
即此时实际只需要满足 next[k]=h(前面已经求解过)时,P[h]=P[j] 就有next[j+1]=h+1,否则(不存在这样的h)next[j+1]等于0。
由此可以得到计算next的递推公式
KMP算法实现
╔
/* ******************************************************************* created: 2006/07/02 filename: KMP.cpp author: 李创 http://www.cppblog.com/converse/ 参考资料: 严蔚敏<<数据结构>> purpose: KMP字符串匹配算法的演示 ******************************************************************** */ #include < stdio.h > #include < stdlib.h > #include < assert.h > #include < string .h > #define MAX_LEN_OF_STR 30 // 字符串的最大长度 typedef struct String // 这里需要的字符串数组,存放字符串及其长度 { char str[MAX_LEN_OF_STR]; // 字符数组 int length; // 字符串的实际长度 } String, * PString; // 得到字符串的next数组 void GetNextArray(PString pstr, int next[]) { assert(NULL != pstr); assert(NULL != next); assert(pstr -> length > 0 ); // 第一个字符的next值是-1,因为C中的数组是从0开始的 next[ 0 ] = - 1 ; for ( int i = 0 , j = - 1 ; i < pstr -> length - 1 ; ) { // i是主串的游标,j是模式串的游标 // 这里的主串和模式串都是同一个字符串 if ( - 1 == j || // 如果模式串游标已经回退到第一个字符 pstr -> str[i] == pstr -> str[j]) // 如果匹配成功 { // 两个游标都向前走一步 ++ i; ++ j; // 存放当前的next值为此时模式串的游标值 next[i] = j; } else // 匹配不成功j就回退到上一个next值 { j = next[j]; } } } // KMP字符串模式匹配算法 // 输入: S是主串,T是模式串,pos是S中的起始位置 // 输出: 如果匹配成功返回起始位置,否则返回-1 int KMP(PString S, PString T, int pos) { assert(NULL != S); assert(NULL != T); assert(pos >= 0 ); assert(pos < S -> length); if (S -> length < T -> length) return - 1 ; printf( " 主串\t = %s\n " , S -> str); printf( " 模式串\t = %s\n " , T -> str); int * next = ( int * )malloc(T -> length * sizeof ( int )); // 得到模式串的next数组 GetNextArray(T, next); int i, j; for (i = pos, j = 0 ; i < S -> length && j < T -> length; ) { // i是主串游标,j是模式串游标 if ( - 1 == j || // 模式串游标已经回退到第一个位置 S -> str[i] == T -> str[j]) // 当前字符匹配成功 { // 满足以上两种情况时两个游标都要向前进一步 ++ i; ++ j; } else // 匹配不成功,模式串游标回退到当前字符的next值 { j = next[j]; } } free(next); if (j >= T -> length) { // 匹配成功 return i - T -> length; } else { // 匹配不成功 return - 1 ; } }
╝③
§5 Karp-Rabin(KR)算法
Karp-Rabin算法是利用hash函数的特性进行字符串匹配的。 KR算法对模式串和循环中每一次要匹配的子串按一定的hash函数求值,如果hash值相同,才进一步比较这两个串是否真正相等。
Karp-Rabin算法适用于多个字符串匹配较好。
§6 Aho-Corasick算法
Aho-Corasick算法又叫AC自动机算法,是一种多模式匹配算法。Aho-Corasick算法可以在目标串查找多个模式串,出现次数以及出现的位置。
Aho-Corasick算法原理
Aho-Corasick算法主要是应用有限自动机的状态转移来模拟字符的比较,下面对有限状态机做几点说明:
上图是由多模式串{he,she,his,hers}构成的一个有限状态机:
1.该状态当字符匹配是按实线标注的状态进行转换,当所有实线路径都不满足(即下一个字符都不匹配时)按虚线状态进行转换。
2.对ushers匹配过程如下图所示:
当转移到红色结点时表示已经匹配并且获得模式串
Aho-Corasick算法步骤
Aho-Corasick算法和前面的算法一样都要对模式串进行预处理,预处理主要包括字典树Tire的构造,构建状态转移表(goto),失效函数(failure function),输出表(Output)。
Aho-Corasick算法包括以下3个步骤
1.构建字典树Tire
2.构建状态转移表,失效函数(failure function),输出表(Output)
3.搜索路径(进行匹配)
下面3个步骤分别进行介绍
构建字典树Tire
Tire是哈希树的变种,Tire树的边是模式串的字符,结点就是Tire的状态表,下图是多模式串{he,she,his,hers}的Tire树结构:
构建goto函数、failure function和Output函数
goto函数(状态转移函数):goto(pre,v)=next,完成这样的任务:在当前状态pre,输入字符v,得到下一个状态next,如果没有下个状态则next=failure。
failure function:失效函数是处理当前状态是failure时的处理。
output函数:当完成匹配是根据状态输出匹配的模式串。
下面是多模式串{he,she,his,hers}的goto函数,failure函数,output函数
goto函数:
output函数
多模式串{he,she,his,hers}最终的有限状态机图
Aho-Corasick算法实现
//////////////////////////////////////////////////// /* 程序说明:多模式串匹配的AC自动机算法 自动机算法可以参考《柔性字符串匹配》里的相应章节,讲的很清楚 */ #include <stdio.h> #include <string.h> const int MAXQ = 500000+10; const int MAXN = 1000000+10; const int MAXK = 26; //自动机里字符集的大小 struct TrieNode { TrieNode* fail; TrieNode* next[MAXK]; bool danger; //该节点是否为某模式串的终结点 int cnt; //以该节点为终结点的模式串个数 TrieNode() { fail = NULL; memset(next, NULL, sizeof(next)); danger = false; cnt = 0; } }*que[MAXQ], *root; //文本字符串 char msg[MAXN]; int N; void TrieInsert(char *s) { int i = 0; TrieNode *ptr = root; while(s[i]) { int idx = s[i]-'a'; if(ptr->next[idx] == NULL) ptr->next[idx] = new TrieNode(); ptr = ptr->next[idx]; i++; } ptr->danger = true; ptr->cnt++; } void Init() { int i; char s[100]; root = new TrieNode(); scanf("%d", &N); for(i = 0; i < N; i++) { scanf("%s", s); TrieInsert(s); } } void Build_AC_Automation() { int rear = 1, front = 0, i; que[0] = root; root->fail = NULL; while(rear != front) { TrieNode *cur = que[front++]; for(i = 0; i < 26; i++) if(cur->next[i] != NULL) { if(cur == root) cur->next[i]->fail = root; else { TrieNode *ptr = cur->fail; while(ptr != NULL) { if(ptr->next[i] != NULL) { cur->next[i]->fail = ptr->next[i]; if(ptr->next[i]->danger == true) cur->next[i]->danger = true; break; } ptr = ptr->fail; } if(ptr == NULL) cur->next[i]->fail = root; } que[rear++] = cur->next[i]; } } } int AC_Search() { int i = 0, ans = 0; TrieNode *ptr = root; while(msg[i]) { int idx = msg[i]-'a'; while(ptr->next[idx] == NULL && ptr != root) ptr = ptr->fail; ptr = ptr->next[idx]; if(ptr == NULL) ptr = root; TrieNode *tmp = ptr; while(tmp != NULL && tmp->cnt != -1) { ans += tmp->cnt; tmp->cnt = -1; tmp = tmp->fail; } i++; } return ans; } int main() { int T; scanf("%d", &T); while(T--) { Init(); Build_AC_Automation(); //文本 scanf("%s", msg); printf("%d\n", AC_Search()); } return 0; }
§7 小结
这篇文章把字符串匹配的六个算法——BM、Horspool、Sunday、KMP、KR、AC算法,从原理到步骤,再从流程到实现都做了讲解了,能有了一定的认识和理解,基本可以掌握这些算法。如果你有任何建议或者批评和补充,请留言指出,不胜感激,更多参考请移步互联网。
参考(后面3个链接很不错的哟):
②ouyangjia7:http://ouyangjia7.iteye.com/blog/353137
③
那谁的技术博客:http://www.cppblog.com/converse/archive/2006/07/05/9447.html
④CobbLiu:http://www.cnblogs.com/cobbliu/archive/2012/05/29/2524244.html
⑤kmplayer:http://kmplayer.iteye.com/blog/704187
⑥志文工作室:http://www.zhiwenweb.cn/Category/Security/1274.htm
⑦http://www.iti.fh-flensburg.de/lang/algorithmen/pattern/sundayen.htm
⑧http://www-igm.univ-mlv.fr/~lecroq/string/
⑨http://www.csie.ntnu.edu.tw/~u91029/StringMatching.html