【模式匹配】之——多模匹配 上篇(AC算法)

作者:sun2043430

  1. 前言
  2. 多模匹配之二维数组法代码
  3. 多模匹配之二维数组法原理讲解二维数组构造方法
  4. 多模匹配过程及代码实现
  5. 二维数组法适用于模糊匹配
  6. 二维表多模匹配的局限性
  7. 二维表的优化
  8. 总结

完整的实现代码和扩展代码在下面给出的网址中可下载:


前言

多模匹配的意思在目标字符串中同时查找多个模式串,比较常用的算法为AC自动机算法,读者可以参照维基百科上的AC自动机算法介绍(http://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm)。

一般会构造一颗查找树,并建立失败跳转关系。本文我们将讲解另外一种方式,二维数组法。此二维数组法,在没有看到代码生成的二维数组之前,很难说明白它的思路,所以我将不同于以往在单模匹配中的讲解方法,先讲原理,后写代码。这里我将先给出代码,针对给定的多个模式串,构造出二维数组,然后对照生成的二维数组来讲解该方法的原理和查找过程。


多模匹配之二维数组法代码

注意:此代码中为了将所有的小写字母存放在大小为26的数组中,对每个小写字母进行了 -'a'操作。
#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));

将会得到一个二维数组pJmpTable ,它的维度为[16][26],其中的16等于三个模式串

{'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开始进行查表

  1. 目标串中下标0处f查表,pJmpTable[0]['f'] = 0,所以下一个字符(下标1处的'o')还是和第0行进行试匹配
  2. 目标串中下标1处o查表,pJmpTable[0]['o'] = 1,所以下一个字符(下标2处的'b')和第1行试匹配(这里可能是一个匹配的开始,记录为nMatchMaybe 位置 = 1)
  3. 目标串中下标2处b查表,pJmpTable[1]['b'] = 2,所以接下来看(下标3处的'y')
  4. 目标串中下标3处y查表,pJmpTable[2]['y'] = 0,等于0表示这里没有模式串可以匹配,匹配到这里就失败了。
  5. 接下来,我们需要继续从目标串的nMatchMaybe+1位置(目标串下标2位置)重复上面的查表过程(这里产生了回溯,不是很好,用多叉树代替查表法可以无需回溯,下一篇文章对此进行介绍)直到找到匹配模式串或者到达目标串的末尾。


总结查表匹配过程:

  1. 从目标串开头的字符、表的第0行开始查二维表,查到的数组指示下一个字符该到哪一行去继续查表,记为NextRow
  2.     如果NextRow非0,该位置可能是一个匹配的开始,设置nMatchMaybe= 该位置
  3. 依据NextRow的指引继续查后面的字符
  4.     如果nMatchMaybe被设置后,查表得到的NextRow又出现了 = 0的情况(说明失配),则需要回溯到nMatchMaybe+1位置从表的第0行重新查表匹配
  5. 循环上面的过程,直到查找到表示模式串结尾的数值(找到了一个匹配位置)或到达目标串结尾。

对应查表匹配代码如下:

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'} },
};

{'y', 'i', 'x', '*', 'n'}中的'*'表示模糊匹配,{'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. 每个模式串的首字符都填写在了第一行,所有从第2个模式串开始,每个模式串的首字符,可以减少一行内存开销,也就是第5行和第a行,是可以省略的,相应的后面行数值少加一个1
  2. 如果模式串前面有相同的字符,例如编号1、2的模式串都是以'yi’开头的,在处理编号为2的模式串时,会有两行和编号1模式串共用,所以造成第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行)

改进之后的二维表不影响模式匹配过程。


总结

二维表格法进行多模匹配时,形成的表格直观、易理解,但是消耗过多内存(表格中为0的地方);
二维表格法有模糊匹配的能力,但是需要模式串满足一定的条件(不能产生冲突);
二维表格法不能很好地处理具有前缀包含关系的模式串。

上面的代码里面为了便于演示,假设了所有的模式串和目标串中的字符都是小写字母,且二维表是char类型的,不是int类型的,如果匹配长模式串,需要扩展到int类型,同时二维表应该扩充到256个字节空间,完整表示所有单字节字符。

完整的实现代码和扩展代码在下面给出的网址中可下载:

http://download.csdn.net/detail/sun2043430/5276263


发表评论

0个评论

我要留言×

技术领域:

我要留言×

留言成功,我们将在审核后加至投票列表中!

提示x

人工智能规划与决策知识库已成功保存至我的图谱现在你可以用它来管理自己的知识内容了

删除图谱提示×

你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?

删除节点提示×

无法删除该知识节点,因该节点下仍保存有相关知识内容!

删除节点提示×

你确定要删除该知识节点吗?