中缀计算器表达式解析器

3

如何解析和评估中缀计算器语法中的表达式?我想到了两种方法。

第一种方法涉及使用两个栈。一个用于数字,另一个用于操作符,并且我将评估操作符优先级和关联以确定如何评估表达式。

第二种方法涉及将中缀表达式转换为后缀表达式,但我不知道该如何去做。这只是一个想法。目前,我设置了我的程序,打算使用第一种方法。

#include <iostream>
#include <string>
#include <sstream>
using namespace std;

bool die(const string &msg);

//stack class
class Stack{
public:
    Stack();
    void push(const double &val);
    void push(const string &oper);
    double popnum();
    string popop();
    double getopele();
    double getnumele();
private:
    static const unsigned MAX=30;
    string opstack[MAX];
    double numstack[MAX];
    unsigned opele;
    unsigned numele;
};

//operator type
struct OP{
    string name;
    void * func;
    unsigned arity;
    unsigned prec;
    bool lass;
    string descrip;
};
//operator table
OP op[]={{"+", add, 2, 4, true, "2+3 is 5"},
        {"-", subtract, 2, 4, true, "2-3 is -1"},
        {"*", multiply, 2, 6, true, "2*3 is 6"},
        {"/", divide, 2, 6, true, "2/3 is 0.666666..., div by 0 illegal"}};
unsigned OPELE =sizeof(op)/sizeof(op[0]);

//operators
bool add(double &r, double &x, double &y);
bool subtract(double &r, double &x, double &y);
bool multiply(double &r, double &x, double &y);
bool divide(double &r, double &x, double &y);

//Manip
unsigned findindex(string token, OP op[], unsigned OPELE);
bool parse(double &t, const string &token);
bool evaluate(double &result, string line);
bool weird(double x);

int main(){
    for(string line; getline(cin, line);){
        if(line=="QUIT") break;
        if(line.empty()) continue;
        if(line=="DOC")
            for(unsigned i=0; i<OPELE; i++)
                cout<<op[i].name<<" | "<<op[i].descrip<<'\n';
        double result;
        if(evaluate(result, line)){
            cout<<result<<'\n';
        }else{
            cout<<"Could not understand input\n\n";
        }
    }
}

Stack::Stack(){
    opele=0;
    numele=0;
}

void Stack::push(const double &val){
    if(MAX) die("Stack Overflow");
    numstack[numele++]=val;
}

void Stack::push(const string &oper){
    if(MAX) die("Stack Overflow");
    opstack[opele++]=oper;
}

double Stack::popnum(){
    if(!numele) die("Stack Underflow");
    return numstack[--numele];
}

string Stack::popop(){
    if(!opele) die("Stack Underflow");
    return opstack[--opele];
}

double Stack::getopele(){
    return opele;
}

double Stack::getnumele(){
    return numele;
}

bool add(double &r, double &x, double &y){
    double t = x + y;
    if( weird(t) )  return false;
    r = t;
    return true;
}

bool subtract(double &r, double &x, double &y){
    double t = x - y;
    if( weird(t) )  return false;
    result = t;
    return true;
}

bool multiply( double & r, double& x, double &y ){
    double t = x * y;
    if( weird(t) )  return false;
    result = t;
    return true;
}

bool divide( double & result, double &x, double &y ){
    double t = x / y;
    if( weird(t) )  return false;
    result = t;
    return true;
}

unsigned findindex(string token, OP op[], unsigned OPELE){
    for(unsigned i=0l i<OPELE; i++)
        if(op[i].name==token)
            return i;
    return UINT_MAX;

}

bool parse(double &t, const string &token){
    istringstream sin( token );
    double t;
    if( !(sin >>t) )  return false;
    char junk;
    if( sin >>junk )  return false;
    value = t;
    return true;
}

bool evaluate(double &result, string line){
    istringstream sin(line);
    Stack s;
    for(string token; sin>>token;){
        double t;
        if(parse(t, token)){
            s.push(t);
        }else if(
    }
}

bool weird( double x ){
    return  x != x || x != 0 && x == 2*x;
}

2
你可能会对逆波兰表达式转换算法或使用Pratt解析器的自顶向下运算符优先级解析感兴趣。 - Jon Purdy
1
您还可以查看GNU Bison及其文档。该教程将教您很多关于LALR解析器的知识。 - Alexandre C.
2个回答

9
这将是一篇长文,但无论如何,我将与您分享我用于解析中缀表达式并将其存储为二叉树的算法。不是堆栈,而是二叉树。解析它将轻松得到后缀顺序。我不会说这是最好的算法,但对于我的脚本语言来说,这很有效。
算法:
我们有一个方法,它处理二叉树的“当前节点”和“当前表达式”。节点包含“数据”字段和“类型”字段。
第一阶段:简单的东西,例如“4”直接进入节点,我们指定类型为“DATA”,即按原样使用此信息。
第二阶段:现在,让我们考虑以下表达式:
a) 2 + 3

