Math 基于堆栈的数学解析表达式求值的效率
出于学术目的,我必须编写一个应用程序来绘制用户输入表达式,如:f(x)=1-exp(3^(5*ln(cosx))+x) 我选择的编写解析器的方法是使用分流码算法转换RPN中的表达式,将“cos”等基本函数视为一元运算符。这意味着上面编写的函数将转换为一系列令牌,如:Math 基于堆栈的数学解析表达式求值的效率,math,parsing,performance,rpn,shunting-yard,Math,Parsing,Performance,Rpn,Shunting Yard,出于学术目的,我必须编写一个应用程序来绘制用户输入表达式,如:f(x)=1-exp(3^(5*ln(cosx))+x) 我选择的编写解析器的方法是使用分流码算法转换RPN中的表达式,将“cos”等基本函数视为一元运算符。这意味着上面编写的函数将转换为一系列令牌,如: 1, x, cos, ln, 5, *,3, ^, exp, - 问题是,为了绘制函数,我必须对其进行多次求值,因此对每个输入值应用堆栈求值算法将非常低效。 我怎样才能解决这个问题?我必须忘记RPN思想吗?为什么不围绕解析树(我松
1, x, cos, ln, 5, *,3, ^, exp, -
问题是,为了绘制函数,我必须对其进行多次求值,因此对每个输入值应用堆栈求值算法将非常低效。
我怎样才能解决这个问题?我必须忘记RPN思想吗?为什么不围绕解析树(我松散地使用“树”,在您的例子中,它是一系列操作),并相应地标记输入变量?(例如,对于输入x、y、z等,用0注释“x”表示第一个输入变量,用1注释“y”表示第二个输入变量,等等) 这样,您就可以一次性解析表达式,保留解析树,接收输入数组,并应用解析树进行计算
如果您担心评估步骤(相对于解析步骤)的性能方面,我认为您不会做得更好,除非您开始矢量化(立即对输入向量应用解析树)或将操作硬编码为固定函数。在什么意义上效率低下?有机器时间和程序员时间。是否有一个标准来衡量它在特定复杂程度下的运行速度?完成任务并继续下一个任务是否更重要(完美主义者有时永远不会完成) 所有这些步骤都必须针对每个输入值进行。是的,您可以使用一种启发式方法来扫描操作列表并稍微清理一下。是的,您可以将其中的一些编译成汇编,而不是将+、*等作为高级函数调用。您可以将矢量化(先做所有的+,然后做所有的*,等等,用一个值的矢量)与一次只做一个值的整个过程进行比较。但是你需要吗 我的意思是,如果你用gnuplot或Mathematica绘制一个函数,你认为会发生什么 多少是“很多次”?一百万 可以输入什么样的函数?我们能假设它们是连续的吗 您是否尝试过测量代码的性能 (对不起,从问题开始!) 您可以尝试下面简要介绍的两种方法中的一种(或两者兼用)(可能还有更多): 1) 解析树。 您可以创建一个解析树。然后像大多数编译器那样优化表达式、常数折叠、公共子表达式消除(可以通过链接公共表达式子树并缓存结果来实现)等等 然后可以使用惰性评估技术来避免整个子树。例如,如果你有一棵树
*
/ \
A B
如果A的计算结果为0,则可以完全避免计算B,因为您知道结果为0。使用RPN,您将在惰性评估中失败
2) 插值
假设你的函数是连续的,你可以用这个函数来近似你的函数到一个很高的精度。通过这种方式,您可以对函数进行几次复杂的计算(基于您选择的多项式次数),然后在其余时间进行快速多项式计算
要创建初始数据集,您可以使用方法1,也可以坚持使用RPN,因为您只会生成几个值
所以如果你使用插值,你可以保持你的RPN
希望有帮助 为什么要重新发明轮子?改用快速脚本语言。 将lua之类的东西集成到代码中只需要很少的时间,而且速度非常快 通常,您可以对表达式进行字节编译,这将导致代码运行速度非常快,对于简单的1D图来说速度肯定足够快
我推荐lua,因为它速度快,并且与C/C++的集成比任何其他脚本语言都容易。另一个好的选择是python,但尽管它更为人所知,但我发现它的集成更为棘手。我所做的是使用分流算法来生成RPN。然后,我将RPN“编译”成一种标记形式,可以重复执行(解释性地执行),而无需重新解析表达式。您对RPN的简单解释应该可以正常工作,尤其是因为它包含
- 数学库函数,如
、cos
和exp
(pow,涉及日志)^
- 符号表查找
另外,对于解析,您的语法非常普通,因此一个简单的递归下降解析器(大约一页代码,O(n)与分流场相同)应该可以正常工作。事实上,您可能只需要在解析时计算结果(如果数学函数占用了大部分时间),而不必担心解析树、RPN之类的东西。Michael Anderson建议。如果您想尝试Lua完成此任务,请参阅我的库。我认为此基于RPN的库可以达到以下目的:
我把它和我的一个计算器项目一起使用,效果很好。它小而简单,但可扩展。一种优化方法是用一个值数组替换堆栈,并将求值器实现为一个函数,其中每个操作从两个(或一个)位置加载并保存到第三个位置。这可能导致代码非常紧凑:
struct Op {
enum {
add, sub, mul, div,
cos, sin, tan,
//....
} op;
int a, b, d;
}
void go(Op* ops, int n, float* v) {
for(int i = 0; i < n; i++) {
switch(ops[i].op) {
case add: v[op[i].d] = v[op[i].a] + v[op[i].b]; break;
case sub: v[op[i].d] = v[op[i].a] - v[op[i].b]; break;
case mul: v[op[i].d] = v[op[i].a] * v[op[i].b]; break;
case div: v[op[i].d] = v[op[i].a] / v[op[i].b]; break;
//...
}
}
}
struct Op{
枚举{
加、分、多、分区,