如何解析包含括号的数学表达式

22

这不是一个学校作业或其他什么,但我意识到这是一个大多数属于学术性质的问题。但是,我一直在努力解析“数学”文本并得出答案。

例如-我可以弄清楚如何解析“5 + 5”或“3 * 5”,但当我尝试正确地链接操作时,我失败了。

(5 + 5) * 3

主要是让我烦恼的是我无法弄清楚它。如果有人能指点我方向,我将非常感激。

编辑 感谢所有快速回复。很抱歉我没有做得更好的解释。

首先-我没有使用正则表达式。我也知道已经有可用库,可以将数学表达式作为字符串输入,并返回正确的值。因此,我主要是在看这个,因为可悲的是,我“不理解”它。

其次-我尝试过的事情(可能是错误的),但我正在计算'('和')'并首先评估最深的项。在简单的示例中,这起作用;但我的代码不太好看,并且更复杂的东西会崩溃。当我“计算”最低级别时,我正在修改字符串。

所以... (5 + 5) * 3

将变成 10 * 3

然后会计算为 30

但这感觉很“不对”。

希望这有助于澄清事情。我一定会查看提供的链接。


2
你正在使用正则表达式吗?你目前是如何解析文本的? - Ryan Hayes
2
《龙书》是这些东西的优秀入门介绍。(http://en.wikipedia.org/wiki/Dragon_Book_(computer_science)) - user113476
Ronald Mak的书《Writing Compilers and Interpreters》。http://www.amazon.com/Writing-Compilers-Interpreters-Ronald-Mak/dp/0471113530 也是一个很好的资源。 - user113476
可能有帮助的线程... https://dev59.com/xnVD5IYBdhLWcg3wRpaX - user113476
这绝对不是一个“学术性”的问题——解析表达式是计算机科学和软件工程中的核心问题。 - Escualo
显示剩余6条评论
13个回答

16
很久以前,在制作一个简单的绘图应用程序时,我使用了这个算法(相当容易理解,并且对于这些简单的数学表达式非常有效),首先将表达式转换为逆波兰式(RPN),然后计算结果。 RPN在不同变量值下执行起来非常快速便捷。

当然,语言解析是一个非常广泛的主题,有许多其他方法可以处理它(也有预先制作的工具可用)。


这是一个小型计算器解析器的绝佳想法,可以快速实现。但如果您想执行稍微复杂一些的操作,比如函数调用(sincos),它可能会变得非常棘手。 - shoosh
2
@shoosh:实际上,函数调用可以很容易地实现为一元运算符(尽管维基百科页面似乎忽略了它们,但算法可以扩展以考虑它们)。对于多个参数,您可以引入一个二进制逗号运算符来打包值。 - Matti Virkkunen
除了高阶数学,还有哪些数学调用会在括号内包含多个参数?如果你正在对具有多个维度的内容进行解析器处理,那么我想你已经完成了一个更简单的解析器... 另外,(简单来说)sin( VALUE )VALUE ::= [paren-open] term [operator term] [paren-close]是吗? - jcolebrand
1
当然,你是正确的。但是当你想要将相同的函数名称重载以接受一个或两个参数时,真正的困难出现了。 - shoosh
2
@shoosh:如果你认为f(x, y)是应用于单个参数(x, y)的f函数,那么你就没问题了。你可以将这个参数看作由,运算符创建的元组(正如Matti所暗示的那样)。 - Joren

9

@Rising Star [我本来希望把这个作为评论添加,但格式失败了]

看起来可能有些违反直觉,但是二叉树既简单又灵活。在这种情况下,节点可以是常量(数字)或运算符。当您决定使用控制流和函数等元素扩展语言时,二叉树会使生活变得更加容易。

例如:

((3 + 4 - 1) * 5 + 6 * -7) / 2

                  '/'
                /     \
              +        2
           /     \
         *         *
       /   \     /   \
      -     5   6     -7
    /   \
   +     1
 /   \
3     4

在上述情况下,扫描器被编程为将“-”后面跟随一系列数字识别为单个数字,因此“-7”被作为“number”标记的值组件返回。如果“-”后跟空格,则会返回一个“minus”标记。这使得解析器的编写变得更加容易。但是对于想要使用“-(x * y)”的情况,它会失败,但您可以轻松地将表达式更改为“0-exp”。

复合模式的一个简单应用将把内部(复合)节点看作“二元运算符”,而叶节点则为“常量”。当然,这不再严格意义上是一棵二叉树。 - Andre Artus
我必须纠正自己: "-" 紧接着 "[0-9]" 变成数字,任何其他情况下的 "-" 都会作为减号标记返回。一个简单的正则表达式可以返回一系列 "标记":"(-?[0-9]+|[*+-/()]|[a-z][a-z0-9]+|<=|>=|<|>|=)"。它处理标识符和关系运算符。它将未指定的词素视为空格。 - Andre Artus

7

这是一个简单(操作符优先级较低)的语法,适用于您想要的内容。

expression = 
    term
    | expression "+" term
    | expression "-" term .
term = 
    factor
    | term "*" factor
    | term "/" factor .
factor = 
    number
    | "(" expression ")" .

当你处理“factor”时,只需检查下一个标记是否为数字或“(”,如果是“(”,则再次解析“expression”,当expression返回时,检查下一个标记是否为“)”。您可以通过使用outref参数将[计算|读取]的值冒泡到父级,或构建表达式树来实现。
以下是EBNF中相同的内容:
expression = 
    term
    { "+" term | "-" term  } .

term = 
    factor
    { "*" factor | "/" factor }.

factor = 
    number
    | "(" expression ")" .

5

对于任何在这篇文章发布九年后看到这个问题的人:如果你不想重复造轮子,那么有许多奇特的数学解析器可供选择。

我多年前用Java写了一个,它支持算术运算、方程求解、微积分、积分计算、基本统计、函数/公式定义、绘图等等。

它叫做ParserNG,而且是免费的。

评估表达式就像这样简单:

    MathExpression expr = new MathExpression("(34+32)-44/(8+9(3+2))-22"); 
    System.out.println("result: " + expr.solve());

    result: 43.16981132075472

或者使用变量和计算简单表达式:

 MathExpression expr = new MathExpression("r=3;P=2*pi*r;"); 
System.out.println("result: " + expr.getValue("P"));

或者使用函数:

MathExpression expr = new MathExpression("f(x)=39*sin(x^2)+x^3*cos(x);f(3)"); 
System.out.println("result: " + expr.solve());

result: -10.65717648378352

或者在给定点评估导数(注意:它在幕后进行符号微分(而不是数值微分),因此精度不受数值逼近误差的限制):

MathExpression expr = new MathExpression("f(x)=x^3*ln(x); diff(f,3,1)"); 
System.out.println("result: " + expr.solve());

 result: 38.66253179403897

在x=3处对 x^3 * ln(x) 进行一阶微分。 目前可微分的次数为1。

或者对于数值积分:

MathExpression expr = new MathExpression("f(x)=2*x; intg(f,1,3)"); 
System.out.println("result: " + expr.solve());

result: 7.999999999998261... approx: 8

这个解析器速度相当快,而且具有许多其他功能。

免责声明:ParserNG是由我编写的。


1
干得好@gbenroscience,我喜欢那个项目上的努力。 - 0x00001F
1
你不知道这有多鼓舞人心!非常感谢! - gbenroscience

4

1
仅提供链接的答案更适合作为评论。 - mickmackusa

2
你在学校里是否曾上过形式语言课程?实际上,你需要一种文法来进行解析。
编辑:天啊,维基百科说我错了,但现在我忘记了正确的名称 :( http://en.wikipedia.org/wiki/Formal_grammar

1
中缀表示法需要语法。后缀(逆波兰)可以使用下推自动机进行解析,这比语法实现要容易得多。 - andand
1
请注意,不要将(正式的)语法与无上下文语法等同起来。 一个正则语言也是由一种语法生成的,即正则语法。 - anno
@anno ~ 基本数学运算不是可以通过形式语法来描述吗?但是没错,最初我想到的是无上下文文法和自动机。 - jcolebrand

2
去年左右,我为了某些我已经记不清的原因编写了一个基本的数学计算器。它并不是真正意义上的“解析器”,而且……就像所有旧代码一样,现在我对它并不感到自豪。
但是你可以看一下,看看它是否能帮到你。
你可以通过启动这个独立的Java应用程序来运行一些输入测试。

2

当我想要解析某些内容时,我决定使用GOLD Parser:

  • 自包含文档(不需要书籍来理解)
  • 各种运行时引擎,包括我所需的编程语言。

该解析器包括示例语法,例如操作符优先级。


除了GOLD之外,还有其他更著名的解析器,例如ANTLR,但我没有使用过。


2

正如其他答案所述,问题在于您需要使用具有关联规则的递归分析器,因为您可能会遇到以下表达式:

val = (2-(2+4+(3-2)))/(2+1)*(2-1)

你的解析器需要知道以下内容:
  1. 括号表达式从内到外进行计算
  2. 除法优先于乘法(先除后乘)
  3. 乘法优先于加减法
可以想象,编写一个(好的)解析器是一门艺术。好消息是有一些称为"解析器生成器"的工具让你轻松定义语言的文法和解析规则。你可能需要查阅维基百科中关于"巴克斯诺尔范式(BNF)"的条目,以了解如何定义文法。
最后,如果你是为了学习而做这个,请继续。如果是为了生产代码,请不要重复造轮子,找一个现有的库,否则你可能会因为想要计算2+2而写出1000行的代码。

我同意第1点和第2点,但第3点似乎是不必要的,因为除法与乘以倒数(乘法逆元)相同(即a / b = a * 1/b),而乘法是可结合的[(a * b) * c = a * (b * c)]和可交换的[a * b = b * a]。这些属性连同分配律[a * (b + c) = (a * b) + (a * c)]经常用于优化表达式。 - Andre Artus
看起来我在评论中犯了一个错误: 1.括号表达式从内向外计算 3.乘法优先于加法/减法 我不同意的是第二项(除法优先于乘法)。 - Andre Artus
1
@Andre:但如果除法不比乘法优先,你怎么做这个:6/24?你怎么能用一个你没有值的东西乘以四呢?难道不是真的,你首先得到值6/2 = 3,然后34 = 12吗?正如你所提到的,“除法就是乘以倒数”,为了有倒数,你必须进行除法(否则,倒数在哪里?) - Escualo
1
我认为术语“precedence”不正确,因为它们具有相同的“operator precedence”,也许应该是“associativity”? - Escualo
1
@Arrieta:你说得对,这通常可以通过关联性解决:加、减、乘、除操作符通常是左关联的。因此,6/24本质上被解析为(6/2)4;46/2被解析为(46)/2。如果你将“^”定义为指数运算符,则使用右关联(递归),例如:2^3^4 --> 2^(3^4)。 - Andre Artus

1

本质上,您正在询问我们如何编写“解析器”。这里有另一个关于解析器的Stack Overflow问题:手动编写解析器


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