改进shunting-yard调度场算法,解析含有函数式的数学表达式
在最近的量化系统开发过程中,我遇到了一个问题,需要解析用户输入的数学/逻辑表达式,并计算这个表达式的结果。解析数学表达式的经典方法是所谓的调度场算法(Shunting-yard algorithm),将数学表达式由人类易读的“中缀表达式”重写为“后缀表达式”(逆波兰表达式),从而让计算机容易计算其结果。
不过,经典的调度场算法只能处理数学/逻辑运算符表达式,如果表达式中含有函数式,它是无能为力的,充其量可以处理单变量函数例如sqrt(), sin() 等等。而我的工作恰恰需要解析含有函数式的表达式,而且函数的参数个数是不确定的,例如下面的表达式:
1
+
s
u
m
(
1
,
2
,
(
3
+
5
)
?
4
,
m
a
x
(
3
,
(
4
+
5
)
?
6
)
,
7
?
8
)
?
(
2
+
3
)
1+sum(1,2,(3+5)*4, max(3, (4+5)*6), 7-8) * (2+3)
1+sum(1,2,(3+5)?4,max(3,(4+5)?6),7?8)?(2+3) 这个表达式中出现了两个函数,第一个sum() 有5个参数,第二个max() 有2个参数。为了实现对上述表达式的解析,需要改进标准的shunting-yard调度场算法,使它能在解析表达式的同时识别并存储函数的参数个数,从而实现对函数表达式的解析。
改进的Shunting-yard算法
本文的目的并不为了介绍经典的调度场算法,如果对本算法不熟悉的同学,建议阅读这篇文章。
我假设这篇文章的读者本身已经研究过调度场算法,只是为了改进它以解析函数式,这里直接给出我改进后的调度场算法,其中改进的部分用高亮显示:
算法使用一个队列,记为“输出队列” 算法使用两个栈,分别记为“op栈”和“args栈”,分别用于临时存放运算符和式子中函数的参数数量
- 当还有记号可以读取时:
- 读取一个记号。
- 如果这个记号表示一个数字,那么将其添加到输出队列中。
- 如果这个记号表示一个函数,那么将其压入op栈当中,同时将数字0压入args栈中。
- 如果这个记号表示一个函数参数的分隔符,例如一个半角逗号,那么:
- 从op栈当中不断地弹出操作符并且放入输出队列中去,直到栈顶部的元素为一个左括号为止。如果一直没有遇到左括号,那么要么是分隔符放错了位置,要么是括号不匹配。
- args栈最顶端的元素+1
- 如果这个记号表示一个操作符,记做o1,那么:
- 只要存在另一个记为o2的操作符位于op栈的顶端,并且
- 如果o1是左结合性的并且它的运算符优先级要小于或者等于o2的优先级,或者
- 如果o1是右结合性的并且它的运算符优先级比o2的要低,那么:
- 将o2从op栈的顶端弹出并且放入输出队列中(循环直至以上条件不满足为止);
- 然后,将o1压入op栈的顶端。
- 如果这个记号是一个左括号,那么就将其压入op栈当中。
- 如果这个记号是一个右括号,那么:
- 从栈当中不断地弹出操作符并且放入输出队列中,直到运算符栈顶部的元素为左括号为止。
- 将左括号从栈的顶端弹出并丢弃。
- 如果此时位于op栈顶端的记号表示一个函数,那么:
- args栈最顶端的元素+1,将元素弹出,记为arg_count
- 将op栈顶端的函数弹出并放入输出队列中去,记录该函数的参数数量为arg_count
- 如果在找到一个左括号之前栈就已经弹出了所有元素,那么就表示在表达式中存在不匹配的括号。
- 当再没有记号可以读取时:
- 如果此时在栈当中还有操作符:
- 如果此时位于栈顶端的操作符是一个括号,那么就表示在表达式中存在不匹配的括号。
- 退出算法。
后缀表达式的计算
带函数式的后缀表达式的计算方式与通常的表达式一样,唯一的区别是在计算函数值的时候,需要读取该函数的参数数量,并且从数字栈中读取相应数量的参数带入函数式计算即可。
|