多模式字符串匹配(转)
转自:http://stblog.baidu-tech.com/?p=418
1.hash
可以单字、双字、全字、首尾字hash。
优点:简单、通常有效
缺点:受最坏情况制约,空间消耗大,需要回朔。
2.Trie树
改进:进行穿线,参考KMP的算法,进行相同前缀匹配,建立跳转路径,避免回朔。
跳转路径建立的算法思想:
如果要建立节点 A -> A’ 的 跳转路径需要满足:
1)A = A’ 节点有相同的value值,代表同一个字
2)A的深度>A’的深度
3)对于A节点的父节点F,和A’节点的父节点(如果有父节点的话),有F->F’
优点:无回朔,查询效率一般较高
缺点:数据结构复杂难以维护,浪费空间多,建树时间长。
3.AC算法
本质上来说和Trie树一样。
转向函数:建立一个根据输入字符转变状态的有限自动机(建立状态机来避免建立Trie树的复杂数据结构)
失效函数:当出现状态无法根据输入字符继续走时,需要根据失效函数转化当前状态。失效函数的建立需要满足:节点r深度之前都已建立失效函数f。则若有g(r, a) = s,回朔r’=f( r )直至找到g(r’, a) 存在,则将f(s)=g(r’, a)。和Trie树是一致的。实际上,如果某状态节点r对输入字符a无路径,则可以将该节点的失效函数f( r )指向的状态节点r’的g(r’, a)作为g(r, a)。这样在搜索中就不需要专门考虑失效节点的问题了,只需要沿着转向函数一直走。
输出函数:某状态代表着匹配某模式的结束,因此输出函数的值就是匹配成功模式的集合。因为模式之间可能会有互包含,因此可能有多个成功匹配的模式。
AC算法比Trie树数据结构简单,因此运用广泛。用于snort等代码中。
4.KMP算法
参见:http://www.cnblogs.com/jslee/p/3458345.html
5.BM算法
BM算法是KMP之外的另一个单模式字符串匹配算法,其思想也很简单:
假设模式串是P主串是T, m=strlen(P),n=strlen(T)
1) 从左向右移动模式串
2) 对于模式串的匹配, 从右向左检查, 也就是P[m-1],p[m-2]…
3) 当发现不匹配时, 使用好后缀和/或坏字符来决定模式串移动的距离通常同时使用两个来加快查找速度
利用好后缀和坏字符可以大大加快模式串的移动距离,不是简单的++j,而是j+=max (shift(好后缀), shift(坏字符))
坏字符
先来看如何根据坏字符来移动模式串,shift(坏字符)分为两种情况:
- 坏字符没出现在模式串中,这时可以把模式串移动到坏字符的下一个字符,继续比较,如下图:
- 坏字符出现在模式串中,这时可以把模式串第一个出现的坏字符和母串的坏字符对齐,当然,这样可能造成模式串倒退移动,如下图:
为了用代码来描述上述的两种情况,设计一个数组bmBc['k'],表示坏字符‘k’在模式串中出现的位置距离模式串末尾的最大长度,那么当遇到坏字符的时候,模式串可以移动距离为: shift(坏字符) = bmBc[T[i]]-(m-1-i)。如下图://感觉有错,应该是bmBc[T[i+k]],k为P在T开始的位置。
数组bmBc(BM bad char)的创建非常简单,直接贴出代码如下:
int i;
for (i =0; i < ASIZE; ++i)
bmBc[i] = m;
for (i =0; i < m -1; ++i)
bmBc[x[i]] = m - i -1;
}
好后缀
再来看如何根据好后缀规则移动模式串,shift(好后缀)分为三种情况:
- 模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠左边的子串对齐。
- 模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。
- 模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。
为了实现好后缀规则,需要定义一个数组suffix[],其中suffix[i] = s 表示以i为边界,与模式串后缀匹配的最大长度,如下图所示,用公式可以描述:满足P[i-s, i] == P[m-s, m]的最大长度s。
构建suffix数组的代码如下:
for (i=m-2;i>=0;--i){
q=i;
while(q>=0&&P[q]==P[m-1-i+q])
--q;
suffix[i]=i-q;
}
有了suffix数组,就可以定义bmGs[]数组,bmGs[i] 表示遇到好后缀时,模式串应该移动的距离,其中i表示好后缀前面一个字符的位置(也就是坏字符的位置),构建bmGs(BM good suffix)数组分为三种情况,分别对应上述的移动模式串的三种情况
- 模式串中有子串匹配上好后缀
- 模式串中没有子串匹配上好后缀,但找到一个最大前缀
- 模式串中没有子串匹配上好后缀,但找不到一个最大前缀
构建bmGs数组的代码如下:
int i, j, suff[XSIZE];
suffixes(x, m, suff);
for (i =0; i < m; ++i) //空白空格任选,所以所有都初始化成最大,没有匹配的情况
bmGs[i] = m;
j =0;
for (i = m -1; i >=0; --i)
if (suff[i] == i +1) //当suff的值为i+1,找到一个最大前缀的情况
for (; j < m -1- i; ++j) //上图的空白空格任选,从i到m-1-i-1,都是可以赋此值的
if (bmGs[j] == m) //因为上述i从大到小,所以保证赋的值是相对好的,所以j赋过,就++,不用从i起。
bmGs[j] = m -1- i;
for (i =0; i <= m -2; ++i) //模式串中有子串匹配上好后缀的情况
bmGs[m -1- suff[i]] = m -1- i;
}
再来重写一遍BM算法:
while (j <= strlen(T) - strlen(P)) {
for (i = strlen(P) -1; i >=0&& P[i] ==T[i + j]; --i)
if (i <0)
match;
else
j += max(bmGs[i], bmBc[T[i]]-(m-1-i));
}
BM算法的最坏时间复杂度为O(m*n),最好是O(n/m),但实际比较次数只有文本串长度的20%~30%。可以看作是亚线性的时间复杂度算法。
6.WM算法(多模式)
WM算法采用字符块技术,增大了主串和模式串不匹配的可能性,从而增加了直接跳跃的机会。使用散列表选择模式串集合中的一个子集与当前文本进行完全匹配。使用前缀表进一步过滤不匹配的模式串,使算法获得了较高的运行效率。
WM算法的思想从BM算法思想演变而来,但是用于多模匹配中。WM算法也是从右到左进行匹配。WM算法有一个重要假设,假设所有的模式的字符串长度是一样的,为m。若不一样,则按最短的那个模式长度在做匹配时截断其他的模式。
WM算法首先对模式串集合进行预处理。预处理阶段将建立3个表格:SHIFT表,HASH表和PREFIX表。SHIFT表用于在扫描文本串的时候,根据读入字符串决定可以跳过的字符数,如果相应的跳跃值为0,则说明可能产生匹配。HASH表用来存储尾块字符散列值相同的模式串。PREFIX表用于存储尾块字符散列值相同的模式串的首块字符散列值。
移动表的建立
移动表和正常的BM算法的移动表(SHIFT TABLE)起相同的作用,除此之外它还决定最后B个字符的移动而不是仅仅一个字符。
哈希表的建立
文本中当前的子串和模式中的一些模式匹配。但是是哪个模式呢?为了避免和每个模式的子串都进行比较,我们使用哈希的技术来最小化需要比较的数量。
前缀表的建立
例如,后缀“ion”或者“ing”是英文中非常常见的。这些后缀不会经常在文本中出现,但是它们很可能在一些文本中出现。它们会引起哈希表的冲突;也就是,所有的有相同后缀的模式在哈希表中有相同的入口。当我们遇到文本中这样一个后缀时,我们发现SHIFT值为0(假设它是一些模式的后缀),我们不得不单独检查所有带有这个后缀的模式看它是否和文本匹配。为了加速这个过程,我们引进另一张表,称为前缀表。除了匹配所有模式的后B个字符,我们也匹配前缀表中的所有模式的头B’个字符。
过程
假设模式串集合P中最短的模式长度为m,那么,后续仅考虑所有模式的前m个字符组成的模式串。
设X=x1…xB为T中的待比较的长度为B的子串,通过hash函数映像得到一个索引值index,以该索引值作为偏移得到SHIFT表中的值,该值决定读到当前子串x后可以跳过的位数。假设X映射到SHIFT表的入口为index的表项,即index=hash(x)。
SHIFT表中值的计算原则为:
(1) 如果X不出现在模式串中,则SHIFT[h]=m-B+1。
(2) 如果X出现在某些模式串中,而且在所有的模式串中的最右出现位置为q,则SHIFT[h]=m-q。
算法匹配的大致原理:
(1) 设当前比较的文本串X的hash值为h。如果SHIFT[h]=0,说明可能产生了匹配,那么需要进一步的判断。
(2) 用该h值作为索引,查HASH表找到HASH[h],它存储的是指标,指向两个单独的表:一个是模式链表,另一个是PREFIX表。模式链表中存放的是后B个字符的hash值同为h的所有模式。
(3) 对于待比较长度为m的串,如果其长度为B的前缀与模式的前缀的hash值也相同,则再将相应的文本串与符合的模式逐一进行比较,最终判定是否完全匹配。
算法匹配的主要过程:
基于后缀的模式匹配,每次扫描B个字符T[m-B+1]…..T[m]。
(1) 扫描text的末B位T[m-B+1]…..T[m]通过hash function计算其哈希值h。
(2) 查表,SHIFT[h]>0,text小指针后滑SHIFT[h]位,执行(1);SHIFT[h]=0,执行那个(3)。
(3) 计算此m位text的前缀的哈希值,记为text_prefix。
(4) 对于每个p(HASH[h]≦p<HASH[h+1]),看是否PREFIX[p]=text_prefix。
(5) 如果相等,让真正的模式串去和text匹配。
实践证明,大部分时间SHIFT都不为0,(在一个典型的例子中,对于100个模式5%的时间移动值为0,1000个模式27%的时间移动值为0,5000个模式53%的时间。),也就代表匹配串是跳跃着前进的,因此可以达到亚线性的时间复杂度。经过计算,复杂度为O(mp)+ O(BN/m),设N是文本的大小,P是模式的数量,m是每个模式的长度。
优点:快速,数据结构简单,实现容易。
缺点:需要所有模式长度基本相同(不能有太短的模式),不支持变长的编码,例如GB18030。