
本文介绍如何在PHP中不使用`eval()`函数,实现一个能够正确处理数学运算符优先级的表达式计算器。核心思想是将中缀表达式转换为逆波兰表示法(RPN),然后通过栈结构计算RPN表达式的结果,从而安全有效地解析和计算复杂的数学公式,避免了`eval()`带来的潜在安全风险。
在PHP开发中,有时我们需要解析并计算用户输入的数学表达式。虽然eval()函数能够直接执行字符串中的PHP代码,从而实现表达式计算,但其存在显著的安全隐患,因为它允许执行任意代码。为了构建一个更安全、可控的表达式计算器,我们需要采用一种不依赖eval()的方法。本教程将详细介绍如何通过将中缀表达式转换为逆波兰表示法(Reverse Polish Notation, RPN)并利用栈结构来计算表达式的值,同时正确处理运算符优先级和括号。
核心原理:中缀转逆波兰与逆波兰计算
实现安全表达式计算的核心在于两个步骤:
中缀表达式转换为逆波兰表示法(RPN): 中缀表达式是我们日常使用的数学表达式形式(例如 2 + 3 * 4)。RPN(也称为后缀表达式)则将运算符置于其操作数之后(例如 2 3 4 * +)。RPN的优点在于它消除了对括号的需求,并且运算符的优先级在转换过程中就已经确定,使得计算过程变得非常简单。这个转换过程通常使用“调度场算法”(Shunting-yard algorithm)来实现。逆波兰表达式计算: 一旦表达式被转换为RPN,就可以使用一个简单的栈结构来计算其结果。
详细实现
我们将通过一系列PHP函数来实现上述逻辑。
立即学习“PHP免费学习笔记(深入)”;
1. 辅助函数
首先,定义几个辅助函数来判断字符类型和读取数字。
= '0' && $char <= '9'));}/** * 从字符串指定位置开始读取一个完整的数字 * @param string $string 完整的表达式字符串 * @param int $i 当前读取的起始索引 * @return string 读取到的数字字符串 */function readnumber($string, $i) { $number = ''; while (isset($string{$i}) && is_number($string{$i})) { $number .= $string{$i}; $i++; } return $number;}
2. 中缀表达式转换为逆波兰表示法 (mathexp_to_rpn)
此函数负责将中缀表达式字符串解析并转换为RPN数组。它使用两个栈:一个用于存储最终的RPN序列(final_stack),另一个用于临时存储运算符(operator_stack)。
/** * 将中缀表达式转换为逆波兰表示法 (RPN) * @param string $mathexp 中缀数学表达式字符串 * @return array 逆波兰表示法数组 */function mathexp_to_rpn($mathexp) { // 定义运算符优先级 $precedence = array( '(' => 0, // 括号优先级最低,用于控制流 '-' => 3, '+' => 3, '*' => 6, '/' => 6, '%' => 6 ); $i = 0; $final_stack = array(); // 存储最终的RPN序列 $operator_stack = array(); // 存储运算符 while ($i < strlen($mathexp)) { $char = $mathexp{$i}; // 1. 如果是数字,直接添加到最终栈 if (is_number($char)) { $num = readnumber($mathexp, $i); array_push($final_stack, (float)$num); // 转换为浮点数 $i += strlen($num); // 跳过已读取的数字部分 continue; } // 2. 如果是运算符 if (is_operator($char)) { // 比较当前运算符与运算符栈顶的优先级 while ( !empty($operator_stack) && ($top_operator = end($operator_stack)) && $top_operator != '(' && // 栈顶不是左括号 $precedence[$char] <= $precedence[$top_operator] // 当前运算符优先级小于等于栈顶运算符 ) { array_push($final_stack, array_pop($operator_stack)); // 将栈顶运算符弹出并添加到最终栈 } array_push($operator_stack, $char); // 将当前运算符压入运算符栈 $i++; continue; } // 3. 如果是左括号 if ($char == '(') { array_push($operator_stack, $char); // 左括号直接压入运算符栈 $i++; continue; } // 4. 如果是右括号 if ($char == ')') { // 弹出运算符直到遇到左括号 while ( !empty($operator_stack) && ($operator = array_pop($operator_stack)) && $operator != '(' ) { array_push($final_stack, $operator); } // 如果栈空了还没找到左括号,说明表达式有误,这里不做严格检查 $i++; continue; } // 忽略其他字符,例如空格(如果表达式中包含空格) $i++; } // 循环结束后,将运算符栈中剩余的所有运算符弹出并添加到最终栈 while (!empty($operator_stack)) { array_push($final_stack, array_pop($operator_stack)); } return $final_stack;}
3. 逆波兰表达式计算 (calculate_rpn)
此函数接收一个RPN数组,并使用一个栈来计算最终结果。
/** * 计算逆波兰表示法 (RPN) 表达式的结果 * @param array $rpnexp 逆波兰表示法数组 * @return float 计算结果 */function calculate_rpn($rpnexp) { $stack = array(); foreach($rpnexp as $item) { if (is_operator($item)) { // 运算符操作需要两个操作数,从栈顶弹出 $operand2 = array_pop($stack); $operand1 = array_pop($stack); switch ($item) { case '+': array_push($stack, $operand1 + $operand2); break; case '-': array_push($stack, $operand1 - $operand2); break; case '*': array_push($stack, $operand1 * $operand2); break; case '/': if ($operand2 == 0) { // 避免除零错误,这里可以抛出异常或返回特定值 trigger_error("Division by zero", E_USER_WARNING); return NAN; // Not a Number } array_push($stack, $operand1 / $operand2); break; case '%': array_push($stack, $operand1 % $operand2); break; } } else { // 操作数直接压入栈 array_push($stack, $item); } } return array_pop($stack); // 最终栈中只剩一个结果}
4. 主计算函数 (calculate)
这个函数作为入口点,将中缀表达式计算过程封装起来。
/** * 计算中缀数学表达式的结果 * @param string $exp 中缀数学表达式字符串 * @return float 计算结果 */function calculate($exp) { return calculate_rpn(mathexp_to_rpn($exp));}// 示例用法$expression = "27+38+81+48*33*53+91*53+82*14+96";$result = calculate($expression);echo "表达式: " . $expression . "n";echo "计算结果: " . $result . "n"; // 预期输出 90165$expression_with_parentheses = "(2 + 3) * 4 - 10 / 2";$result_with_parentheses = calculate($expression_with_parentheses);echo "表达式: " . $expression_with_parentheses . "n";echo "计算结果: " . $result_with_parentheses . "n"; // 预期输出 (5 * 4) - 5 = 20 - 5 = 15$expression_with_floats = "10.5 + 2 * 3.5";$result_with_floats = calculate($expression_with_floats);echo "表达式: " . $expression_with_floats . "n";echo "计算结果: " . $result_with_floats . "n"; // 预期输出 10.5 + 7 = 17.5?>
完整示例代码
将上述所有函数整合到一个PHP文件中,可以直接运行。
= '0' && $char 0, '-' => 3, '+' => 3, '*' => 6, '/' => 6, '%' => 6 ); $i = 0; $final_stack = array(); $operator_stack = array(); while ($i < strlen($mathexp)) { $char = $mathexp{$i}; if (is_number($char)) { $num = readnumber($mathexp, $i); array_push($final_stack, (float)$num); $i += strlen($num); continue; } if (is_operator($char)) { while ( !empty($operator_stack) && ($top_operator = end($operator_stack)) && $top_operator != '(' && $precedence[$char]
注意事项与扩展
输入验证: 提供的代码没有对输入表达式进行严格的有效性检查。例如,它不会检测到不匹配的括号、无效的字符或连续的运算符。在生产环境中,强烈建议在mathexp_to_rpn函数之前添加一个预解析步骤来验证表达式的语法。浮点数精度: PHP的浮点数运算可能存在精度问题,尤其是在连续除法或涉及小数的复杂计算中。如果需要极高的精度,可以考虑使用BCMath扩展进行任意精度数学运算。错误处理: 当前代码在除零时会触发一个警告并返回NAN。根据应用需求,可以改为抛出自定义异常,以便更好地控制错误流程。扩展运算符: 要添加新的运算符(例如幂运算 ^),需要更新is_operator函数和precedence数组,并在calculate_rpn函数的switch语句中添加相应的处理逻辑。支持负数和一元运算符: 当前实现假定所有数字都是正数,并且运算符都是二元的。要支持负数(例如 -5)或一元减号(例如 -(2+3)),需要对readnumber和mathexp_to_rpn的解析逻辑进行更复杂的修改,以区分一元和二元减号。性能: 对于非常长的表达式,这种基于字符串遍历和栈操作的方法可能不是最高效的。但对于大多数常见用例,其性能是完全可以接受的。
总结
通过将中缀表达式转换为逆波兰表示法并利用栈进行计算,我们成功地在PHP中实现了一个不依赖eval()的安全、可控的数学表达式计算器。这种方法不仅解决了运算符优先级和括号处理的复杂性,还避免了eval()带来的潜在安全风险,使得应用程序能够更安全地处理用户提供的数学表达式。在实际应用中,结合输入验证和适当的错误处理,可以构建出健壮且功能强大的表达式解析器。
以上就是PHP实现不依赖eval()的数学表达式解析与计算(含运算符优先级)的详细内容,更多请关注php中文网其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1334825.html
微信扫一扫
支付宝扫一扫