
本教程将详细介绍如何在php中不使用`eval()`函数,安全有效地计算包含运算符优先级的数学表达式。核心方法是采用调度场算法将中缀表达式转换为逆波兰表示法(rpn),随后利用栈结构对rpn表达式进行求值,从而实现对复杂数学运算的精确处理。
在PHP开发中,直接使用eval()函数来执行用户提供的数学表达式存在严重的安全隐患,因为它允许执行任意的PHP代码。为了避免这种风险,同时又能灵活地处理带有运算符优先级的数学表达式,我们需要一种自定义的解析与计算方案。本教程将深入探讨如何通过将中缀表达式转换为逆波兰表示法(Reverse Polish Notation, RPN)并对其进行求值来实现这一目标。
处理数学表达式通常涉及两个主要步骤:解析和求值。
我们日常使用的数学表达式,如 2 + 3 * 4,被称为中缀表达式。它的特点是运算符位于操作数之间,并且需要考虑运算符优先级和括号。
逆波兰表示法(RPN),也称为后缀表达式,是一种没有括号的表达式形式,其中运算符位于其操作数之后。例如,中缀表达式 2 + 3 * 4 对应的 RPN 形式是 2 3 4 * +。RPN 的优点在于,它在求值时无需考虑运算符优先级,只需按照从左到右的顺序处理即可,这大大简化了计算逻辑。
立即学习“PHP免费学习笔记(深入)”;
调度场算法是 Dijkstra 提出的一种将中缀表达式转换为 RPN 的经典算法。它利用两个栈:一个用于存储运算符(运算符栈),另一个用于存储输出(输出栈或队列,最终形成 RPN 序列)。
算法的核心规则如下:
我们将通过一系列 PHP 函数来实现数学表达式的解析和计算。
calculate() 函数是整个流程的入口,它负责协调中缀表达式到 RPN 的转换以及 RPN 表达式的求值。
<?php
/**
* 计算中缀数学表达式的结果
* @param string $exp 中缀数学表达式字符串
* @return float 表达式计算结果
*/
function calculate($exp) {
return calculate_rpn(mathexp_to_rpn($exp));
}
// ... 后续辅助函数和核心函数 ...
?>这个函数实现了调度场算法,将中缀表达式字符串转换为 RPN 数组。
<?php
/**
* 将中缀表达式转换为逆波兰表示法 (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) && end($operator_stack) != '(' && $precedence[$char] <= $precedence[end($operator_stack)]) {
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)) != '(') {
array_push($final_stack, $operator);
}
$i++;
continue;
}
$i++; // 跳过空格或其他未知字符
}
// 表达式处理完毕,将运算符栈中剩余的所有运算符弹出到结果栈
while ($oper = array_pop($operator_stack)) {
array_push($final_stack, $oper);
}
return $final_stack;
}
/**
* 从字符串中读取一个完整的数字
* @param string $string 原始字符串
* @param int $i 当前读取位置的索引(引用传递)
* @return string 读取到的数字字符串
*/
function readnumber($string, &$i) {
$number = '';
$start_i = $i; // 记录开始位置
while ($i < strlen($string) && is_number($string{$i})) {
$number .= $string{$i};
$i++;
}
$i = $start_i; // 恢复 $i 到数字开始位置,因为外部循环会重新增加 $i
return $number;
}
/**
* 判断字符是否为运算符
* @param string $char 待判断字符
* @return bool
*/
function is_operator($char) {
static $operators = array('+', '-', '/', '*', '%');
return in_array($char, $operators);
}
/**
* 判断字符是否为数字或小数点
* @param string $char 待判断字符
* @return bool
*/
function is_number($char) {
return (($char == '.') || ($char >= '0' && $char <= '9'));
}
?>代码解析要点:
这个函数接收 RPN 数组,并使用一个栈来计算表达式的结果。
<?php
/**
* 计算逆波兰表示法 (RPN) 表达式的结果
* @param array $rpnexp 逆波兰表示法数组
* @return float 表达式计算结果
*/
function calculate_rpn($rpnexp) {
$stack = array(); // 存储操作数的栈
foreach ($rpnexp as $item) {
if (is_operator($item)) {
// 如果是运算符,弹出两个操作数进行计算
$j = array_pop($stack);
$i = array_pop($stack);
switch ($item) {
case '+':
array_push($stack, $i + $j);
break;
case '-':
array_push($stack, $i - $j);
break;
case '*':
array_push($stack, $i * $j);
break;
case '/':
// 避免除以零
if ($j == 0) {
throw new InvalidArgumentException("Division by zero.");
}
array_push($stack, $i / $j);
break;
case '%':
// 取模操作数必须为整数
if (!is_int($i) || !is_int($j)) {
throw new InvalidArgumentException("Modulo operator requires integer operands.");
}
if ($j == 0) {
throw new InvalidArgumentException("Modulo by zero.");
}
array_push($stack, $i % $j);
break;
}
} else {
// 如果是数字,直接推入栈
array_push($stack, $item);
}
}
return $stack[0]; // 最终结果在栈顶
}
?>代码解析要点:
将以上所有函数组合在一个文件中,即可进行测试。
<?php
// 辅助函数
function readnumber($string, &$i) {
$number = '';
$start_i = $i;
while ($i < strlen($string) && is_number($string{$i})) {
$number .= $string{$i};
$i++;
}
$i = $start_i;
return $number;
}
function is_operator($char) {
static $operators = array('+', '-', '/', '*', '%');
return in_array($char, $operators);
}
function is_number($char) {
return (($char == '.') || ($char >= '0' && $char <= '9'));
}
// 核心函数
function calculate($exp) {
return calculate_rpn(mathexp_to_rpn($exp));
}
function calculate_rpn($rpnexp) {
$stack = array();
foreach($rpnexp as $item) {
if (is_operator($item)) {
$j = array_pop($stack);
$i = array_pop($stack);
switch ($item) {
case '+': array_push($stack, $i + $j); break;
case '-': array_push($stack, $i - $j); break;
case '*': array_push($stack, $i * $j); break;
case '/':
if ($j == 0) throw new InvalidArgumentException("Division by zero.");
array_push($stack, $i / $j);
break;
case '%':
if (!is_int($i) || !is_int($j)) throw new InvalidArgumentException("Modulo operator requires integer operands.");
if ($j == 0) throw new InvalidArgumentException("Modulo by zero.");
array_push($stack, $i % $j);
break;
}
} else {
array_push($stack, $item);
}
}
return $stack[0];
}
function mathexp_to_rpn($mathexp) {
$precedence = array(
'(' => 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) && end($operator_stack) != '(' && $precedence[$char] <= $precedence[end($operator_stack)]) {
array_push($final_stack, array_pop($operator_stack));
}
array_push($operator_stack, $char);
$i++; continue;
}
if ($char == '(') {
array_push($operator_stack, $char);
$i++; continue;
}
if ($char == ')') {
while (!empty($operator_stack) && ($operator = array_pop($operator_stack)) != '(') {
array_push($final_stack, $operator);
}
$i++; continue;
}
$i++; // 忽略其他字符,例如空格
}
while ($oper = array_pop($operator_stack)) {
array_push($final_stack, $oper);
}
return $final_stack;
}
// 使用示例
try {
$expression1 = "27+38+81+48*33*53+91*53+82*14+96";
echo "表达式: " . $expression1 . " = " . calculate($expression1) . "\n"; // 预期输出: 90165
$expression2 = "3 + 2 * (5 - 1)";
echo "表达式: " . $expression2 . " = " . calculate($expression2) . "\n"; // 预期输出: 11
$expression3 = "(10 + 20) / 5 - 3";
echo "表达式: " . $expression3 . " = " . calculate($expression3) . "\n"; // 预期输出: 3
$expression4 = "10 / 3";
echo "表达式: " . $expression4 . " = " . calculate($expression4) . "\n"; // 预期输出: 3.333...
$expression5 = "10 % 3";
echo "表达式: " . $expression5 . " = " . calculate($expression5) . "\n"; // 预期输出: 1
// 尝试除以零
// $expression_error = "5 / 0";
// echo "表达式: " . $expression_error . " = " . calculate($expression_error) . "\n";
} catch (InvalidArgumentException $e) {
echo "计算错误: " . $e->getMessage() . "\n";
}
?>通过调度场算法将中缀表达式转换为逆波兰表示法,并利用栈结构对 RPN 表达式进行求值,我们成功地在 PHP 中实现了一个不依赖 eval() 函数的数学表达式计算器。这种方法不仅避免了 eval() 带来的安全风险,还提供了一个清晰、可控且易于理解的表达式处理机制。虽然当前实现有一些限制,但其模块化的设计为未来功能的扩展和优化奠定了坚实的基础。
以上就是PHP实现数学表达式解析与计算:基于逆波兰表示法(不使用eval())的详细内容,更多请关注php中文网其它相关文章!
PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号