如何使用非递归堆栈编写递归函数?

时间:2014-04-20 00:43:26

标签: javascript algorithm parsing recursion state-machine

为了尝试在JavaScript中实现PEG而不会使旧版浏览器因堆栈溢出而崩溃,我想制作一个解析表达式语法,以非递归方式解析字符串。你怎么做到这一点?这让人心旷神怡。

假设您有这样的结构:

  • grammar有多个表达
  • expression有许多matchers
  • matcher有很多tokens(或者其他更好的词)
  • token可以指向另一个expression,也可以是原始字符串或正则表达式。因此,如果它指向另一个表达式,则这是递归开始的地方。

所以说你定义这样的层次结构:

var grammar = new Grammar('math');
var expression = grammar.expression;

expression('math')
  .match(':number', ':operator', ':number', function(left, operator, right){
    switch (operator) {
      case '+': return left + right;
      case '-': return left - right;
      case '*': return left * right;
      case '/': return left / right;
    }
  });

expression('number')
  .match(/\d+/, parseInt);

expression('operator')
  .match('+')
  .match('-')
  .match('*')
  .match('/');

var val = grammar.parse('6*8'); // 42

当你打电话给grammar.parse时,它会从根表达式开始(与你的名字相同,所以" math")。然后它遍历每个匹配器,然后遍历每个标记,如果标记是表达式,则递归。基本上这个(解析器将跟踪它匹配模式的字符串的偏移/位置;这只是伪代码):

function parse(str, expression, position) {
  var result = [];

  expression.matchers.forEach(function(matcher){
    matcher.tokens.forEach(function(token){
      var val;
      if (token.expression) {
        val = parse(str, token.expression, position);
      } else {
        val = token.parse(str, position);
      }
      if (val) result.push(val);
    });
  });

  return result;
}

parse('6*8', grammar.root, 0);

因此,对于像6*8这样的简单表达式,其递归非常少,但您可以快速找到具有多层嵌套的复杂表达式。再加上嵌套for循环的嵌套,堆栈变大(我实际上不使用forEach,我用于循环,但在for循环中它大部分时间调用函数,所以最终几乎是一样的。)

问题是,你怎么把这个弄平了?"?而不是做递归,你怎么做到这一点所以它基本上是这样的:

while (token = stack.pop()) {
  val = token.parse(val);
  if (val) result.push(val);
}

我没有寻找如何实现这个特定PEG问题的解决方案的细节,我更倾向于寻找以非递归方式跟踪递归状态的一般方法。

2 个答案:

答案 0 :(得分:1)

通常,您所做的是在代码中编写堆栈,并将“本地”变量放在“堆栈框架”上下文对象中,并保留在该堆栈中。然后,在您将进行“递归调用”的地方,存储当前堆栈帧并为新的当前上下文创建一个新堆栈帧。做“返回”只是扭转操作的问题。它并不是特别复杂,但它确实使代码有点混乱。唯一需要注意的是,当你完成解析表达式时,你会到达堆栈的底部(这样尾随令牌和丢失的令牌不会导致问题)。

这非常类似于机器代码中维护的堆栈所发生的情况,除非您不限于原始值,因此可以使事情更加整洁(在数据结构级别)。

如果你有时间,可以考虑写(或使用别人的)LR(1)解析器。那些维护很少的系统堆栈并且在语法中处理许多恶意案例比你的家庭卷LL(k)语法更好。然而,他们在工作方式上比现在更加神秘。

答案 1 :(得分:1)

  

我更倾向于寻找你追踪的一般方式   以递归方式递归状态。

在堆栈(数组)中使用推送和弹出。
如果你有goto的话会更容易。
VBA中的(阶乘)方法(因为goto而更清晰)。

Option Explicit
Sub Main()
  MsgBox fac(1)
  MsgBox fac(5)
End Sub
Function fac(n&)
  Dim answer&, level&, stackn&(99)
  level = 0
zentry:
  If n = 1 Then answer = 1: GoTo zreturn
  level = level + 1 ' push n
  stackn(level) = n
  n = n - 1 ' call fac(n-1)
  GoTo zentry
zreturn:
  If level = 0 Then fac = answer: Exit Function
  n = stackn(level) ' pop n
  level = level - 1
  answer = n * answer ' factorial
  GoTo zreturn
End Function

javascript中的相同方法。

console.log(fac(1));
console.log(fac(5));
function fac(n) { // non-recursive
  var answer, level; 
  var stackn = []; 
  level = 0;
  while (true) { // no goto's
    if (n == 1) { answer = 1; break; }
    level = level + 1; // push n
    stackn[level] = n;
    n = n - 1; } // call fac(n-1) 
  while (true) { // no goto's
    if (level == 0) { return answer; }
    n = stackn[level]; // pop n
    level = level - 1;
    answer = n * answer; } // factorial
  }