PHP实现的一个简单的混乱计算器
最近准备换工作,就多留意了一下招聘信息,正好有一个HR说他们要招高级PHP工程师,就跟她聊了一会儿。然后她就让他们的技术老大给我出了一道题来考我。题目是:实现一个混乱计算器。
那么,什么是混乱计算器?
普通的计算器是从左到右计算结果,*/的优先级大于+-,因此1+2-3/3=2。而混乱计算器刚好相反,从右往左计算结果,+-的优先级大于*/,括号具有最高优先级。因此1+2-3/3=1.5。(混乱计算器貌似没有任何实际意义!)
那么怎么实现这样的功能呢?仔细观察了一下,这个跟普通的计算器基本上没有区别除了优先级和运算的放向。因此,我就按照计算器的实现原理做了一个混乱计算器的版本:
假设函数为:
function calculate($expression){}
首先通过正则表达式把$expression中的数字与符号分离出来,放到一个数组中。我们可以通过下面两句得到这样的结果:
$pattern = '/\+|\-|\*|\/|\d*\.?\d|\(|\)/'; // 匹配表达式 preg_match_all($pattern, $expression, $out)
这个$out就是我们匹配出来的结果。通常的计算器到这一步就够了,但是因为混乱计算器的运算放向是相反的,因此我们需要把数组的顺序颠倒一下:
$out = array_reverse($out[0]); // 先把数组反转
下面我们开始对$out进行解析。解析过程是这样的:
首先创建两个栈$opStack和$numStack分别用来存储操作符(+-*/和括号)和数字。php中可以通过array来表示栈。
循环依次取得$out的所有值$v,分以下情况处理:
1. 如果$v是数字,则放入数字栈$numStack。
2. 如果$v是符号,则做以下判断:
1)$opStack为空,或者符号栈不为空但$v==“)”,则直接存入$opStack。
2)$opStack不为空,且$v !=“(”,则判断当$v与$opStack中最顶部的符号$op的优先级。
如果$v的优先级大于$op,则把$v放入$opStack
如果$v的优先级小于或等于$op,则计算$numStack中两个数与$op之间的结果,并将结果存到$numStack中,然后将$v存入$opStack中
3)$opStack不为空,且$v == "(",则应该计算括号之内的所有值,并且将计算结果存到$numStack中。
循环完成之后,就可以直接通过循环把$numStack中的值计算出来了。
需要注意的是,因为混乱计算器的放心跟普通计算器的放向刚好相反,因此做计算的时候需要用$val2-$va1(假设$op为减号):
$val1 = array_pop($numStack); $val2 = array_pop($numStack); array_push($numStack, calc($val2, $val1, $op)); // 把两个值与之前的操作符进行计算并放回数组。
这里的calc函数的实现如下:
function calc($val1, $val2, $op) { switch ($op) { case '+': return $val1+$val2; case '-': return $val1-$val2; case '*': return $val1*$val2; case '/': if ($val2 == 0) { throw new Exception('除0错误!'); } else { return $val1/$val2; } } }
$calculate函数的实现如下:
function calculate($expression) { $pattern = '/\+|\-|\*|\/|\d*\.?\d|\(|\)/'; // 匹配表达式 $numStack = array(); $opStack = array(); if (preg_match_all($pattern, $expression, $out) !== false) { $out = array_reverse($out[0]); // 先把数组反转 foreach ($out as $v) { if (is_numeric($v)) { // 添加到数字堆栈 array_push($numStack, $v); } else { $opNum = count($opStack); if (count($opStack) == 0) { array_push($opStack, $v); // 没有操作符,加入堆栈 } else { // 有操作符,则比较两个操作符的优先级 if ( in_array($v, array('+', '-')) && in_array($opStack[$opNum-1], array('*', '/')) || $v == ')' || $opStack[$opNum-1] == ')' && $v !== '(' ) { array_push($opStack, $v); } else if ($v !== '('){ $val1 = array_pop($numStack); $val2 = array_pop($numStack); array_push($numStack, calc($val2, $val1, array_pop($opStack))); // 把两个值与之前的操作符进行计算并放回数组。 array_push($opStack, $v); // 把新的操作符放入堆栈 } else { // 遇到括号的结尾,则计算括号范围内的所有的表达式的值 while (count($opStack) > 0 && ($op = array_pop($opStack)) != ')') { $val1 = array_pop($numStack); $val2 = array_pop($numStack); array_push($numStack, calc($val2, $val1, $op)); // 把两个值与之前的操作符进行计算并放回数组。 } } } } } // 输出结果 while (count($opStack) >0 ) { $op = array_pop($opStack); $val1 = array_pop($numStack); $val2 = array_pop($numStack); array_push($numStack, calc($val2, $val1, $op)); // 把两个值与之前的操作符进行计算并放回数组。 } return $numStack[0]; } else { throw new Exception('解析错误!'); } }
测试了一下:
echo calculate('1+2-3/3').'<br>'; // 1.5 echo calculate('1+(2-3*5)/3').'<br>'; // 0.5 echo calculate('1+((2-3*5)/3)').'<br>'; // 1.6 echo calculate('1+(2-3*5+0.2)/3').'<br>'; // 0.48387096774194
看起来大功告成!但是这个程序还有很多值得优化和完善的地方:
1. 没有对非法的输入进行检查。
2. 如果括号缺失,可能会导致计算结果异常甚至出错。
3. 性能可以做进一步的优化。
总之,混乱计算器的实现方法跟普通计算器是一样的。