打赏

[Alg] 文本匹配-多模匹配-WM算法

转载:https://blog.csdn.net/joylnwang/article/details/6801720

1. 算法原理

提到多模式匹配算法,就得说一下Wu-Manber算法,其在多模式匹配领域相较于Aho-Corasick算法,就好象在单模式匹配算法中BM算法相较于KMP算法一样,在绝大多数场合,Wu-Manber算法的匹配效率要好于Aho-Corasick算法。这个算法是由吴升(台湾)和他的导师Udi Manber在九十年代提出。当然,要想充分理解WM算法如何加快多模式匹配的效率,还需要对BM算法的深刻了解。

在BM算法中引入的坏字符跳转概念,是BM算法能够在一般应用场景中,效率高于KMP算法的主要原因。WM算法在多模式匹配中,也引入了类似的概念,从而实现了模式匹配中的大幅度跳转。但是在单模式应用场景,很少有哪个模式串会包含所有可能的输入字符,而在多模式匹配场景,如果模式集合的规模较大的话,很可能会覆盖很大一部分输入字符,导致坏字符跳转没有用武之地。所以WM算法中使用的坏字符跳转,不是指一个字符,而是一个字符块,或者说几个连续的字符。通过字符快的引入,扩大了字符范围,使得实现坏字符跳转的可能性大大增加。

WM算法所要维护的数据结构

WM算法一般由三个表构成,SHIFT,HASH,PREFIX。

SHIFT表就相当于BM算法中的坏字符表,其构建过程有如下几点需要关注

我们对模式集合中所有模式的前m个字符构建SHIFT表,其中的m,是模式集合中最短模式的长度值。
对于字符块的长度B的选择,我们一般选择2,3个字节。
在构建SHIFT表的时候,对一个模式p的前m个字符,我们要处理其所有长度为B的子串,并填充对应的SHIFT值,假设字符块大小为B,当前字符快的尾字符与模式前缀的末尾距离为n,则SHIFT[p]=n。以模式abcdefgh为例,假设要处理其前6个字符构成的子串,那么SHIFT[ab]=4,SHIFT[bc]=3,SHIFT[cd]=2,SHIFT[de]=1,SHIFT[ef]=0都要加入SHIFT表中。
如果多个模式串前缀,或者同一模式前缀中,有相同的字符块,则保留其中SHIFT值的最小者。比如模式串p1 = abcab,p2=dcabe,其中对于块ab可以计算出三个SHIFT值3,1,0,这里我们需要保留SHIFT[ab]=0。
HASH表就是对应字符块B,所有SHIFT[B]=0的模式与B的映射关系。比如模式串abcde,bcbde,对于块de,他们的SHIFT值都是0,所以他们都由de索引。

实际上,在WM算法中,是可以没有PREFIX表的,但是对于字母文字来说,可能存在多个模式由一个字符块共同索引的情况,如上例,如果存在10个最末两个字符为de的模式串的话,那么在目标串中检索出de组合之后,要用当前的子串逐个尝试匹配这10个模式串,对于算法效率影响很大。所以WM算法同时截取了模式串的一个长度为2或者3的前缀,构建PREFIX表。在执行中缀查找的基础之上,再执行前缀查找,缩小备选模式集,提高匹配效率。如上例,abcde,bcbde,有共同的字符块de,使得SHIFT[de]=0,如果没有PREFIX表,就需要将游标向前移动5位,然后逐一尝试匹配这两个备选模式,如果有了PREFIX表,我们就可以用两个模式的前缀ab,bc再执行一次索引,一般情况下SHIFT值相同,PREFIX也相同的模式串比例很小,本例中二者的前缀是不同的,索引之后就只剩下一个备选模式,此时执行一次字符串比较即可判断当前位置是否发现了匹配模式。

WM算法所要维护的数据结构如下图所示:

  

WM算法执行模式匹配的流程

对于目标串target,游标i,模式前缀长度m,字符快长度B,前缀长度C。我们取target[i-B+1...i],查找其在SHIFT表中的对应值SHIFT[target[i-B+1...i]],如果找不到,则i+=m-B+1,如果其值为c(c != 0),那么我们i+=c,再执行上述操作。如果其SHIFT值等于0,我们需要取出target[i-m+1...i-m+C],然后在SHIFT[de]=0对应PREFIX结合中查找PREFIX[target[i-m+1...i-m+C]],如果不存在,则将游标i+=1。如果存在则用target[i-m+1]开始的子串,依次匹配满足条件的所有模式串,直到找到匹配模式,或者未发现匹配位置。然后i+=1,继续向后查找。

