起因是这样,单位在开发的时候遇到了这么一个需求:
- 用户可以自定义函数
- 函数可以嵌套函数
- 函数内计算有优先级,可以使用括号等
大致可以理解为 $$ f(x) = g(x) + c(x) \
g(x) = c(x) * 4 + 1 \
c(x) = 10 + x $$
在这里有两个难点
- 如何识别并灵活使用用户自定的函数
- 如何保证计算优先级
因为有优先级+用户自定函数的组合,直接解决的最好的解决办法就是eval()函数。但eval()函数会有一个很致命的问题,就是任意代码都可运行。如果这里碰到了特定的注入攻击,还是很危险的。
还有一种就是遍历字符串检测恶意代码和替换字符串模版,再去用eval()函数运行,这样就解决了优先级问题,但这个也有问题,因为我们需要知道所有恶意代码的可能
或者我们直接遍历字符串去生成算式并计算,但这个实现起来成本太高,相当于开发了一套编译器
但如果我们不使用eval,那又应该如何让计算机理解算式并按优先级处理呢
前阵子在看linux系统实现的时候无意中看到linux中的计算器:bc命令
下面是bc命令的使用记录
bash$ bc
bc 1.06
Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
1+1
2
1 + 1
2
1+2*3
7
(1+2)*3
9
quit
bash$
哇这玩意太神奇了,它还可以算括号,于是在好奇心的驱使下,我决定去看一下他的实现原理,一番搜索过后,发现了下面这句
bc 命令是 dc 命令的预处理程序。除非指定 -c(仅编译)标志,否则它自动调用 dc 命令
wtf?这是命令套命令吗?dc又是什么玩意,这俩咋交互的啊?
其实命令bc只做了一件事,那就是将你输入的“(3+4)×5-6”这种中缀表达式转换为“3 4 + 5 × 6 -”这个样子的后缀表达式(逆波兰表达式),然后将结果交给dc
而dc也只做了一件事,那就是计算bc的结果——逆波兰表达式
老规矩,上定义
逆波兰表示法(Reverse Polish notation,RPN,或逆波兰记法),是一种是由波兰数学家扬·武卡谢维奇1920年引入的数学表达式形式,在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法。逆波兰记法不需要括号来标识操作符的优先级。
逆波兰结构由弗里德里希·鲍尔(Friedrich L. Bauer)和艾兹格·迪科斯彻在1960年代早期提议用于表达式求值,以利用堆栈结构减少计算机内存访问。逆波兰记法和相应的算法由澳大利亚哲学家、计算机学家查尔斯·汉布尔(Charles Hamblin)在1960年代中期扩充[1][2]
其实我们需要逆波兰表达式的最主要的原因就是逆波兰记法不需要括号来标识操作符的优先级。而且这玩意被广泛用于台式计算器,有很强的背书啊。我们再来看一遍中缀表达式和逆波兰表达式的区别
中缀表达式: (3+4)×5-6
逆波兰表达式:3 4 + 5 × 6 -
我们在日常使用的表达式其实叫中缀表达式,也就是运算符在运算数的中间。这种表达式咱们人类很容易理解、计算,但如果让计算机理解这种表达式那你真的是难为它了。
通过逆波兰表达式的处理,我们可以直接消除掉括号,并在计算时按照指定的优先级进行运算
接下来会开始讲逆波兰表达式的生成与计算,但在讲之前我们先来复习下常用数据结构“栈”
老规矩
栈(英语:stack)又称为堆栈或堆叠,栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。
栈是允许在同一端进行插入和删除操作的特殊线性表。允许进行插入和删除操作的一端称为栈顶(top),另一端为栈底(bottom);栈底固定,而栈顶浮动;栈中元素个数为零时称为空栈。插入一般称为进栈(PUSH),删除则称为退栈(POP)。
大家不要被这段话吓到啊,咱们整体通读下来会发现,他一共就俩方法比较重要,一个是进栈(PUSH),一个是退栈(POP)。这不就是我们常用的数组自带的方法吗。
所以在下面的实现里我会用数组代替栈来实现逆波兰表达式的生成与计算
注意:逆波兰表达式的生成一般指将中缀表达式转换为逆波兰表达式
如果要生成逆波兰表达式,那么我们需要操作两个栈,分别是操作符栈与结果栈,并根据以下规则操作