用于将字符集转换为nfa / dfa的高效算法

时间:2010-08-21 19:13:55

标签: regex algorithm dfa nfa

我目前正在研发扫描仪发生器。 发电机已经正常工作。但是当使用字符类时,算法变得非常慢。

扫描仪生成器为UTF8编码文件生成扫描仪。应支持全范围的字符(0x000000到0x10ffff)。

如果我使用大字符集,例如任何运算符'。'或者unicode属性{L},nfa(以及dfa)包含许多状态(> 10000)。因此,将nfa转换为dfa并创建最小dfa需要很长时间(即使输出最小dfa只包含几个状态)。

这是我目前在创建nfa的字符集部分时的实现。

void CreateNfaPart(int startStateIndex, int endStateIndex, Set<int> characters)
{
transitions[startStateIndex] = CreateEmptyTransitionsArray();
foreach (int character in characters) {
    // get the utf8 encoded bytes for the character
    byte[] encoded = EncodingHelper.EncodeCharacter(character);
    int tStartStateIndex = startStateIndex;
    for (int i = 0; i < encoded.Length - 1; i++) {
        int tEndStateIndex = transitions[tStartStateIndex][encoded[i]];
        if (tEndStateIndex == -1) {
           tEndStateIndex = CreateState();
               transitions[tEndStateIndex] = CreateEmptyTransitionsArray();
        }                   
        transitions[tStartStateIndex][encoded[i]] = tEndStateIndex;
        tStartStateIndex = tEndStateIndex;
    }
    transitions[tStartStateIndex][encoded[encoded.Length - 1]] = endStateIndex;
}

是否有人知道如何更有效地实现该功能以仅创建必要的状态?

编辑:

更具体地说,我需要一个像:

这样的功能
List<Set<byte>[]> Convert(Set<int> characters)
{
     ???????
}

将字符(int)转换为UTF8编码byte []的辅助函数定义为:

byte[] EncodeCharacter(int character)
{ ... }

5 个答案:

答案 0 :(得分:3)

有很多方法可以处理它。它们都归结为在数据结构中一次处理一组字符,而不是一直枚举整个字母表。这也是你在合理数量的内存中制作Unicode扫描仪的方法。

您有很多关于如何表示和处理字符集的选择。我目前正在使用一种解决方案来保持边界条件和相应目标状态的有序列表。如果必须在每个时刻扫描整个字母表,您可以比在这些列表上更快地处理这些列表上的操作。事实上,它足够快,以可接受的速度在Python中运行。

答案 1 :(得分:3)

我将澄清您的要求:合并一组Unicode代码点,以便生成最小状态DFA,其中过渡表示这些代码点的UTF8编码序列。

当您说“更有效”时,这可能适用于运行时,内存使用或最终结果的紧凑性。有限自动机中“最小”的通常含义是指使用最少的状态来描述任何给定的语言,这就是通过“仅创建必要的状态”而得到的。

每个有限自动机都有一个等效的状态最小 DFA(请参见 Myhill-Nerode 定理[1]或Hopcroft&Ullman [2])。为了您的目的,我们可以直接使用Aho-Corasick算法[3]来构建此最小DFA。

为此,我们需要从Unicode代码点到它们对应的UTF8编码的映射。无需预先存储所有这些UTF8字节序列;它们可以即时编码。 UTF8编码算法已被详细记录,在此不再赘述。

Aho-Corasick首先构建一个 trie 。在您的情况下,这将是依次添加的每个UTF8序列的特里。然后,在其余算法中对该过渡进行注释,并将其转换为DAG。 overview of the algorithm here很不错,但我建议您阅读论文本身。

此方法的伪代码:

trie = empty
foreach codepoint in input_set:
   bytes[] = utf8_encode(codepoint)
   trie_add_key(bytes)
dfa = add_failure_edges(trie) # per the rest of AC

这种方法(形成一个由UTF8编码的序列,然后是Aho-Corasick,然后渲染出DFA)是在我的regexp和有限状态机库的实现中采用的方法,在此我正是用这种方法来构造Unicode字符类。在这里,您可以看到以下代码:

其他方法(如对该问题的其他答案所述)包括处理代码点和表达代码点范围,而不是拼出每个字节序列。

[1] Myhill-Nerode:Nerode,Anil(1958年),线性自动机变换,AMS论文集,第9卷,JSTOR 2033204
[2] Hopcroft&Ullman(1979),第3.4节,定理3.10,第67页
[3] Aho,阿尔弗雷德·五世;玛格丽特·J·科拉西克(1975年6月)。 高效的字符串匹配:书目搜索的辅助工具。 ACM的通信。 18(6):333–340。

答案 2 :(得分:2)

了解Google RE2和TRE正在使用的正则表达式库。

答案 3 :(得分:0)

我的扫描仪生成器遇到了同样的问题,所以我想出了用间隔​​树确定的ID替换间隔的想法。例如,dfa中的a..z范围可以表示为:97,98,99,...,122,而我将范围表示为[97,122],然后构建间隔树结构,因此最后它们表示为指间隔树的id。鉴于以下RE:a..z +,我们最终得到了这样的DFA:

0 -> a -> 1
0 -> b -> 1
0 -> c -> 1
0 -> ... -> 1
0 -> z -> 1

1 -> a -> 1
1 -> b -> 1
1 -> c -> 1
1 -> ... -> 1
1 -> z -> 1
1 -> E -> ACCEPT

现在压缩间隔:

0 -> a..z -> 1

1 -> a..z -> 1
1 -> E -> ACCEPT

从DFA中提取所有间隔并从中构建间隔树:

{
    "left": null,
    "middle": {
        id: 0,
        interval: [a, z],
    },
    "right": null
}

将实际间隔替换为其ID:

0 -> 0 -> 1
1 -> 0 -> 1
1 -> E -> ACCEPT

答案 4 :(得分:0)

在这个库(http://mtimmerm.github.io/dfalex/)中,我通过在每个转换上放置一系列连续字符而不是单个字符来实现。这贯穿于NFA构建,NFA-&gt; DFA转换,DFA最小化和优化的所有步骤。

它非常紧凑,但它为每一步都增加了代码复杂性。