下面以WM算法对目标串target[1...10]=dcbacabcde,模式结合P{abcde,bcbde,abcabe},的匹配过程来形象的说明一下。首先,对于模式集合P预处理之后的结果如上面的程序结构图所示。然后从i=5开始执行算法。首先我们发现target[4...5] = ac,SHIFT表中不存在ac,所以i+4,到target[9],此时发现target[8...9]=cd,SHIFT[cd]=1,所以i+1,然后发现target[9...10]=0,我们取target[6...7]=ab,发现PREFIX[ab]对应的模式串是abcde,然后我们从target[6]开始用目标串与模式串比较,发现匹配模式abcde。

后继:

既然WM算法也是堪称经典的多模式匹配算法,就不能回避与AC算法的比较。

从算法的效率角度讲,按照Wu Sun,Manber两人的论文所列的数据,WM算法的性能要明显优于AC算法。
从模式集预处理的角度讲,两者的预处理过程都很复杂,但是AC算法由于要维护一个状态机,所以在构建的时间和空间复杂度上,还是要比WM算法更消耗资源。而且AC算法既然要维护自动机,就不得不考虑用什么样的算法来实现自动机的问题,所以自动机的实现方式也会影响AC算法的效果,而WM算法需要考虑的问题就要简单多,一个给力的字典结构足矣。
从模式集角度讲,如果模式集动态可变,AC算法动态调整自动机的成本可能要比WM算法高很多。
从程序适应性的角度讲,AC算法对于任何模式结合,任何目标串都是O(n)的时间复杂度,而WM算法,对于某些模式集和目标串可能会发生退化现象。

2. 代码实现 (Python)

#coding:utf-8
# suffix: val
shifts = dict()
# set of prefix
prefixs = set()
# prefix: [p_idx1, p_idx2, ...]
prefix_patterns_map = dict()
# suffix£º [prefix1, prefix2,...]
shift_prefix_map = dict()

patterns = ['abcde', 'bcbde', 'adcabe']
seq = 'dcbacabcde'

block_len = 2
pre_len = min([len(i) for i in patterns])

# create map structure
for p_idx in range(len(patterns)):
    # add prefix 
    # add prefix_patterns_map
    p = patterns[p_idx][:pre_len]
    prefix = p[:block_len]
    prefixs.add(prefix)
    if prefix in prefix_patterns_map:
        prefix_patterns_map[prefix].append(p_idx)
    else:
        prefix_patterns_map[prefix] = [p_idx]
    
    # add shifts
    for i in range(len(p) - block_len + 1):
        part = p[i: i + block_len]
        dis = pre_len - (i + block_len)  #µ±Ç°×Ö·û¿éµÄβ×Ö·ûÓëģʽǰ׺µÄĩβ¾àÀë
        if part in shifts:
            shifts[part] = dis if dis < shifts[part] else shifts[part]
        else:
            shifts[part] = dis
    
        # if shifts[part] = 0: add shift_prefix_map
        if shifts[part] == 0:
            if part not in shift_prefix_map:
                shift_prefix_map[part] = [prefix]
            else:
                shift_prefix_map[part].append(prefix)

# search
i = pre_len - block_len  # start index of seq
while i < len(seq):
    cur = seq[i: i + block_len]
    if cur not in shifts:
        i = i + pre_len - block_len + 1
        continue
    else:
        if shifts[cur] != 0:
            i += shifts[cur]
            continue
        else:
            prefix_list = shift_prefix_map[cur]
            for prefix in prefix_list:
                for str_idx in prefix_patterns_map[prefix]:
                    if seq[i + block_len - pre_len: i + block_len] == patterns[str_idx]:
                        print(patterns[str_idx])
                        break
            i += 1

                

print("shifts = ", shifts)
print("shift_prefix_map = ", shift_prefix_map)
print("prefixs = ", prefixs)
print("prefix_patterns_map = ", prefix_patterns_map)
print("patterns = ", patterns)

# output:
# abcde
# ('shifts = ', {'bd': 1, 'ab': 0, 'ad': 3, 'bc': 2, 'cb': 2, 'ca': 1, 'de': 0, 'dc': 2, 'cd': 1})
# ('shift_prefix_map = ', {'de': ['ab', 'bc'], 'ab': ['ad']})
# ('prefixs = ', set(['ab', 'ad', 'bc']))
# ('prefix_patterns_map = ', {'ab': [0], 'ad': [2], 'bc': [1]})
# ('patterns = ', ['abcde', 'bcbde', 'adcabe'])
    

 

posted @ 2020-04-02 23:26  listenviolet  阅读(1686)  评论(0编辑  收藏  举报