避免递归 - 下降解析器

时间:2016-08-05 19:35:53

标签: c++ algorithm parsing recursion

我参与了与解析器相关的项目,并使用recursive descent parser实现了它。但问题是,它很容易导致堆栈溢出。处理此类问题的技术有哪些?

为了说明,这里是简单的数学表达式解析器,具有加法,减法,乘法和除法支持。可以使用分组括号,它们会明显触发递归。

这里是完整的代码:

#include <string>
#include <list>
#include <iostream>

using namespace std;

struct term_t;
typedef list<term_t> prod_t;
typedef list<prod_t> expr_t;
struct term_t
{
    bool div;
    double value;
    expr_t expr;
};

double eval(const expr_t &expr);
double eval(const term_t &term)
{
    return !term.expr.empty() ? eval(term.expr) : term.value;
}
double eval(const prod_t &terms)
{
    double ret = 1;
    for (const auto &term : terms)
    {
        double x = eval(term);
        if (term.div)
            ret /= x;
        else
            ret *= x;
    }
    return ret;
}
double eval(const expr_t &expr)
{
    double ret = 0;
    for (const auto &prod : expr)
        ret += eval(prod);
    return ret;
}

class expression
{
public:
    expression(const char *expr) : p(expr)
    {
        prod();
        for (;;)
        {
            ws();
            if (!next('+') && *p != '-') // treat (a-b) as (a+-b)
                break;
            prod();
        }
    }
    operator const expr_t&() const
    {
        return expr;
    }

private:
    void term()
    {
        expr.back().resize(expr.back().size() + 1);
        term_t &t = expr.back().back();
        ws();
        if (next('('))
        {
            expression parser(p);  // recursion
            p = parser.p;
            t.expr.swap(parser.expr);
            ws();
            if (!next(')'))
                throw "expected ')'";
        }
        else
            num(t.value);
    }
    void num(double &f)
    {
        int n;
        if (sscanf(p, "%lf%n", &f, &n) < 1)
            throw "cannot parse number";
        p += n;
    }
    void prod()
    {
        expr.resize(expr.size() + 1);
        term();
        for (;;)
        {
            ws();
            if (!next('/') && !next('*'))
                break;
            term();
        }
    }
    void ws()
    {
        while (*p == ' ' || *p == '\t')
            ++p;
    }
    bool next(char c)
    {
        if (*p != c)
            return false;
        ++p;
        return true;
    }

    const char *p;
    expr_t expr;
};

int main()
{
    string expr;
    while (getline(cin, expr))
        cout << "= " << eval(expression(expr.c_str())) << endl;
}

如果你跑,你可以输入简单的数学表达式,如1+2*3+4*(5+6*7),并正确计算195。 我还添加了简单的表达式求值,它还会导致递归并导致堆栈溢出比解析更容易。无论如何,解析本身很简单明了,如何在不对代码进行大量更改的情况下重写它并完全避免递归?在我的情况下,我使用类似于这个(((((1)))))的表达式来引起递归,如果我只有几百个括号,我将得到堆栈溢出。如果我只使用调试器(在Visual Studio中)递归树,如果只有三个函数:[term - &gt;] expression ctor - &gt; prod - &gt; term并且从寄存器检查这三个函数占用700-1000字节的堆栈空间。使用优化设置和摆弄代码,我可以减少占用,并且使用编译器设置我可以增加堆栈空间,或者在这种情况下我也可以使用Dijksta's shunting-yard algorithm但这不是问题的关键点:我想要知道如何重写它以避免递归,同时,如果可能的话,不用完全重写解析代码。

3 个答案:

答案 0 :(得分:3)

递归 -descent解析器必然是递归的;这个名字并非反复无常。

如果一个产品是右递归的,那么它相应的递归下降动作是尾递归的。因此,使用适当的语法,您可以生成一个尾递归解析器,但括号表达式的生成将难以成为该约束。 (见下文。)

您可以通过维护模拟调用堆栈来模拟递归,但堆栈操作可能会压倒递归下降解析器的简单性。在任何情况下,都有更简单的迭代算法使用显式解析堆栈,因此使用其中一个更有意义。但这不会回答这个问题。

注意:如果您使用C ++,那么您必须跳过一些箍来创建尾部上下文。特别是,如果使用非平凡的析构函数(例如std :: list)分配对象,则自动析构函数调用在尾部上下文中发生,最后一个显式函数调用不是尾部调用。

答案 1 :(得分:2)

递归下降解析器的常见做法是递归到子表达式,非终结符或嵌套构造,但不使用递归来继续在同一级别进行解析。这使得堆栈大小限制了您可以解析的字符串的最大“深度”,但不限制其长度。

看起来你做的那部分是正确的,所以让我们看一下典型的数字......

由于基于堆栈的限制,通常会编写递归解析函数,以便它们不会使用大量堆栈 - 大约128字节左右的平均值。

所以,如果你有128K的堆栈空间(这通常意味着你的堆栈已经满90%),那么你应该能够获得1000个左右的水平,那就是很多用于程序员实际键入的真实世界文本。

在您的情况下,您在堆栈中只获得200个级别。对于现实生活来说这可能也没问题,但除非你在一个非常有限的硬件环境中运行,否则它表明你只是在递归函数中使用了太多的堆栈空间。

我不知道整个班级的大小,但我猜想主要的问题是在term()中你用{{1}将一个全新的expression放在堆栈上} 宣言。这是非常不寻常的,看起来可能需要很大的空间。你应该避免制作这个全新的对象。

打印expression parser(p);,看看这有多大。

答案 2 :(得分:1)

对于解析表达式,请查看运算符优先级解析,例如http://epaperpress.com/oper/download/OperatorPrecedenceParsing.pdf。它使用数据堆栈在一个简单的循环中解析表达式。 200个嵌套括号所需的唯一空间是数据堆栈中的200个条目。

有些语言可以在运行时添加新的运算符,编译的程序指定了这些运算符的关联性和优先级,这是一个无法用递归的正确解析器处理的东西。