在递归下降语法分析器中避免了递归算法中的stackoverflow
avoid stackoverflow in recursive algorithm in 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
->]expression ctor
->prod
->term
,并且从寄存器检查来看,这三个函数占用700-1000字节的堆栈空间。通过优化设置和稍微篡改代码,我可以减少它的占用,通过编译器设置,我可以增加堆栈空间,或者在这种情况下,我也可以使用Dijksta的调车场算法,但这不是问题的关键:我想知道如何重写它以避免递归,同时,如果可能的话,不需要完全重写解析代码。
递归下降语法分析器的常见做法是递归到子表达式、非终端或嵌套结构中,但不使用递归在同一级别继续解析。这使得堆栈大小限制了可以解析的字符串的最大"深度",但不限制其长度。
看起来你做得对,所以让我们看看典型的数字。。。
由于基于堆栈的限制,递归解析函数通常是这样编写的,这样它们就不会使用太多堆栈——128字节左右的平均值会很高。
因此,如果你有128K的堆栈空间(这通常意味着你的堆栈已经满了90%(,那么你应该能够获得1000个级别左右,对于程序员实际键入的现实世界文本来说,这是足够的。
在您的情况下,堆栈中只有200个级别。这对于现实生活来说可能也是可以的,但除非你在一个非常受限的硬件环境中运行,否则这表明你只是在递归函数中使用了太多的堆栈空间。
我不知道整个类的大小,但我想主要的问题是在term()
中,您使用expression parser(p);
声明在堆栈上放置了一个全新的expression
。这是非常不寻常的,看起来可能需要很大的空间。你可能应该避免制作这个全新的对象。
打印出sizeof(expression)
,看看它到底有多大。
递归-下降语法分析器必然是递归的;这个名字并不任性。
如果一个乘积是右递归的,那么它对应的递归下降动作是尾递归的。因此,使用适当的语法,您可以生成尾部递归解析器,但生成带括号的表达式将很难硬塞进该约束中。(见下文。(
您可以通过维护模拟的调用堆栈来模拟递归,但堆栈操作可能会压倒递归下降解析器的简单性。在任何情况下,都有更简单的迭代算法使用显式解析堆栈,因此使用其中一个可能更有意义。但这并不能回答问题。
注意:如果使用C++,则必须跳过一些环节才能创建尾部上下文。特别是,如果您为对象分配了一个非平凡的析构函数(如std::list(,那么自动析构函数调用发生在尾部上下文中,最后一个显式函数调用不是尾部调用。
对于解析表达式,请查看运算符优先级解析,例如http://epaperpress.com/oper/download/OperatorPrecedenceParsing.pdf。它使用数据堆栈在一个简单的循环中解析表达式。200个嵌套圆括号所需的唯一空间是数据堆栈中的200个条目。
有些语言可以在运行时添加新的运算符,编译后的程序会指定这些运算符的关联性和优先级,这是递归解析器无法处理的。
- 需要为 C++ 中的以下问题设计递归算法
- 这种用于查找连续子数组中最大和的递归算法有什么优势吗?
- 运行合并排序递归算法时EXC_BAD_ACCESS错误
- 如何改进搜索二项式系数的递归算法
- 如何转换多次调用自己的递归算法?
- 如何将字符串保存在最长的常见子序列递归算法中
- 编写递归算法以从链表中删除元素.编写递归算法以将元素添加到链表中
- 指针似乎迷失在递归算法中
- 如何从递归算法返回节点
- 从递归算法到迭代算法
- 可传递值影响递归算法的渐近时间复杂性
- 我的递归算法中的问题,用于查找所有最短、唯一的路径
- 带有两个递归调用的递归算法的时间复杂性
- 这种递归算法有什么问题?
- C++递归算法中抛出异常
- 将条件递归算法转换为迭代算法
- 递归算法中的分段错误
- printbinary递归算法
- 迭代等价于递归算法
- O(n^m) 复杂度的递归算法