使用后缀表达式绘制一元函数图像

文章目录
  1. 1. 效果图
  2. 2. Github链接
  3. 3. UI部分实现原理
  4. 4. 函数图像绘制原理
  5. 5. 后缀表达式解析、计算实现
  6. 6. 效率优化
  7. 7. 该绘制方法的缺点
  8. 8. 结束

在早些时候,我写过一个 ExpParser,是一个利用后缀表达式解析表达式并计算结果的玩意,既然已经可以解析表达式计算出结果了,那能否使用后缀表达式来绘制函数图像?借着这次Java实训课,我把这个脑洞实践了一下,故总结出这篇文章。

效果图

y=sin(x)
y=x^2
软件界面截图

Github链接

JDrawFunc,Idea的工程。

UI部分实现原理

首先UI部分使用Java Swing的Graphics2D来绘制图像,通过继承重写JComponent组件可以实现画板一样的组件,具体是通过重写paintComponent方法实现。组件里的图像是可拖动的,并且图像还会根据拖动绘制相应的图像,这个实现起来比较复杂,大体的思路就是先确定初始时候坐标原点的位置,再根据鼠标拖动的offset来计算原点偏移的位置,再根据偏移后的原点和屏幕范围,计算出可见画面的范围并计算对应的图像,到时候有兴趣的朋友直接去翻代码好了,代码里写了很详细的注释。

函数图像绘制原理

函数图像绘制部分,绘制函数图像的原理是根据表达式计算出几千个点,然后把点画上去,就实现了函数图像的绘制,但是这里比较麻烦的是点的数量,点多了,函数图像虽然平滑,但是如果一次绘制的时间超过1/60s即16.7ms的话,画面拖动重绘的时候就会有延迟,我的办法是每次大概绘制4000个点,再开一下Graphics2D的抗锯齿,就能满60帧。

那么到底怎么使用后缀表达式计算函数点?在表达式里加入小写字母x,计算后缀表达式的时候将x替换成对应的x坐标,这样计算出来的就是对应这个x坐标的y坐标,那么这里的x和y就是对应函数上的一个点了。

后缀表达式解析、计算实现

后缀表达式需要先解析,然后才能计算,我实现的解析的过程是:

  1. 拆分操作数和操作符。(因为减号和负号难以区分,这里先统一以减号拆分出来,括号也是如此)
  2. 字符串后处理。(这里处理上述的负号问题,判断负号并合并到数字字符串里,“-(-50)”这种情况统一处理成“0-(0-50)”)
  3. 将拆分好的字符串数组转后缀表达式。(注意转换好后依然是一个字符串数组)
  4. 计算。(这里会将字符串转成数字来计算)

效率优化

程序可能在一帧的时间里就需要计算几千次,这时候就需要好好优化效率了。

  1. 字符串转数字时,利用hashmap缓存。根据上面的后缀表达式计算流程我们可以知道,在计算的过程中依旧需要大量的字符串转数字的操作。其实本来这样做反而会拖慢转换速度,因为hashmap计算hashcode也大概需要O(n)的时间,字符串转数字也需要O(n)的时间,但是我加了小数的支持,所以在转换的时候还要判断一下字符串里是否有小数点,这个操作也要一次O(n),虽然整体下来也只是O(2n),还是n的幂级,但是实际测试用了hashmap的快一些。
  2. 对象复用。在我的程序里,每一帧都需要计算约4000个点来绘制,也就是4000个对象,这里我用了类似对象池的思想,在程序初始化的时候先new好要用的数组,并将其中的对象全部初始化满,然后用一个变量k来标记当前使用的对象数量,绘制的时候根据k来绘制,这样可以极大地减少临时对象数量。其实要是时间、精力够的话,还可以好好造一个对象池来优化,这里的对象池过于简陋,效果也没那么极致。

该绘制方法的缺点

  1. 由于这个方法是用点描的线,而且线的流畅度取决于点的密度,而点的密度又取决于dx的粒度,dx小,线描出来确实流畅,但绘制的耗时会增加不少,这是一个缺点。

  2. 另一个缺点就是某些函数的某些部分,即便dx改变量很小,如只增加了0.01,dy却改变很大,几十,上百,这样的话在屏幕上就会形成疏密不一致的点,如下图:
    y=x^(sin(x))

  3. 还有,这个方式只能绘制形如y=f(x)的函数图像,一旦变成如x+y=1这种形式就无解了,必须手动转换形式。

结束

使用后缀表达式来绘制函数图像确实不是一个好的办法,但是如果你恰好需要应付什么课程作业,并对此感兴趣的话,可以一试。

分享到