这将被转化为以下二叉树:

  +
 / \
2   3

因此,运算符进入节点,操作数进入叶子。将表达式a)转换为树相当简单:找到运算符,将其放入树的“当前”节点中,指定节点类型为运算符“PLUS”,左侧的内容进入节点的左侧部分,右侧的内容进入右侧树中。很好很简单,使用阶段1中的信息,两个叶子将成为具有值2和3的“DATA”叶子。
第三阶段:但对于更复杂的表达式:
b) 2 * 3 + 4

树将会是:

这棵树将会:

    +
   / \ 4
  *
 / \ 
2   3

因此,我们需要修改上述算法如下:查找具有最高优先级的第一个运算符(考虑c++指南... +(加)和-(减)的优先级为6,而*(乘),/(除)和%(取模)的优先级为5),将表达式分成两部分(在具有最高优先级的操作数之前和之后),并递归调用该方法来处理这两部分,同时将具有最高优先级的运算符放置在当前节点中。因此,我们创建了一棵带有数据的树:

    +
   / \ 
  /  call method with "4"
call method with "2*3"

在这个阶段,我们针对“2*3”使用“第二阶段”来处理调用,并针对“4”使用“第一阶段”处理。

阶段4:如果表达式中有括号呢?例如

c) 2 * (3 + 4)

这将给我们一棵树:
      *
     / \
    2   +
       / \
      3   4

我们修改算法如下:
  • 当当前表达式被括号包围时,从中去掉括号并重新开始算法。请注意,(2 + 3 * 4 + 5) 被认为被括号包围而 (2+3)*(4+5) 不被认为。因此,需要计算括号的数量,而不仅仅是表达式的起始和结束字符。(这是一个递归方法,不要害怕第一步...)
  • 现在,找到括号外第一个具有最高优先级的运算符。同样地,取表达式的左右两侧并重复调用该方法,直到到达“阶段1”,即单个数据元素。

    现在这是一个由普通数字和运算符组成的表达式算法。对于更复杂的信息,您可能需要改进它以适应自己的需求。如果您认为值得考虑,请查看https://sourceforge.net/p/nap-script/mercurial/ci/default/tree/compiler/interpreter.cpp。其中包含关于更复杂概念(变量、方法调用、后缀/前缀运算符等)的上述算法的完整实现(使用C编写),其方法是 build_expr_tree,从第1327行开始。


我其实正在考虑如果出现括号的情况下将整个程序递归。我很可能会实现这一点。我必须说我真的很喜欢这种方法。 - Painguy

4

递归下降方法是手动实现正确表达式解析器的最简单方式。在这里,编程语言堆栈执行与您尝试使用的显式堆栈相同的操作。可以通过谷歌找到许多RD示例,并且任何好的编译器书籍都会有一些。

链接的维基百科页面显示了一个解析器,但没有说明如何添加评估。因此,以下是C中完整的基本表达式求值器。它可以很容易地包装在一个C++类中,全局变量变为实例变量。它缺少生产系统中需要的功能。例如,当它发现错误时,它只是退出。 C++异常将轻松允许您取消递归并继续。它还需要保护以防止数值溢出,除以零等,您显然知道如何做。

递归下降的思想是将所需语言的语法转换为称为LL(1)的形式。完成后,有固定的规则-保证每次都能正常工作-用于将语法规则转换为过程。我已经手动完成了以下操作。还有自动执行此操作的工具。

所以这个评估器非常容易扩展。只需添加必要的语法规则,然后实现所需的扫描器、解析器和评估代码增强即可。例如,一个内置函数规则将是unsigned_factor -> FUNCTION_NAME ( expr ),其中扫描器将所有函数名称识别为相同的标记,而unsigned_factor C函数被增强以解析和计算值。
我必须包含一个小型扫描器才能得到一个工作程序。注意超过一半的代码是扫描器。基本的RD解析器很简单。
如果添加错误恢复功能,则它们会变得更加复杂:智能跳过错误并继续解析,同时仅发出一个精确措辞的错误消息。但是,这会给任何解析器增加很多复杂性。
// Bare bones scanner and parser for the following LL(1) grammar:
// expr -> term { [+-] term }     ; An expression is terms separated by add ops.
// term -> factor { [*/] factor } ; A term is factors separated by mul ops.
// factor -> unsigned_factor      ; A signed factor is a factor, 
//         | - unsigned_factor    ;   possibly with leading minus sign
// unsigned_factor -> ( expr )    ; An unsigned factor is a parenthesized expression 
//         | NUMBER               ;   or a number
//
// The parser returns the floating point value of the expression.

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

