出于学术目的,我必须编写一个绘制用户输入表达式的应用程序,如:f(x)= 1 - exp(3 ^(5 * ln(cosx))+ x)
我选择编写解析器的方法是使用Shunting-Yard算法转换RPN中的表达式,将原始函数(如“cos”)视为一元运算符。这意味着上面写的函数将被转换为一系列令牌,如:
1, x, cos, ln, 5, *,3, ^, exp, -
问题在于绘制我必须评估它的函数很多次,因此对每个输入值应用堆栈评估算法将是非常低效的。 我怎么解决这个问题?我是否必须忘记RPN的想法?
答案 0 :(得分:3)
“很多次”多少钱?一百万?
可以输入什么样的功能?我们可以假设它们是连续的吗?
您是否尝试过测量代码的效果?
(抱歉,问题开始了!)
您可以尝试下面简要描述的两种方法中的一种(或两者)(可能还有更多):
您可以创建一个解析树。然后执行大多数编译器所做的工作来优化表达式,常量折叠,公共子表达式消除(可以通过将常用表达式子树链接在一起并缓存结果来实现)等。
然后你可以使用懒惰的评估技术来避免整个子树。例如,如果你有一棵树
*
/ \
A B
其中A的计算结果为0,你可以完全避免评估B,因为你知道结果为0.使用RPN你会失去懒惰的评估。
假设您的功能是连续的,您可以使用Polynomial Interpolation以高精度逼近您的功能。通过这种方式,您可以对函数进行几次复杂的计算(根据您选择的多项式的次数),然后在其余时间进行快速多项式计算。
要创建初始数据集,您可以使用方法1或只是坚持使用您的RPN,因为您只会生成一些值。
因此,如果你使用Interpolation,你可以保留你的RPN ......
希望有所帮助!
答案 1 :(得分:2)
为什么重新发明轮子?请改用快速脚本语言。 将lua之类的东西集成到你的代码中将花费很少的时间并且非常快。
您通常可以对表达式进行字节编译,这样可以使代码运行得非常快,对于简单的一维图形来说肯定足够快。
我推荐lua作为它的快速,并且比其他任何脚本语言更容易与C / C ++集成。另一个不错的选择是python,但是虽然它更为人所知我发现整合起来比较棘手。
答案 2 :(得分:1)
为什么不在一个解析树周围(我松散地使用“树”,在你的情况下它是一系列操作),并相应地标记输入变量? (例如,对于输入x,y,z等,注释“x”,用0表示第一个输入变量,“y”用1表示第二个输入变量,等等。)
这样你就可以解析表达式一次,保留解析树,接受一组输入,并应用解析树进行评估。
如果你担心评估步骤的性能方面(与解析步骤相比),我认为你做得更好,除非你进入矢量化(在输入向量上应用你的解析树)立即)或将操作硬编码为固定功能。
答案 3 :(得分:1)
我所做的是使用分流算法来生成RPN。然后我将RPN“编译”成一个标记化的形式,可以重复执行(解释),而无需重新解析表达式。
答案 4 :(得分:1)
答案 5 :(得分:0)
在什么意义上效率不高?有机器时间和程序员时间。是否有一个标准,它需要以特定的复杂程度运行多快?完成任务并转到下一个任务更重要(完美主义者有时候永远不会完成)?
所有这些步骤都必须针对每个输入值进行。是的,您可以使用启发式扫描操作列表并稍微清理一下。是的,您可以将其中的一部分编译为汇编而不是将+,*等作为高级函数调用。您可以比较矢量化(使用所有+,然后所有*等,使用值向量)来一次执行一个值的整个过程。但你需要吗?
我的意思是,如果你在gnuplot或Mathematica中绘制一个函数,你会怎么想?
答案 6 :(得分:0)
您对RPN的简单解释应该可以正常工作,特别是因为它包含
数学库函数,如cos
,exp
和^
(pow,涉及日志)
符号表查找
希望您的符号表(包含x中的变量)将简短明了。
图书馆的功能很可能是你最大的时间,所以除非你的翻译写得不好,否则不会有问题。
但是,如果真的要求速度,你可以将表达式转换为C代码,然后编译并将其链接到dll中并加载它(大约需要一秒钟) )。那,加上数学函数的memoized版本,可以给你最好的表现。
P.S。对于解析,你的语法很香草,所以一个简单的递归下降解析器(关于一个代码页,O(n)与shunting-yard相同)应该可以正常工作。实际上,您可能只能在解析时计算结果(如果数学函数占用大部分时间),而不用打扰树,RPN,任何这些东西。
答案 7 :(得分:0)
我认为这个基于RPN的库可以达到目的:http://expressionoasis.vedantatree.com/
我将它与我的一个计算器项目一起使用,效果很好。它既小又简单,但可扩展。
答案 8 :(得分:0)
一种优化方法是用一组值替换堆栈,并将求值程序实现为three address mechine,其中每个操作从两个(或一个)位置加载并保存到第三个。这可能会造成非常严密的代码:
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;
//...
}
}
}
从RPN到3地址的转换应该很容易,因为3地址是一种概括。