关键词匹配的问题在防垃圾等安全项目中普遍存在,一般有一组数量较大的关键词列表,对某一输入串进行检定,以判定该串中是否含有列表中的任一关键词。在一些实时性很强的情况,如即时消息的传递中,对效率有较高的要求。
在多关键词的匹配算法中,常用的有Aho-Corasick算法、Wu-Manber算法等,在关键词的长度较小的情况下,Aho-Corasick算法能得到比较稳定的复杂度。本文对Aho-Corasick算法作了一定的改进,在存储空间和运行效率之间得到一个比较好的平衡。
1. 匹配树:
我们先按以下条件建立匹配树:
1) 存在一个根节点,不代表任何字符。树中的其它每个节点保存关键词中的一个字符,我们以字符值代指该节点
2) 若存在一个关键词,A是词中的一个字符,B是A的后继字符,则称B是A的子节点,所有关键词的第一个字符都是根节点的子节点。相同的值用同一个子节点表示。
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个指针,或者用256个BIT位来指明指定的值是否存在,但在空间上有较大的浪费。本算法用一个简单的hash算法,在空间和时间上的得到一个平衡,在实际的应用中取得较好的效果。