关键词匹配的问题在防垃圾等安全项目中普遍存在,一般有一组数量较大的关键词列表,对某一输入串进行检定,以判定该串中是否含有列表中的任一关键词。在一些实时性很强的情况,如即时消息的传递中,对效率有较高的要求。

在多关键词的匹配算法中,常用的有Aho-Corasick算法、Wu-Manber算法等,在关键词的长度较小的情况下,Aho-Corasick算法能得到比较稳定的复杂度。本文对Aho-Corasick算法作了一定的改进,在存储空间和运行效率之间得到一个比较好的平衡。

1. 匹配树:

我们先按以下条件建立匹配树:

1) 存在一个根节点,不代表任何字符。树中的其它每个节点保存关键词中的一个字符,我们以字符值代指该节点

2) 若存在一个关键词,A是词中的一个字符,BA的后继字符,则称BA的子节点,所有关键词的第一个字符都是根节点的子节点。相同的值用同一个子节点表示。

3) 如果从根到A经过的所有节点组成一条关键词,则把关键词结束标志(0)也加入到A的子节点中,这个0节点称为叶子节点。

4) 在节点A中记录子节点个数n,对A的任一子节点B,节点值对n取模,所有模相同的子节相连组成一个链表

5) 所有的链表组成一个数组,A通过child指针指向该数组

6) 所有从根节点开始通过child指针到达某个结点的路径是唯一的,从根到任一叶子节点可以得到一条关键词。反之,每条关键词都在树中存在一条唯一的从根到叶子的路径。

7) 从根到某个节点A经过的节点相连得到一个字符串,设长度为m,则可以得到m-1个以A结尾的真子串,如果存在最长的真子串S,使S是某个关键词的起始部份,则在树中存在一条从根到达某个节点F的路径,代表该真子串,A通过next指针与节点F相连。F就是匹配到A failure后需要继续进行匹配的下一个节点。

2. 算法

2.1. 数据结构

typedef struct _node

{

char c;

int n;

struct _node** child;

struct node* sibling;

struct _node* next;

} NODE;

2.2. 构造匹配树

buildMatchTree()

for(列表中的每一条关键词s)

{

p=root;

for(i=0; i < strlen(s); i++)

{

if(p->child == NULL)

{

p->child = new (NODE *)[1];

p->child[0] = NULL;

}

for(ptr=p->child[0];ptr!=NULL;ptr=ptr->sibling)

if(ptr->c==s[i])

{

p = ptr;

break;

}

if(ptr == NULL)

{

ptr = new NODE(s[i]);

}

}

ptr = p->child[0];

p->child[0] = new NODE(0);

p->child[0]->sibling = ptr;

}

for(树中的每一个节点p,从根到此节点的串为s

{

for(i=1 to s的长度n)

{

取子串sub = s(i,n);

if(sub在树中匹配到,且匹配的最后一个节点为q)

p->next = q;

break;

}

}

for(树中的每个节点p)

{

for(num = 0, ptr = p->child[0]; ptr != NULL; ptr = ptr->sibling, num++);

p->n = num;

if(num == 0) continue;

ptr = p->child[0];

p->child = new (NODE *)[num];

for(q=ptr; q != NULL; q = q->sibling)

{

idx = q->c % num;

t = p->child[idx];

p->child[idx] = new NODE(q->c);

p->child[idx]->sibling = t;

}

delete ptr链表;

}

2.3. 匹配串

bool matchStr(s)

p = root;

for(i=0; i < strlen(s); i++)

{

while(p->child != NULL)

{

if(p->child[0]->c == 0) return true;

for(ptr = p->child[s[i] % p->n]; ptr != NULL; ptr = ptr->sibling)

{

if(ptr->c==s[i])

{

p = ptr;

break;

}

}

if(ptr == NULL)

{

if(p->next != NULL)

{

p = p->next;

continue;

}

p = root;

}

break;

}

}

return false;

3. 复杂度估计

在构造匹配树时,设有K条关键词,关键词的最大长度为M,则在确定next节点时,需要最大的开销。

时间复杂度=O(KM^3)

空间复杂度=O(KM)

在对一长度为N的串进行匹配时,对串中的每个字符,需要比较子节点链表,子节点链表的值各不相同,所以长度是有限的;同时需要取next列表,由于next列表的长度是递减的,总共不会超过M个,所以

时间复杂度=O(MN)

从复杂度估计可以看到,相对而言,匹配树的构造需要较大的时间开销,但在关键词最大长度受限的情况下,两者的时间复杂度基本都是线性的。

另一个对效率造成影响的是子节点链表的长度,此处采用了一个简单的hash算法将子节点链表划分成若干个子链表。由于

所以链表的最大长度是16

4. 结论

通常的Aho-Corasick及其改进算法中,对子节点的处理或者用256个指针,或者用256BIT位来指明指定的值是否存在,但在空间上有较大的浪费。本算法用一个简单的hash算法,在空间和时间上的得到一个平衡,在实际的应用中取得较好的效果。