我使用简单的堆栈算法开发了一个方程解析器,它将处理二进制(+, - ,|,&,*,/等)运算符,一元(!)运算符和括号.
但是,使用这种方法会让我拥有相同优先级的所有内容 - 无论操作符如何,都会从左到右进行评估,尽管可以使用括号强制执行优先级.
所以现在"1 + 11*5"会返回60,而不是人们所期望的56.
虽然这适用于当前项目,但我希望有一个通用例程,我可以用于以后的项目.
编辑清晰:
解析具有优先级的方程的好算法是什么?
我对一些简单的实现感兴趣,并且理解我可以自己编写代码来避免使用可用代码的许可问题.
语法:
我不明白语法问题 - 我是手写的.这很简单,我认为不需要YACC或Bison.我只需要用诸如"2 + 3*(42/13)"之类的方程计算字符串.
语言:
我在C中这样做,但我对算法感兴趣,而不是语言特定的解决方案.C足够低,如果需要,很容易转换成另一种语言.
代码示例
我发布了上面讨论的简单表达式解析器的测试代码.项目要求发生了变化,因此我从不需要优化性能或空间代码,因为它没有包含在项目中.它是原始的详细形式,应该易于理解.如果我在运算符优先级方面做了更多的事情,我可能会选择宏hack,因为它简单地匹配程序的其余部分.但是,如果我在一个真实的项目中使用它,我将寻求一个更紧凑/更快速的解析器.
相关问题
数学解析器的智能设计?
-亚当
该调度场算法是这样的工具.维基百科对此非常困惑,但基本上算法的工作原理如下:
比如说,你要评估1 + 2*3 + 4.直观地说,你"知道"你必须先做2*3,但你怎么得到这个结果?关键是要意识到当你从左到右扫描字符串时,你将评估一个运算符,当它跟随它的运算符具有较低(或等于)的优先级时.在示例的上下文中,这是您要执行的操作:
看看:1 + 2,什么都不做.
现在看1 + 2*3,仍然没有做任何事情.
现在看1 + 2*3 + 4,现在你知道必须评估2*3,因为下一个运算符的优先级较低.
你是如何实现的?
您希望有两个堆栈,一个用于数字,另一个用于运算符.你总是把数字推到堆栈上.您将每个新运算符与堆栈顶部的运算符进行比较,如果堆栈顶部的运算符具有更高的优先级,则将其从运算符堆栈中弹出,从数字堆栈中弹出操作数,应用运算符并推送结果到数字堆栈.现在重复与堆栈运算符顶部的比较.
回到这个例子,它的工作原理如下:
N = [] Ops = []
读1. N = [1],Ops = []
阅读+.N = [1],Ops = [+]
读2. N = [1 2],Ops = [+]
读*
.N = [1 2],Ops = [+*]
读3. N = [1 2 3],Ops = [+*]
阅读+.N = [1 2 3],Ops = [+*]
弹出3,2并执行2 *
3,并将结果推到N. N = [1 6],Ops = [+]
+
是左关联的,所以你想要关闭1和6并执行+.N = [7],Ops = [].
最后将[+]推到操作员堆栈上.N = [7],Ops = [+].
阅读4. N = [7 4].Ops = [+].
你输掉了输入,所以你现在要清空堆栈.你将得到结果11.
在那里,这不是那么困难,是吗?并且它不会调用任何语法或解析器生成器.
你想要一个递归下降解析器.
要获得优先权,您需要递归思考,例如,使用您的示例字符串,
1+11*5
要手动执行此操作,您必须阅读1
,然后查看加号并启动一个全新的递归解析"会话",以11
... 开头,并确保将其解析11 * 5
为自己的因子,生成一个解析树1 + (11 * 5)
.
即使尝试解释,这一切都感到非常痛苦,特别是在C的额外无能为力的情况下.在解析11之后,如果*实际上是+而不是,你将不得不放弃尝试制定一个术语而是解析11
本身就是一个因素.我的脑袋已经爆炸了.这有可能采用递归的体面策略,但还有更好的方法......
如果您使用像Bison这样的GPL工具,您可能不需要担心许可问题,因为Bison生成的C代码不在GPL中(IANAL,但我很确定GPL工具不会强制使用GPL)生成代码/二进制文件;例如,Apple编译代码,例如Aperture与GCC,并且他们出售它而不必使用GPL代码).
下载Bison(或类似的东西,ANTLR等).
通常有一些示例代码可以运行bison并获得所需的C代码,演示这四个函数计算器:
http://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html
查看生成的代码,看看这并不像听起来那么容易.此外,使用像Bison这样的工具的优点是1)你学到了一些东西(特别是如果你读了龙书并学习语法),2)你避免NIH试图重新发明轮子.使用真正的解析器生成器工具,您实际上希望稍后扩展,向其他人展示解析器是解析工具的域.
更新:
这里的人提供了很多合理的建议.我反对跳过分析工具或只用调车场算法或手滚递归体面解析器唯一的警告是,小玩具语言1可能在某一天变成大实际语言与功能(SIN,COS,日志)和变量,条件和循环.
对于一个简单的小型解释器来说,Flex/Bison可能会有点过分,但是当需要进行更改或需要添加功能时,一个解析器+评估器可能会导致故障.你的情况会有所不同,你需要用你的判断; 只是不要为了你的罪 而惩罚别人[2]并建立一个不够充分的工具.
我最喜欢的解析工具
世界上最好的工具是Parsec库(用于递归代码解析器),它带有编程语言Haskell.它看起来很像BNF,或者像解析的一些专用工具或领域特定语言(示例代码[3]),但它实际上只是Haskell中的常规库,这意味着它在与其余的相同的构建步骤中编译你的Haskell代码,你可以编写任意Haskell代码并在你的解析器中调用它,你可以在同一个代码中混合和匹配其他库.(顺便说一句,在Haskell之外的语言中嵌入这样的解析语言会导致语法残留.我在C#中做了这个并且它运行得很好但是它不是那么漂亮和简洁.)
笔记:
1 Richard Stallman说,为什么你不应该使用Tcl
Emacs的主要教训是,扩展语言不应仅仅是"扩展语言".它应该是一种真正的编程语言,专为编写和维护实质性程序而设计.因为人们会想要那样做!
[2]是的,我永远不会使用那种"语言".
另外请注意,当我提交此条目时,预览是正确的,但是不太合适的解析器在第一段上吃了我的关闭锚标记,证明解析器不是一件小事,因为如果你使用正则表达式而且一个黑客攻击你可能会得到一些微妙而小的错误.
[3]使用Parsec的Haskell解析器的片段:一个四函数计算器,扩展了指数,括号,用于乘法的空格和常量(如pi和e).
aexpr = expr `chainl1` toOp
expr = optChainl1 term addop (toScalar 0)
term = factor `chainl1` mulop
factor = sexpr `chainr1` powop
sexpr = parens aexpr
<|> scalar
<|> ident
powop = sym "^" >>= return . (B Pow)
<|> sym "^-" >>= return . (\x y -> B Pow x (B Sub (toScalar 0) y))
toOp = sym "->" >>= return . (B To)
mulop = sym "*" >>= return . (B Mul)
<|> sym "/" >>= return . (B Div)
<|> sym "%" >>= return . (B Mod)
<|> return . (B Mul)
addop = sym "+" >>= return . (B Add)
<|> sym "-" >>= return . (B Sub)
scalar = number >>= return . toScalar
ident = literal >>= return . Lit
parens p = do
lparen
result <- p
rparen
return result
http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
对不同方法的非常好的解释:
递归下降识别
调车场算法
经典的解决方案
优先攀登
用简单的语言和伪代码编写.
我喜欢'优先攀登'之一.
这里有一篇很好的文章,关于将简单的递归下降解析器与运算符优先级解析相结合.如果您最近一直在编写解析器,那么阅读它应该非常有趣和有益.
很久以前,我编写了自己的解析算法,在任何解析书中都找不到(比如Dragon Book).看看Shunting Yard算法的指针,我确实看到了相似之处.
大约2年前,我在http://www.perlmonks.org/?node_id=554516上发了一篇关于Perl源代码的文章..移植到其他语言很容易:我做的第一个实现是在Z80汇编程序中.
它是使用数字直接计算的理想选择,但如果必须,可以使用它来生成解析树.
更新因为更多人可以阅读(或运行)Javascript,所以在重新组织代码之后,我已经在Javascript中重新实现了我的解析器.整个解析器不到5k的Javascript代码(解析器大约100行,包装函数大约15行),包括错误报告和注释.
您可以在http://users.telenet.be/bartl/expressionParser/expressionParser.html上找到现场演示.
// operator table
var ops = {
'+' : {op: '+', precedence: 10, assoc: 'L', exec: function(l,r) { return l+r; } },
'-' : {op: '-', precedence: 10, assoc: 'L', exec: function(l,r) { return l-r; } },
'*' : {op: '*', precedence: 20, assoc: 'L', exec: function(l,r) { return l*r; } },
'/' : {op: '/', precedence: 20, assoc: 'L', exec: function(l,r) { return l/r; } },
'**' : {op: '**', precedence: 30, assoc: 'R', exec: function(l,r) { return Math.pow(l,r); } }
};
// constants or variables
var vars = { e: Math.exp(1), pi: Math.atan2(1,1)*4 };
// input for parsing
// var r = { string: '123.45+33*8', offset: 0 };
// r is passed by reference: any change in r.offset is returned to the caller
// functions return the parsed/calculated value
function parseVal(r) {
var startOffset = r.offset;
var value;
var m;
// floating point number
// example of parsing ("lexing") without aid of regular expressions
value = 0;
while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
if(r.string.substr(r.offset, 1) == ".") {
r.offset++;
while("0123456789".indexOf(r.string.substr(r.offset, 1)) >= 0 && r.offset < r.string.length) r.offset++;
}
if(r.offset > startOffset) { // did that work?
// OK, so I'm lazy...
return parseFloat(r.string.substr(startOffset, r.offset-startOffset));
} else if(r.string.substr(r.offset, 1) == "+") { // unary plus
r.offset++;
return parseVal(r);
} else if(r.string.substr(r.offset, 1) == "-") { // unary minus
r.offset++;
return negate(parseVal(r));
} else if(r.string.substr(r.offset, 1) == "(") { // expression in parens
r.offset++; // eat "("
value = parseExpr(r);
if(r.string.substr(r.offset, 1) == ")") {
r.offset++;
return value;
}
r.error = "Parsing error: ')' expected";
throw 'parseError';
} else if(m = /^[a-z_][a-z0-9_]*/i.exec(r.string.substr(r.offset))) { // variable/constant name
// sorry for the regular expression, but I'm too lazy to manually build a varname lexer
var name = m[0]; // matched string
r.offset += name.length;
if(name in vars) return vars[name]; // I know that thing!
r.error = "Semantic error: unknown variable '" + name + "'";
throw 'unknownVar';
} else {
if(r.string.length == r.offset) {
r.error = 'Parsing error at end of string: value expected';
throw 'valueMissing';
} else {
r.error = "Parsing error: unrecognized value";
throw 'valueNotParsed';
}
}
}
function negate (value) {
return -value;
}
function parseOp(r) {
if(r.string.substr(r.offset,2) == '**') {
r.offset += 2;
return ops['**'];
}
if("+-*/".indexOf(r.string.substr(r.offset,1)) >= 0)
return ops[r.string.substr(r.offset++, 1)];
return null;
}
function parseExpr(r) {
var stack = [{precedence: 0, assoc: 'L'}];
var op;
var value = parseVal(r); // first value on the left
for(;;){
op = parseOp(r) || {precedence: 0, assoc: 'L'};
while(op.precedence < stack[stack.length-1].precedence ||
(op.precedence == stack[stack.length-1].precedence && op.assoc == 'L')) {
// precedence op is too low, calculate with what we've got on the left, first
var tos = stack.pop();
if(!tos.exec) return value; // end reached
// do the calculation ("reduce"), producing a new value
value = tos.exec(tos.value, value);
}
// store on stack and continue parsing ("shift")
stack.push({op: op.op, precedence: op.precedence, assoc: op.assoc, exec: op.exec, value: value});
value = parseVal(r); // value on the right
}
}
function parse (string) { // wrapper
var r = {string: string, offset: 0};
try {
var value = parseExpr(r);
if(r.offset < r.string.length){
r.error = 'Syntax error: junk found at offset ' + r.offset;
throw 'trailingJunk';
}
return value;
} catch(e) {
alert(r.error + ' (' + e + '):\n' + r.string.substr(0, r.offset) + '<*>' + r.string.substr(r.offset));
return;
}
}
如果您可以描述当前用于解析的语法,将会有所帮助.听起来问题可能就在那里!
编辑:
您不理解语法问题以及"您手动编写"的事实很可能解释了为什么您遇到"1 + 11*5"形式的表达式问题(即,运算符优先级) .例如,谷歌搜索"算术表达式的语法"应该会产生一些好的指针.这样的语法不需要复杂:
::= + | - | ::= * | / | ::= x | y | ... | ( ) | - |
例如,它可以做到这一点,并且可以通过简单的增强来处理一些更复杂的表达式(包括例如函数或幂,......).
我建议你看看这个帖子,例如.
几乎所有语法/解析的介绍都以算术表达式为例.
请注意,使用语法并不意味着使用特定工具(la yacc,Bison,...).实际上,你肯定已经使用了以下语法:
:: | :: + | - | * | / :: | ( )
(或类似的东西)不知道它!
你有没有想过使用Boost Spirit?它允许你用C++编写类似EBNF的语法,如下所示:
group = '(' >> expression >> ')';
factor = integer | group;
term = factor >> *(('*' >> factor) | ('/' >> factor));
expression = term >> *(('+' >> term) | ('-' >> term));
当你提出问题时,不需要任何递归.答案是三件事:Postfix符号加Shunting Yard算法加Postfix表达式评估:
1).Postfix符号=发明以消除对显式优先级规范的需要.在网上阅读更多内容,但这里是它的要点:中缀表达式(1 + 2)*3,同时人们很容易阅读和处理通过机器计算效率不高.什么是?简单的规则,即"通过优先级缓存重写表达式,然后始终从左到右处理".因此,中缀(1 + 2)*3成为后缀12 + 3*.POST因为操作符总是放在操作数之后.
2).评估后缀表达式.简单.从后缀字符串中读取数字.将它们推到堆叠上直到看到操作员.检查操作员类型 - 一元吗?二进制?第三?根据需要弹出尽可能多的操作数以评估此运算符.评估.将结果推回堆栈!你几乎完成了.继续这样做,直到堆栈只有一个条目=值你寻找.
让我们做(1 + 2)*3,后缀是"12 + 3*".读取第一个数字= 1.将其推入堆栈.读下一个.Number = 2.将其推入堆栈.读下一个.操作员.哪一个?+.哪一种?二进制=需要两个操作数.弹出堆栈两次= argright为2,argleft为1. 1 + 2为3.将3推回堆栈.从postfix字符串开始阅读.它的数量.3.Push.读下一个.操作员.哪一个?*.哪一种?Binary =需要两个数字 - > pop stack两次.首先进入argright,第二次进入argleft.评估操作 - 3次3是9.Push 9在堆栈上.阅读下一个postfix char.它是空的.输入结束.Pop stack onec =这是你的答案.
3).Shunting Yard用于将人(易)可读的中缀表达式转换为后缀表达式(在某些练习后人类也很容易阅读).易于手动编码.见上面的评论和网.