根据语言L

时间:2018-10-30 10:25:58

标签: c regex automata fsm computation

我正在尝试根据正则表达式(也由用户提供)创建字母顺序(由用户提供)的单词序列,但无法实现。

示例场景1:

Alphabet = [a,b,c]

Regex = (a+c)b*

Word Count = 6

Words = ["a", "c", "ab", "cb", "abb", "cbb"]

示例场景2:

Alphabet = [a,b]

Regex = (a+b)*a

Word Count = 3

Words = ["a", "aa", "ba"]

我尝试将正则表达式转换为后缀/中缀,然后从那里去,但无法构建引擎算法。

基本上有3个操作;

联盟(+)
康卡特()
闭包(*)

我为每种运算符类型编写了一个函数;

void union(char* x[], char y)
{
    printf("%s\n%c\n", x, y);

    remainingWordCount -= 2;
}

void concat(char* x[], char* y[])
{
    printf("%s%s\n", x, y);
    remainingWordCount--;
}

void closure(char* x[], char* y[])
{
    while (remainingWordCount > 0)
    {
        concat(x, y);
    }
}

仅在大多数基本情况下有效。

所以我的问题是如何在不使用任何正则表达式库的情况下根据给定的正则表达式创建字符串集?有什么已知的算法吗?

3 个答案:

答案 0 :(得分:0)

“强力”解决方案是将正则表达式解析为有限状态机的状态转换图,其中每个状态都有一个转换列表,每个转换都具有关联的符号(字符)和下一个状态。您可以使用没有过渡的状态来指示终端状态。

然后遍历此图,记住过渡产生的字符串。达到终端状态时,打印单词并减少剩余的单词数,直到达到零时停止。如果通过图形的不同路径最终可能产生相同的单词,那么您还需要记住已经输出的任何单词,如果已经存在相同的单词,则不要打印/递减。

以排序的顺序处理路径(以使较短的路径位于具有相同前缀的较长路径之前,即,按照C中的strcmp)。这样可以避免陷入循环,并给出所需的顺序。

例如,对于正则表达式a*(伪代码):

state[0] = { {'a', 0}, {'\0', 1} };
state[1] = { }; // terminal state
paths = { { .state = 0, .string = "" } }; // set initial state

从状态为0的唯一路径开始,然后(分别)将状态0的两个转换附加到路径上,以创建新路径:

paths = { { .state = 1, .string = "" },
          { .state = 0, .string = "a" } };

由于具有空字符串的路径首先被排序(由于空字符串在任何非空字符串之前被排序),因此将首先对其进行处理,并且由于它处于没有过渡的终端状态,因此它将打印空字符串并减少字数。然后,您采用另一条路径,并再次添加从状态0开始的过渡,最后以:

paths = { { .state = 1, .string = "a" },
          { .state = 0, .string = "aa" } };

(免责声明:这是完全未经测试的,超出我的头脑,并且可能有一些我没想到的极端情况。还要注意,对于非平凡的正则表达式,路径数量会激增。)

答案 1 :(得分:0)

基本算法很简单(如果您知道如何做所有事情):

  1. Construct a DFA from the regular expression。 (构造NFA是不够的,因为如果正则表达式不确定,则NFA会产生重复的字符串。)链接显示了一种执行此操作的方法,但还有其他方法。如果有的话,您可能会在正式语言教科书中找到更长的阐述。

  2. 对从DFA生成的(无限)图进行有序遍历("best-first search"),其中每个节点都是一对(state, prefix),并且边对应于DFA中的过渡。在遍历期间,如果遇到state正在接受的节点,请产生其prefix

该基本算法将对具有prefix属性的字符串之间的任何排序关系起作用:保证字符串的任何适当前缀都小于该字符串。 (如果不是这种情况,则可能在一组字符串中没有“最少”元素。例如,如果排序关系将字符串放在该字符串的任何前缀之前,但按字典顺序,则其中的任何字符串a*之前是下一个较长的字符串,它将产生无限循环。)

请注意,节点中的state仅是为了方便;它在计算上是多余的,因为可以通过将prefix通过DFA来重新生成。结果,该图永远不会包含具有相同prefix的两个不同节点。该观察结果的必然结果是,不必维护一组“可见”节点,因为两个不同节点的后继集合是不相交的。

为了在步骤2中实现有序搜索,必须知道图中每个节点的最少接受后继者,不一定是前缀最少的直接后继者。

例如,字典序(“字母”)的排序关系由下式给出:

(S1, P1) < (S2, P2) iff P1 <lexicographic P2

在这种情况下,接受最少的继任者肯定具有最少的直接后继者作为前缀,因此按前缀对候选者进行排序就足够了。

通过对比,更常见的“先按长度再按字典顺序”的顺序是:

(S1, P1) < (S2, P2) iff |P1| < |P2| or (|P1| = |P2| and P1 <lexicographic P2)

仅通过查看两个节点的直接后继节点,就无法预测它们被接受程度最低的后继节点的顺序。您还需要知道到达接受节点(或等效状态)所需的最小符号数。幸运的是,使用DFA上的任何all-pairs shortest-paths algorithm都可以轻松进行预计算。

答案 2 :(得分:0)

我建议使用“迭代器”设计模式。我看到您正在使用C,它并不特别适合面向对象的代码,但是您可以通过使用包含指向next函数的指针,指向restart的指针的结构来完成此操作。函数,以及指向将传递给这些函数的“上下文”对象的指针,其中“上下文”对象的性质将取决于运算符。

在类似a的情况下,next函数第一次返回"a",第二次返回NULL。 (上下文对象跟踪"a"及其是否已经返回。)

在类似...+...的情况下,next可以先耗尽第一个...的迭代器,然后再进行第二个迭代,或者可以根据需要在它们之间进行交替。 (上下文对象保留了指向两个...的迭代器以及接下来要调用的迭代器的指针。)

。 。 。等等。

最难的部分是解析表达式以创建所有这些对象,但是听起来您已经对解析表达式感到满意了?