【问题标题】:How do you write a recursive function using a non-recursive stack?如何使用非递归堆栈编写递归函数?
【发布时间】:2014-04-20 00:43:26
【问题描述】:

为了尝试在 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 时,它从根表达式开始(与它同名,因此是“数学”)。然后它遍历每个匹配器,然后是每个标记,如果标记是表达式,则递归。基本上这个(解析器将跟踪它匹配模式的字符串的偏移量/位置;这只是伪代码):

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 循环,但在 for 循环中它大部分时间调用一个函数,所以它结束了几乎一样)。

问题是,你如何“把它弄平”?你不做递归,而是如何做到这一点,所以它基本上是这样的:

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

我不是在寻找如何为这个特定的 PEG 问题实施解决方案的细节,我只是在寻找以非递归方式跟踪递归状态的一般方式。

【问题讨论】:

  • 在某种传递对象(如new Context(grammar))中,您似乎必须分别跟踪每种对象的深度。所以它将具有属性expressionStackmatcherStacktokenStack,并跟踪每个属性的索引。但这似乎很复杂,所以不确定是不是这样......
  • 最通用的解决方案是从 Javascript 到 Javascript 的源到源编译器,它将输入程序转换为带有蹦床的连续传递样式。我不知道是否有人写过这样的野兽。
  • 这看起来也很复杂 :)。这是我到目前为止所拥有的,但是(a)它开始变得更加复杂到难以推理的程度,并且(b)它使得处理边缘情况变得更加困难。 github.com/parsejs/grammar/blob/master/lib/context.js
  • 对比带有递归的旧版本:github.com/parsejs/grammar/blob/…
  • @LancePollard 越展平它就越复杂。递归可以使用 Stack\Queue + Loop 来完成,构建一个语言和一个解析器,构建一个复杂的有限状态机,构建一个更复杂的复杂的图灵机

标签: javascript algorithm parsing recursion state-machine


【解决方案1】:

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

这与使用机器代码维护的堆栈非常相似,只是您不限于原始值,因此可以使事情变得更加整洁(在数据结构级别)。

如果您有时间,可以考虑编写(或使用其他人的)LR(1) 解析器。那些维护很少的系统堆栈并且比您的家庭滚动的 LL(k) 语法更好地处理语法中的许多邪恶案例。但是,它们的工作方式相当比你现在所拥有的更加神秘。

【讨论】:

    【解决方案2】:

    我只是在寻找您跟踪 以非递归方式递归状态。

    在堆栈(数组)中使用推送和弹出。
    如果你有 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
      }
    

    【讨论】:

    • 这可能是我第一次听到在同一个句子中使用 'more clear' 和 'goto' ;-) 如果你正确使用 while 循环并将守卫放在哪里可能会更干净它属于...while(n != 1) { ... } answer = 1;
    • @Heuster - 注意。只是想让你知道我当然想到了while(n != 1) { ..,但(对我来说)更像是 VBA 代码。
    猜你喜欢
    • 2021-04-19
    • 2017-06-18
    • 2023-03-27
    • 2019-09-30
    • 2018-10-03
    • 1970-01-01
    • 2017-09-12
    • 1970-01-01
    • 2018-05-28
    相关资源
    最近更新 更多