完整的实现代码和扩展代码在下面给出的网址中可下载:
多模匹配的意思在目标字符串中同时查找多个模式串,比较常用的算法为AC自动机算法,读者可以参照维基百科上的AC自动机算法介绍(http://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm)。
一般会构造一颗查找树,并建立失败跳转关系。本文我们将讲解另外一种方式,二维数组法。此二维数组法,在没有看到代码生成的二维数组之前,很难说明白它的思路,所以我将不同于以往在单模匹配中的讲解方法,先讲原理,后写代码。这里我将先给出代码,针对给定的多个模式串,构造出二维数组,然后对照生成的二维数组来讲解该方法的原理和查找过程。
#define MAX_SIG_SIZE 20
struct MULTI_PATTERN
{
bool bMask;
byte byMask;
int nLen;
byte SigData[MAX_SIG_SIZE];
};
//we assume that, the characters in patterns are lower letters.
//so there are only 26 letters.
#define ALL_LOWER_CASE 26
#define END_FLAG 0x80
typedef byte (*JMPTABLE)[ALL_LOWER_CASE];
//we assume that, the characters in patterns are lower letters.
MULTI_PATTERN g_PatternTable[] =
{
{false, 0, 5, {'o', 'b', 'a', 'm', 'a'} },
{false, 0, 5, {'y', 'i', 'x', 'i', 'n'} },
{false, 0, 6, {'y', 'i', 'd', 'i', 'a', 'n'} },
};
JMPTABLE CreateJumpTable(MULTI_PATTERN patternTable[], int nLen)
{
int nTotalLen = 0;
byte byNextRow = 0;
JMPTABLE pJmpTab;
for (int i = 0; i < nLen; i++)
{
nTotalLen += patternTable[i].nLen;
}
//use byte array only for study, maybe you need use integer array in project
pJmpTab = (JMPTABLE)malloc(nTotalLen*ALL_LOWER_CASE);
if (!pJmpTab)
{
return pJmpTab;
}
memset(pJmpTab, 0, nTotalLen*ALL_LOWER_CASE);
for (int i = 0; i < nLen; i++)
{
byte byCurRow = 0;
for (int j = 0; j < patternTable[i].nLen; j++)
{
byte cSign = patternTable[i].SigData[j] - 'a';
byte cNext = pJmpTab[byCurRow][cSign];
byNextRow++;
if (cNext == 0)
{
if (j == patternTable[i].nLen-1)//is the last character of the pattern, and the inner loop will over
{
pJmpTab[byCurRow][cSign] = i | END_FLAG;// 'i' indicate the pattern number
}
else
{
pJmpTab[byCurRow][cSign] = byNextRow;
}
byCurRow = byNextRow;
}
else
{
assert(0 == (cNext & END_FLAG));//if reach a end position, the prefix of current pattern is another pattern
byCurRow = cNext;
}
}
}
return pJmpTab;
}
调用上面的函数:
JMPTABLE pJmpTable = CreateJumpTable(g_PatternTable,
sizeof(g_PatternTable)/sizeof(MULTI_PATTERN));
{'o', 'b', 'a', 'm', 'a'}(编号为0)
{'y', 'i', 'x', 'i', 'n'}(编号为1)
{'y', 'i', 'd', 'i', 'a', 'n'}(编号为2)
的长度之和。26代表所有的小写字母的集合。为了演示方便,所以我们先假设模式串都是小写字母。上面的代码得到如下所示的二维数组:
(表格1)(表格显示不全,可在另外一个页面中打开图片,显示完整表格)
二维数组中的值首先全部初始化为0,然后依次遍历三个模式串,首先看模式串{'o', 'b', 'a', 'm', 'a'},第一个字符是'o',所以第0行的第o列填写上1,意思是如果目标串出现的也是'o'(匹配上了),那么就继续匹配下一个字符,而下一个字符的信息将会存储到第1行。然后看第二个字符'b',它的信息需要存储到第1行,所以第1行的'b'列填写2,同理第2行的'a'列填3,第3行的'm'列填4。处理最后一个字符'a’的方法和前面不同,是在对应的行列中填写一个0x80|0的数值,前面的0x80是一个标志,代表的是这里是一个模式串的末位字符,后面的0代表这是编号为0的模式串。所以第[9][n]处 = 0x81,代表的是编号为1的模式串{'y', 'i', 'x', 'i', 'n'}的结尾;第[10][n]处 = 0x82,代表的是编号为2的模式串{'y', 'i', 'd', 'i', 'a', 'n'}的结尾。
处理完编号为0的模式串之后,我们将处理编号为1的模式串,需要注意的是编号为1的模式串的第一个字符'y'的字符信息并不填写在第5行,而是需要填写到第0行去,而且编号为2的模式串的首字符信息也要填写到第0行去。这是因为我们进行多模匹配的时候都是从第0行开始的,如果不填在第0行,则首字符的比较需要多个行的参与,这就起不到同时进行多模匹配的效果了。所以我们在第[0][y]处填写6,6表示下一个字符的信息填写在第6行,下一个字符是'i',所以[6][i] = 7。依次处理完编号为1的模式串,最后填写的是[9][n] = 81。
然后来处理编号为2的模式串{'y', 'i', 'd', 'i', 'a', 'n'},同样第一个字符'y'要填写在第0行,这时候我们会发现[0][y]已经有数值 = 6(在处理编号1模式串的第一个字符时填上的),此时我们不改变该数值,而且需以该数值为指示,将下一个字符'i'的信息填写在第6行上,但是[6][i]正好也有数值了(处理模式串1的第2个字符时填上的)= 7,所以我们依旧不更改此数值,而且要根据该数值将下一个字符'd'的信息填写在第7行,[7][d]的值 = 0,所以我们在[7][d]处填写上0x0d(十进制的13)代表下一个字符'a'要填写在第0xd行(下一个字符'i'本身就是所有模式串组成的顺序中编号为13的字符)。
总结一下填表过程:
所有模式串的第一个字符信息要填写在第0行里面。
如果需要填写的位置已经有数值了,那么不要更改该数值,且取该数值作为下一个字符应该填写的行。
否则下一个字符按照自己出现在总序列中的顺序填写在自己对应的行中。
如果到达了模式串的结尾,则填写一个既能表示这是结尾、又包含了这是编号为几的模式串信息的数值。(在上面的代码中采用0x80|模式串编号的处理方法,如果模式串个数超过0x80个则需要扩展)
匹配时,只需要根据生成的二维数组,来进行上一行到下一行(上一个字符到下一个字符)的跳转即可。如果跳转到某个位置时,该位置的值表示这是一个模式串的结尾,那么我们就找到了一个模式匹配位置,对应的模式串编号可以从该数值中分离出来。
举例说明,当在目标串"fobyixinabcd"中查找上述编号0,1,2三个模式串时:
首先从目标串中下标0处的字符f开始进行查表
总结查表匹配过程:
对应查表匹配代码如下:
char* MultiSearch(char *p, int nLen, JMPTABLE pJmpTable, int &nIdx)
{
int nRow = 0;
int nMatchMaybe = 0;
bool bSetMatchMaybe = false;
for (int nStart = 0; nStart < nLen; nStart++)
{
byte byNextRow = pJmpTable[nRow][p[nStart]-'a'];
if (0 != byNextRow)
{
//check whether is end of pattern or not
if (byNextRow & END_FLAG)
{
nIdx = byNextRow & (~END_FLAG);
return &p[nStart];
}
if (!bSetMatchMaybe)
{
bSetMatchMaybe = true;
nMatchMaybe = nStart;
}
}
else
{
if (bSetMatchMaybe)
{
bSetMatchMaybe = false;
nStart = nMatchMaybe;
}
}
nRow = byNextRow;
}
return NULL;
}
上述代码看起来是一重循环,但里面对nStart进行了回溯。该代码可以稍作修改,写成如下2重循环的形式,两者实质上是一样的。
char* MultiSearch2(char *p, int nLen, JMPTABLE pJmpTable, int &nIdx)
{
for (int nStart = 0; nStart < nLen; nStart++)
{
for (int i = nStart, nRow = 0; i < nLen; i++)
{
byte byNextRow = pJmpTable[nRow][p[i]-'a'];
if (0 == byNextRow)
{
break;
}
//check whether is end of pattern or not
if (byNextRow & END_FLAG)
{
nIdx = byNextRow & (~END_FLAG);
return &p[i];
}
nRow = byNextRow;
}
}
return NULL;
}
二维数组法可以适用于模糊匹配,具体体现在填表过程中就是当遇到表示模糊匹配的位置时,在对应的行中每一列都填写下一个字符所在的行号,例如对于下面的3个模式串:
//in fuzzy match, the prefix at fuzzy position can not same as another pattern
//otherwise, it will make a conflict.
//the fuzzy char can not at the begin or end of the pattern, is meaningless
MULTI_PATTERN g_FuzzyPatternTable[] =
{
{false, 0, 5, {'o', 'y', 'i', 'm', 'a'} },
{true, '*', 5, {'y', 'i', 'x', '*', 'n'} },
{true, '?', 6, {'y', 'i', 'd', 'i', '?', 'n'} },
};
生成二维数组的代码需要单独处理模糊匹配的位置:
JMPTABLE CreateFuzzyJumpTable(MULTI_PATTERN patternTable[], int nLen)
{
int nTotalLen = 0;
byte byNextRow = 0;
JMPTABLE pJmpTab;
//we can reduce some rows
//1 every pattern's head char mapping in first row,
// so we can save one row for every pattern begin at second pattern
//2 if a pattern's prefix is same as other pattern's prefix, the repeat part don't need new rows
// but, if the rows reduce, the code must modify for it
for (int i = 0; i < nLen; i++)
{
nTotalLen += patternTable[i].nLen;
}
//use byte array only for study, maybe you need use integer array in project
pJmpTab = (JMPTABLE)malloc(nTotalLen*ALL_LOWER_CASE);
if (!pJmpTab)
{
return pJmpTab;
}
memset(pJmpTab, 0, nTotalLen*ALL_LOWER_CASE);
for (int i = 0; i < nLen; i++)
{
byte byCurRow = 0;
for (int j = 0; j < patternTable[i].nLen; j++)
{
byte cSign = patternTable[i].SigData[j];
byNextRow++;
if (patternTable[i].bMask && patternTable[i].byMask == cSign)//in fuzzy match, add this branch
{
FillTable(pJmpTab[byCurRow], byNextRow);
byCurRow = byNextRow;
continue;
}
cSign -= 'a';
byte cNext = pJmpTab[byCurRow][cSign];
if (cNext == 0)
{
if (j == patternTable[i].nLen-1)//the last character of the pattern, and the inner loop will over
{
pJmpTab[byCurRow][cSign] = i | END_FLAG;// 'i' indicate the pattern number
}
else
{
pJmpTab[byCurRow][cSign] = byNextRow;
}
byCurRow = byNextRow;
}
else
{
assert(0 == (cNext & END_FLAG));//if reach a end position, the prefix of current pattern is another pattern
byCurRow = cNext;
}
}
}
return pJmpTab;
}
(表格2)
我们看到在第8行表示'*'的位置所有列都被填上了9,也就是说匹配到这里,不管目标字符串中是什么字母(26个小写字母中的任意一个)都匹配成功,并指示下一个待匹配字母需要到第9行去查表。
同样,第0xe行的'?'位置所有列都填上了0x0f。
使用二维表进行多模匹配时也有一些局限性,考虑这样的两个模式串'yixin'、'yi*bc',后面一个模式串在模糊匹配的*字符之前的字符和前面一个模式串的同样位置的字符相同,这样在处理后一个模式串的'*'字符时,会将一整行都处理成表示自己的下一个字符所在的行的数值,这样会覆盖第一个字符串中同位置的'x'在表中的信息,造成漏匹配第一个模式串的情况。
另外,如果模式串有包含关系,例如'yixin'、'yixinn',后一个模式串会覆盖掉前一个模式串的结尾标识位,或者反过来,如果先处理长的,后处理短的,短的模式串会截断长的模式串。
我们查看表格1和表格2,可以看到在第5,a,b,c行(黄色背景部分)全部都是0,这些行是两个原因造成的:
针对这两种情况都可以缩减使用行数,降低内存开销,情况1很好处理,计算完所有行后减去(模式串个数-1)即可。
情况2比较麻烦一点,需要计算模式串开头相同的部分,所以我们申请内存时不考虑情况2可以省略的行数,但是我们可以在使用内存时不在中间产生空行,具体代码如下:
//CreateFuzzyJumpTable2 modify from CreateFuzzyJumpTable
//just save some rows for the reason 1(every pattern's head char mapping in first row)
//for reason 2, we can reduce rows further, but is not easy to calculation the repeat prefix
//so we do not do this, but in code we can let the redundant rows all behind the malloc memory.
JMPTABLE CreateFuzzyJumpTable2(MULTI_PATTERN patternTable[], int nLen)
{
int nTotalLen = 0;
byte byNextRow = 0;
JMPTABLE pJmpTab;
for (int i = 0; i < nLen; i++)
{
nTotalLen += patternTable[i].nLen;
}
nTotalLen -= nLen-1;//for reason 1
//use byte array only for study, maybe you need use integer array in project
pJmpTab = (JMPTABLE)malloc(nTotalLen*ALL_LOWER_CASE);
if (!pJmpTab)
{
return pJmpTab;
}
memset(pJmpTab, 0, nTotalLen*ALL_LOWER_CASE);
for (int i = 0; i < nLen; i++)
{
byte byCurRow = 0;
for (int j = 0; j < patternTable[i].nLen; j++)
{
byte cSign = patternTable[i].SigData[j];
//byNextRow++; only add this value in necessary
if (patternTable[i].bMask && patternTable[i].byMask == cSign)//in fuzzy match, add this branch
{
byNextRow++;//need add
FillTable(pJmpTab[byCurRow], byNextRow);
byCurRow = byNextRow;
continue;
}
cSign -= 'a';
byte cNext = pJmpTab[byCurRow][cSign];
if (cNext == 0)
{
if (j == patternTable[i].nLen-1)//the last character of the pattern, and the inner loop will over
{
pJmpTab[byCurRow][cSign] = i | END_FLAG;// 'i' indicate the pattern number
}
else
{
byNextRow++;//need add
pJmpTab[byCurRow][cSign] = byNextRow;
}
byCurRow = byNextRow;
}
else
{
assert(0 == (cNext & END_FLAG));//if reach a end position, the prefix of current pattern is another pattern
byCurRow = cNext;
}
}
}
return pJmpTab;
}
3个模式串可以少申请2行的内存,即上表的e,f两行;又因为编号1,2的模式串前面的'yi'开头部分相同,所以在内存使用上会有两行没有使用(第c,d行)
改进之后的二维表不影响模式匹配过程。
完整的实现代码和扩展代码在下面给出的网址中可下载:
http://download.csdn.net/detail/sun2043430/5276263