繁体   English   中英

递归下降:中缀到后缀转换重复运算符

[英]Recursive descent: infix to postfix conversion repeating operators

我们最近在 uni 的编程课程中了解到使用堆栈将中缀转换为后缀。 我有一段时间写我的解析器,所以我决定使用递归下降。 我正在关注: Recursive Descent Theodore Norvell 解析表达式 这是他使用的语法:

E --> P {B P}
P --> v | "(" E ")" | U P
B --> "+" | "-" | "*" | "/" | "^"
U --> "-" | "+"

我曾尝试在 C 中实现这一点并且它有效。 但是,如果我给它以下输入,操作员像这样一个接一个地尾随:

---+-1-(-(2-1)+3)*-2

它输出这个:

---+-1.00 -2.00 1.00  - 3.00  +  - -2.00 *

以下内容似乎是错误的:

  • - -2.00 *应该是+ -2 * - (基于我的堆栈实现)

我得到的另一个特殊结果是我得到的2+(2^4*(7+2^6))

2.00 2.00 4.00 ^ 7.00 2.00  + 6.00 ^* +

当我期望得到:

 2.00 2.00 4.00 ^ 7.00 2.00 6.00 ^ + * +

我不确定,但我是否需要一个优先级攀爬解析器- 在链接的文章中也有建议。 然而,主要问题是我将如何简化尾随对操作```---+``? 任何帮助将不胜感激。 提前非常感谢。 仍然是这一切的初学者。

这是代码:

#include <stdio.h>
#include <stdlib.h>

void expr();
void term();
void match(int t);
void error();
void parseNumber();
//E --> P {B P}
//P --> v | "(" E ")" | U P
//B --> "+" | "-" | "*" | "/" | "^"
//U --> "-" | "+"
//
// Erecognizer is
//    E()
//    expect( end )
// 
// E is
//     P
//     while next is a binary operator
//        consume
//        P

char lookahead;

int main() {
  lookahead = getchar();
  expr();
return 0;
}
// E is
//     P
//     while next is a binary operator
//        consume
//        P
void expr() {
  term();
 /* optimized by inlining rest() and removing recursive calls */
  while (1)  {
    if (lookahead == '+') {
      match('+');
      term();
      printf(" + ");
    } else if (lookahead == '-') {
      match('-');
      term();
      printf(" - ");
    }else if (lookahead == '*') {
      match('*');
      term();
      putchar('*');
    } else if (lookahead == '/') {
      match('/');
      term();
      putchar('/');
    } else if (lookahead == '^') {
      match('^');
      term();
      putchar('^');
    }  
    else break;
  }
}

// P is
//     if next is a v
//          consume
//     else if next = "("
//          consume
//          E
//          expect( ")" )
//     else if next is a unary operator
//          consume
//          P
//     else
//          error

void term() {
  if (isdigit(lookahead)) {
      parseNumber();
     // printf("lookahead at %c",lookahead);
  } else if(lookahead =='('){
      match('(');
      expr();
      match(')');
  }
  else if (lookahead =='-' ||lookahead =='+') {
      char sign = lookahead;
      match(lookahead);
      (sign=='+'?putchar('+'):putchar('-'));
      term();
  }else {
      error();
      }
}

void match(int t) {
  if (lookahead == t)
    lookahead = getchar();
  else error();

}
void parseNumber() {
  double number = 0;
  // TODO consume spaces
  if (lookahead == '\0'|| lookahead=='\n') return;
  while (lookahead >= '0' && lookahead <= '9') {
    number = number * 10 + lookahead - '0';
    match(lookahead);
  }
  if (lookahead == '.') {
    match(lookahead);
    double weight = 1;
    while (lookahead >= '0' && lookahead <= '9') {
      weight /= 10;
      number = number + (lookahead - '0') * weight;
      match(lookahead);
    }
  }
  printf("%.2f ", number);
  //printf("\ncurrent look ahead at after exiting parseNumber %c\n",lookahead);
}
void error() {
  printf("Syntax error at lookahead %c\n",lookahead);
  exit(1);
}

您引用的文章非常清楚地指出,提出的递归下降算法不是解析器:(强调添加)

让我们看一个基于这个语法的递归下降识别器。 我将此算法称为识别器而不是解析器,因为它所做的只是识别输入是否为语法语言。 它不会生成抽象语法树或任何其他形式的 output 表示输入内容。

这是绝对正确的; 该语法仅适用于识别器。 没有提到的是,如果您尝试修改算法以生成某种形式的 output(除了简单的“是”或“否”,表明表达式是否为目标语言),您将在结构上得到一个错误的答案。

那是因为这不是真的:

我们可以将 G 转换为等价的非左递归文法 G1……

或者至少,您需要非常小心“等效”的含义。 新语法是等效的,因为它识别相同的语言。 但它不会以相同的方式解析表达式,而且左递归消除算法会从语法中消除产生正确解析所必需的信息。 (在这种情况下,必要的信息——每个运算符的优先级和关联性——已经从文法中删除,大概是为了简化。但即使文法一开始是准确的,左递归删除也会被删除左结合运算符和右结合运算符之间的区别。)

在本演示文稿的稍后部分,在经典解决方案标题下,Norvell 描述了一个递归下降解析器,它可以正确解析表达式。 [注 1] 这可能是您想要编写的代码。

顺便说一句,您的 output 不是逆波兰表示法(并且没有括号也不是明确的),因为您在操作数之前是 output 一元运算符。 RPN 总是将运算符放在其操作数之后——这使得它在没有括号的情况下明确——并要求每个操作数明确指定它需要的操作数的数量。 这通常意味着以不同的方式编写一元和二元否定,以便可以将它们区分开来,尽管另一种选择是只给 output 一个额外的 0 操作数,并让 RPN 评估器将它们视为二元运算符。

但是 RPN 并不是一个非常有用的 output 从解析器。 来自解析器的常见 output 是抽象语法树,它是描述解析文本的句法结构的图结构。 另一种常见的 output 是所谓的“三地址码”,它是具有无限(或至少非常大)数量的寄存器的假想机器的虚拟机代码。 (并非所有的 VM 操作码都有三个地址,但其中很多都有,包括所有二进制算术运算符,它们命名两个源寄存器和一个目标寄存器。)当然,对于计算器,您可以像 go 一样计算产生任何结构化的表示。

笔记:

  1. 如果 Norvell 选择了一个不那么特殊的优先顺序,也许最好说语法 G2 将正确解析表达式。 我们通常将一元否定运算符放在乘法和取幂之间,而不是放在加法和乘法之间。 只要你只实现乘法和精确除法,Norvell 的优先级选择无关紧要,但如果你实现地板除法或取模(即//%的 Python 语义),你会发现低优先级一元否定会导致意想不到的评估。 这个错误是可能的,因为否定分布在乘法和精确除法上。 但是(-3) // 2-(3 // 2)不同, -3 // 2的预期结果是第一个,而 Norvell 的优先顺序产生第二个。

    我应该补充一点,C 中的 integer 除法是截断除法,而不是地板除法,并且 C 的%运算符是余数,而不是模数,因此 Z0D61F8370CAD1D412F80B84D1 的问题并不明显。 另一方面,C 缺少指数运算符,因此您可以使用 go 的更简单的解决方案,即给予一元否定比任何二元运算符更高的优先级,这就是 Z0D61F8370CAD1D4D412F80ZB84D1 所做的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM