Wu-Manber 经典多模式匹配算法

本文介绍了一种基于Boyer-Moore思想的多模式匹配算法——Wu-Manber算法,包括SHIFT、HASH、PREFIX三表的构建及算法流程,并提出了两种改进方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多模式匹配的用法,多了去了! DB 中对 selected patterns 进行数挖;安全中对 suspicious keyword 进行匹配;各种日期形式 2009-5-20 2009 5 20 May,20 的搜索; DNA 配对;各种 replace 功能;等等,太口水了枚举这个。

       Wu-Manber 基于 BM 算法思想,如果您佬 BM 还没 OK ,请参照我的 BM 日志 搞搞清楚先。

       提到 Wu-Manber ,其实就是 SHIFT HASH PREFIX 三张表,预处理 patterns 先把这三张表填好,搜的过程忒简单。

 

1.   拿表说事儿:表干嘛用的

先有个大概印象: SHIFT[] 就一跳转表,一张记录向右滑动距离的表。当 SHIFT[i]=0 时,说明那么多 patterns 肯定有匹配上的,这时 HASH[] PREFIX[] 就要站出来指明谁暂时匹配上了,并对它们通通匹配一番。

 

2.   预处理这一堆 patterns :填上三表

这堆 patterns 长度不一, m= 最小长度,该算法也就只检测每 pat m 个字符了(那剩余的怎么办呢? o 还没想明白 )。 Patterns 长度基本相当最好,假如有一 pat 掉链子,特短(长度为 2 ),那么每次滑动距离最多也就才 2 ,还滑个什么劲儿呀。所以各 pat 长度要统一要和谐。

我们把 text 切成长度为 B 的块(一般 2 3 ), B 怎么算呢?假如有 k pat ,那么各 pat 总长 M=k*m |∑| c ,那么 B=log c (2M) 。既然这样,那 SHIFT 滑动距离就不由末尾单个字符而由末尾 B 个字符而定了。如果这 B 个字符跟所有 pat 都没匹配上,那么很自然小指针可以后滑 m-B+1 。为什么是 m-B+1 呢?

 

 

如上图示,虽然 XAB 不出现在 pat 末,但它的后缀 AB pat 前缀,如果你索性移 m=5 ,那么就错过一个匹配了。其实如果 B 块不出现在 pat 末,那么至多它的 B-1 后缀可能出现在 pat 前缀,因此,安全的移动距离为 m-(B-1)=m-B+1 。这其实是相对保守的策略,最后我们会提出改进算法。

 

2.1   SHIFT[]

SHIFT[] 表大小为 |∑| B ,因为要组建长度为 B str ,其中每个字符均有 |∑| 种选择。通过 hash function ,每个 str 计算得到一个值 index 而住进 SHIFT 。我们现在扫描 text X=x1…x B , hashFunc(X)=h ,那应该滑动多少呢?

如果 X 跟各 pat 都没匹配上,那么 SHIFT[h]=m-B+1 。如果 X 跟其中某些 pat 匹配上,那么 SHIFT[h]=m-q ,其中 q X 出现在最右的 pat 中(假如 pat j )的 x B 位置,显然它是最小滑动距离。其实,不用等到实际考查 text 时再计算 SHIFT ,我们预处理各 pat 就可以把 SHIFT 表填上。对于每个 pat ,计算其每个长度为 B subpat(a j-B+1 …a j ) 的值, SHIFT[hashFun(subpat)]=min{m-B+1,m-j}

哈哈,于是乎 SHIFT[] 里记录了 text 能安全滑过的最大距离,滑的越大当然越快,但是滑的小一点倒也没错。根据这一点,我们可以压缩 SHIFT 表,把不同的 subpat 压缩进一个 entry ,只要值留最小的那个就 OK 了。(在 agrep 这个 Wu-Manber 算法的应用中, B=2 时用的原始 SHIFT 表, B=3 时用的压缩 SHIFT 表)。

 

2.2   HASH[]

只要 SHIFT[]>0 ,那尽管滑 text 就好了( 100 pat 的应用中, 5% 遇到 0 1000 个时候 27% 10000 个时候 53% )。如果 =0 ,总不至于去跟所有 pat 一一比对看谁暂时匹配上了吧? o 们文明人要用更懒更聪明的方式,去快速锁定那些暂时匹配上的 pat 们。怎么做呢? HASH

HASH[i] 存放一个指向链表的指针,链表存着这样的 pat (末 B 位通过 hash function 计算是 i )。 HASH[] 表大小同 SHIFT ,但相对就稀疏多了,人那儿存着所有可能的组合的 SHIFT 值。哎,牺牲空间换时间,时空一向两难全。

记现正扫描的 text 的末 B 位的哈希值为 h 。同时引入 PAT_POINT (指向实际 patterns 存储位置的指针链表),该链表按 patterns B 位的哈希值排序,那么 HASH[h] 的值是 p p 指向 PAT_POINT 中哈希值为 h 的第一个结点处),此时相当于找到第一个跟 text B 位匹配上的 pat ,那么进行匹配,如果匹配不上就继续 p++ ,一直到 HASH[h+1] 指向的那处地址截止。以上是 SHIFT[h]=0 的情况。对于 SHIFT[h]≠0, 那么 HASH[h]=HASH[h+1] ,因为没哪个 pat 的末 B 位能匹配上,自然这两个 HASH 值应该相等(开始就是结束)。这样,就填好整张 HASH[] 表了。

 