// The token buffer. We never check for overflow! Do so in production code.
char buf[1024];
int n = 0;

// The current character.
int ch;

// The look-ahead token.  This is the 1 in LL(1).
enum { ADD_OP, MUL_OP, LEFT_PAREN, RIGHT_PAREN, NUMBER, END_INPUT } look_ahead;

// Forward declarations.
void init(void);
void advance(void);
double expr(void);
void error(char *msg);

// Parse expressions, one per line. 
int main(void)
{
  init();
  while (1) {
    double val = expr();
    printf("val: %f\n", val);
    if (look_ahead != END_INPUT) error("junk after expression");
    advance();  // past end of input mark
  }
  return 0;
}    

// Just die on any error.
void error(char *msg)
{
  fprintf(stderr, "Error: %s. I quit.\n", msg);
  exit(1);
}

// Buffer the current character and read a new one.
void read()
{
  buf[n++] = ch;
  buf[n] = '\0';  // Terminate the string.
  ch = getchar();
}

// Ignore the current character.
void ignore()
{
  ch = getchar();
}

// Reset the token buffer.
void reset()
{
  n = 0;
  buf[0] = '\0';
}

// The scanner.  A tiny deterministic finite automaton.
int scan()
{
  reset();
START:
  switch (ch) {
    case ' ': case '\t': case '\r':
      ignore();
      goto START;

    case '-': case '+':
      read();
      return ADD_OP;

    case '*': case '/':
      read();
      return MUL_OP;

    case '(':
      read();
      return LEFT_PAREN;

    case ')':
      read();
      return RIGHT_PAREN;

    case '0': case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': case '9':
      read();
      goto IN_LEADING_DIGITS;

    case '\n':
      ch = ' ';    // delayed ignore()
      return END_INPUT;

    default:
      error("bad character");
  }

IN_LEADING_DIGITS:
  switch (ch) {
    case '0': case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': case '9':
      read();
      goto IN_LEADING_DIGITS;

    case '.':
      read();
      goto IN_TRAILING_DIGITS;

    default:
      return NUMBER;
  }

IN_TRAILING_DIGITS:
  switch (ch) {
    case '0': case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': case '9':
      read();
      goto IN_TRAILING_DIGITS;

    default:
      return NUMBER;
  }          
}

// To advance is just to replace the look-ahead.
void advance()
{
  look_ahead = scan();
}

// Clear the token buffer and read the first look-ahead.
void init()
{
  reset();
  ignore(); // junk current character
  advance();
}

double unsigned_factor()
{
  double rtn = 0;
  switch (look_ahead) {
    case NUMBER:
      sscanf(buf, "%lf", &rtn);
      advance();
      break;

    case LEFT_PAREN:
      advance();
      rtn = expr();
      if (look_ahead != RIGHT_PAREN) error("missing ')'");
      advance();
      break;

    default:
      error("unexpected token");
  }
  return rtn;
}

double factor()
{
  double rtn = 0;
  // If there is a leading minus...
  if (look_ahead == ADD_OP && buf[0] == '-') {
    advance();
    rtn = -unsigned_factor();
  }
  else
    rtn = unsigned_factor();
  return rtn;
}

double term()
{
  double rtn = factor();
  while (look_ahead == MUL_OP) {
    switch(buf[0]) {
      case '*':
        advance(); 
        rtn *= factor(); 
        break; 

      case '/':
        advance();
        rtn /= factor();
        break;
    }
  }
  return rtn;
}

double expr()
{
  double rtn = term();
  while (look_ahead == ADD_OP) {
    switch(buf[0]) {
      case '+': 
        advance();
        rtn += term(); 
        break; 

      case '-':
        advance();
        rtn -= term();
        break;
    }
  }
  return rtn;
}

运行程序:

1 + 2 * 3
val: 7.000000
(1 + 2) * 3
val: 9.000000

1
抱歉回复晚了。这绝对是一种有趣的完成工作的方式。它似乎不那么复杂,看起来可以完成工作。我完成手头的事情后可能会尝试一下。 :) 谢谢 - Painguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接