2.3  PREFIX[]

       如果只有 HASH[] 表,就囧大了。例如自然语言 text 中以 ing ion 结尾的单词非常多, pat 中出现 ing/ion 结尾的也非常多,如果按 HASH[] 的方法,那就得 HASH[h]~HASH[h+1] pat 一个个匹配。能不能更快些呢?引入 PREFIX[]

       对每一个 pat ,除了记录其末 B 位字符的哈希值( PAT_POINT ),我们还要记录其首 B’ 位字符的哈希值( PREFIX ),一般取 B’=2 。这是一种有效的过滤手段,因为既末 B 位相同前 B’ 位也相同的 pat 很少,这样就没那么多 pat 需要去匹配了(不像上面那种要一一匹对)。但是也需要权衡啦,因为你计算 PREFIX 哈希值需要时间,存储它需要空间,换回 一一匹配 的时间,不知划不划算。

 

3.  匹配过程

Step1. 现正扫描的 text 的末 B t m-B+1 …t m 通过 hash function 计算其哈希值 h

Step2. 查表, SHIFT[h]>0 text 小指针后滑 SHIFT[h] 位,执行 1 SHIFT[h]=0 ,执行 3

Step3. 计算此 m text 的前缀的哈希值,记为 text_prefix

Step4. 对于每个 p HASH[h] p<HASH[h+1] )看是否 PREFIX[p]=text_prefix 。如果相等,方才让真正的 pat (即 PAT_POINT[p] )去和 text 匹配。

 

 

计算 SHIFT[],HASH[],PREFIX[];

// 开始匹配

while (text<textend)

{

         hashVal=hashBlock(text);// 计算当前块的哈希值

         // 查找块的坏字符移动表( SHIFT )得到下一个匹配开始位置

         shift_distance=SHIFT[hashval]; //

         if (shift_distance==0) // 当前块出现在某 pat

         {

                   shift_distance=1; //

                   p=HASH[hashval];

                   // 得到可能与当前块匹配的所有 pat 的集合的开始位置

                   while (p)

                            检验子集中的 pat 是否匹配 ;

         }

         text+=shift_distance; // 选择下一个可能的匹配入口

}

 

这里用 C 给出 main() ,源码( glimpse 代码的一部分)可以从 FTP 下载: cs.arizona.edu

 

 

 

复杂度 O BN/M ), B M 含义同前, N text 字符数。 Patterns 很短或很少的时候, Wu-Manber 不是很牛叉。而成千上万的 patterns 一起匹配过去, Wu-Manber 牛气冲天了。

 

3.  Wu-Manber 总结与改进

       一言以闭之: WM BM 处理多模式的派生形式。用的 BM 算法框架,用块字符来计算的坏字符移动距离( SHIFT[] );在进行匹配的时候,用的 HASH[] 选择 patterns 中的一个子集与当前文本进行匹配,减少无谓的匹配操作。 WM 不会随着 patterns 的增加而成比例增长,它远少于使用每一个 pat BM 算法对文本进行匹配的时间总和。

 

改进 1

       但是,上文提到过安全的移动距离最大是 m-B+1 ,这是相对保守的策略。其实像图 1 那种情况的出现次数相对于坏字符情况的出现次数,那真是小乌见大乌了。所以,一次只滑 m-B+1 多慢呀,能滑 m 才够快够刺激。呵呵,怎么办呢?想想 BM 中怎么做地。我们将 SHIFT 作为坏字符转移表,这样算:

 

 

 

这样,坏字符转移函数的值域 0<=y<=m ,而不是原来的 0<=y<=m-B+1 了。下表是 SHIFT 表的两个版本的实现。 SHIFT 表空间不变,时间上多了个 for 循环,总时间是 O(B*|∑|+M) |∑| 是块集中块的个数。

 

原始实现:

 

m-B+1 填写 SHIFT ;

for each pat

{

         for pat 中每一个块

                   计算 SHIFT[Bc];

}

 

 

 

改进的实现:

 

m 填写 SHIFT ;

for (i=1;i<B;i++)

{

         对所有 Bc [suffix(Bc,i)=prefix(pat,i)]

                   SHIFT[Bc]=m-i;

}

for each pat

{

         for pat 中每一个块

                   计算 SHIFT[Bc];

}

 

改进 2

       WM 算法一旦找准匹配入口点,就开始进行 逐个 的比较,能不能用 好后缀 呢?呵呵,可以。引入 GBSShift[] ,该表记录了每 pat 的末 B 位在所有 patterns 中的所有非后缀出现位置与相应 pat_end 的距离的最小值。这样就可以快速确定滑动距离。

 

 

 

       GBSShift[] SHIFT[] 大小相同, |∑| GBSShift[] SHIFT[] 表计算近似,不需额外计算工作,只需复制计算出的 SHIFT[] 表计算结果,所以它所需的额外时间是 O( |∑| ) 。将 WM 伪码中的 // 改为 shift_distance=GBSShift[hashval] 即可。

       结合改进 1 2 ,改进算法在处理大规模数据时比 WM 算法所用时间养活了 8~15%

 

盘点一下:

AC 算法被用于 fgrep1.0 (在 UNIX 中通过 -f 使用)

BM_AC 算法在 gre 中应用,并被 fgrep2.0 收用。

BMH_AC 算法

Wu-Manber 算法在 agrep 中应用,并被 glimpse 收用。

 

学习自:“A Fast Algorithm For Multi-Pattern Searching”和“一种改进的Wu-Manber多关键词匹配算法”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值