PHP实现数学表达式解析与计算:基于逆波兰表示法(不使用eval())


PHP实现数学表达式解析与计算:基于逆波兰表示法(不使用eval())

本教程将详细介绍如何在php中不使用`eval()`函数,安全有效地计算包含运算符优先级的数学表达式。核心方法是采用调度场算法将中缀表达式转换为逆波兰表示法(rpn),随后利用栈结构对rpn表达式进行求值,从而实现对复杂数学运算的精确处理。

在PHP开发中,直接使用eval()函数来执行用户提供的数学表达式存在严重的安全隐患,因为它允许执行任意的PHP代码。为了避免这种风险,同时又能灵活地处理带有运算符优先级的数学表达式,我们需要一种自定义的解析与计算方案。本教程将深入探讨如何通过将中缀表达式转换为逆波兰表示法(Reverse Polish Notation, RPN)并对其进行求值来实现这一目标。

数学表达式解析的核心原理

处理数学表达式通常涉及两个主要步骤:解析和求值。

1. 中缀表达式与逆波兰表示法 (RPN)

我们日常使用的数学表达式,如 2 + 3 * 4,被称为中缀表达式。它的特点是运算符位于操作数之间,并且需要考虑运算符优先级和括号。

逆波兰表示法(RPN),也称为后缀表达式,是一种没有括号的表达式形式,其中运算符位于其操作数之后。例如,中缀表达式 2 + 3 * 4 对应的 RPN 形式是 2 3 4 * +。RPN 的优点在于,它在求值时无需考虑运算符优先级,只需按照从左到右的顺序处理即可,这大大简化了计算逻辑。

2. 调度场算法 (Shunting-yard Algorithm)

调度场算法是 Dijkstra 提出的一种将中缀表达式转换为 RPN 的经典算法。它利用两个栈:一个用于存储运算符(运算符栈),另一个用于存储输出(输出栈或队列,最终形成 RPN 序列)。

算法的核心规则如下:

  • 遇到数字,直接推入输出栈。
  • 遇到运算符:
    • 如果运算符栈为空,或栈顶是左括号,或当前运算符优先级高于栈顶运算符,则将当前运算符推入运算符栈。
    • 否则(当前运算符优先级小于或等于栈顶运算符),将栈顶运算符弹出并推入输出栈,然后重复此比较,直到满足上述条件或运算符栈为空,最后将当前运算符推入运算符栈。
  • 遇到左括号 (,推入运算符栈。
  • 遇到右括号 ),不断将运算符栈中的运算符弹出并推入输出栈,直到遇到左括号。然后将左括号弹出(但不推入输出栈)。
  • 表达式处理完毕后,将运算符栈中剩余的所有运算符依次弹出并推入输出栈。

实现步骤与代码解析

我们将通过一系列 PHP 函数来实现数学表达式的解析和计算。

1. 整体架构:calculate() 函数

calculate() 函数是整个流程的入口,它负责协调中缀表达式到 RPN 的转换以及 RPN 表达式的求值。

SuperDesign SuperDesign

开源的UI设计AI智能体

SuperDesign 216 查看详情 SuperDesign
<?php

/**
 * 计算中缀数学表达式的结果
 * @param string $exp 中缀数学表达式字符串
 * @return float 表达式计算结果
 */
function calculate($exp) {
    return calculate_rpn(mathexp_to_rpn($exp));
}

// ... 后续辅助函数和核心函数 ...

?>

2. 中缀转 RPN:mathexp_to_rpn() 函数

这个函数实现了调度场算法,将中缀表达式字符串转换为 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'));
}

?>

代码解析要点:

  • $precedence 数组:定义了不同运算符的优先级。括号 ( 的优先级设为0,确保其在运算符栈中不会被其他运算符弹出,直到遇到 )。
  • $final_stack (输出栈):存储最终的 RPN 序列。
  • $operator_stack (运算符栈):临时存储运算符。
  • readnumber() 函数:处理多位数字和浮点数,确保能正确读取整个数字。
  • is_operator() 和 is_number():辅助函数,用于判断字符类型。
  • 循环逻辑:遍历表达式字符串,根据字符类型(数字、运算符、括号)执行调度场算法的相应规则。

3. RPN 求值:calculate_rpn() 函数

这个函数接收 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]; // 最终结果在栈顶
}

?>

代码解析要点:

  • $stack (操作数栈):临时存储数字。
  • 遍历 RPN 数组
    • 遇到数字:推入操作数栈。
    • 遇到运算符:从操作数栈中弹出两个操作数(注意顺序,先弹出的为右操作数),执行相应运算,然后将结果推回操作数栈。
  • 错误处理:增加了对除以零和取模操作数类型的基本检查。

示例代码

将以上所有函数组合在一个文件中,即可进行测试。

<?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";
}

?>

注意事项与扩展

  1. 输入验证:当前实现假定输入是一个格式正确的数学表达式。在生产环境中,应在解析之前对输入字符串进行严格的验证,以防止格式错误的表达式导致程序崩溃或产生意外结果。例如,检查是否包含非法字符、括号是否匹配等。
  2. 浮点数精度:PHP 的浮点数计算可能存在精度问题,尤其是在涉及除法等操作时。如果需要高精度计算,可以考虑使用 PHP 的 BCMath 扩展。
  3. 功能限制
    • 本实现仅支持基本的加减乘除和取模运算,以及括号。
    • 不支持负数(作为操作数,如 -5),但支持减法运算。
    • 不支持一元运算符(如 +5, -5 的前缀形式,除非将其视为 0-5)。
    • 不支持函数调用(如 sin(x))、变量、幂运算等。
    • 不支持科学计数法。
  4. 扩展性:如果需要支持更多运算符(如幂运算 ^)、函数或一元运算符,需要修改 precedence 数组、is_operator 函数以及 calculate_rpn 函数中的运算逻辑,甚至可能需要调整 mathexp_to_rpn 的解析逻辑。
  5. 性能:对于非常长的表达式,这种基于字符遍历和栈操作的方法可能不是最高效的。但对于一般长度的数学表达式,其性能通常足够。

总结

通过调度场算法将中缀表达式转换为逆波兰表示法,并利用栈结构对 RPN 表达式进行求值,我们成功地在 PHP 中实现了一个不依赖 eval() 函数的数学表达式计算器。这种方法不仅避免了 eval() 带来的安全风险,还提供了一个清晰、可控且易于理解的表达式处理机制。虽然当前实现有一些限制,但其模块化的设计为未来功能的扩展和优化奠定了坚实的基础。

以上就是PHP实现数学表达式解析与计算:基于逆波兰表示法(不使用eval())的详细内容,更多请关注php中文网其它相关文章!


# 浮点数  # 房山建设公司网站  # 什么源码利于seo  # 盐都seo价格  # 收废旧物资哪个网站推广  # 杭州网站建设服务平台  # 乐平关键词排名优化  # 山西网站高端建设  # 浏阳营销推广企业  # 使用营销推广的条件  # 立春seo  # 怎么看  # 为空  # php  # 遍历  # 不支持  # 求值  # 转换为  # 弹出  # 波兰  # 运算符  # php 函数  # php开发  # switch  #   # go 


相关栏目: 【 Google疑问12 】 【 Facebook疑问10 】 【 优化推广96088 】 【 技术知识133117 】 【 IDC资讯59369 】 【 网络运营7196 】 【 IT资讯61894


相关推荐: AngularJS动态内容中DOM元素查找的时序问题及$timeout解决方案  热血江湖归来医师加点攻略  申通快递物流信息查询 申通快递包裹状态追踪  知音漫客官网首页入口_知音漫客热门漫画推荐  顺丰快递收费标准查询_如何查看顺丰最新收费价格  小米手机屏幕失灵乱跳怎么办 屏幕触控问题自检与临时解决方法【应急】  优酷官网登录入口电脑版 优酷官网网址入口  J*aScript类型数组_TypedArray使用  cad视图选项卡不见了怎么办_cad视图标签恢复显示方法  芒果TV官网登录入口 芒果TV官方网站登录入口  MacBook Pro词典使用指南  如何在Podman容器中运行Composer_Docker替代品Podman的PHP与Composer容器化实践  《大学搜题酱》官网地址登录  抖音网页版地址直接进入_抖音网页版在线观看入口  使用VS Code调试Python代码:从入门到精通  CSS过渡如何实现按钮悬停效果_transition属性控制背景颜色变化  被称为海蜈蚣的海洋动物是  《浙里办》电子发票开具方法  抖音视频如何添加标题?添加标题有哪些好处?  抖音号怎么解除企业认证改成个人?改成个人有影响吗?  手机坏了微信聊天记录怎么导出来 新手机恢复聊天记录技巧  优化Flask模板中SQLAlchemy查询迭代标签:处理字符串空格问题  《三角洲行动》战斗步枪与机枪类改装代码分享  192.168.1.1路由器后台入口 192.168.1.1默认登录入口  如何用mysql开发用户注册登录功能_mysql用户注册登录数据库设计  C++怎么解决数值计算中的精度问题_C++浮点数误差与数值稳定性分析  12306夜间购票失败? | 查看官方公布的暂停服务公告与应对方案  SQL聚合查询、联接与筛选:GROUP BY 子句的正确使用与常见陷阱  如何在mysql中使用索引提示_mysql索引提示优化方法  win11关机几秒又自己开机 Win11关机自动重启问题修复  Sublime怎么格式化HTML代码_Sublime前端代码美化插件使用指南  酷狗音乐多音轨设置教程  TikTok笔记文字无法编辑如何解决 TikTok笔记文字编辑优化方法  如何取消数字签名  小红书网页版在线直达 小红书网页版免费登录入口  《金山词霸》语音翻译方法  视频号视频怎么免费保存到相册?保存到相册需要注意什么?  《地下城堡4:骑士与破碎编年史》墓穴挑战125攻略  哈尔滨城市通昵称修改方法  sublime text 4如何安装_最新版sublime下载与汉化教程  狙击外星人小游戏在线链接_狙击外星人小游戏网页链接  《海底捞》点外卖方法  Win11便笺在哪打开 Win11桌面便笺(Sticky Notes)使用方法【详解】  win11如何开启单声道音频 Win11为听障用户合并左右声道【辅助】  SQLAlchemy 2.0 与 Pydantic 模型类型安全集成指南  《雷电模拟器》自动点击设置方法  win11如何诊断DirectX问题 Win11运行dxdiag工具排查显卡故障【排错】  PHP utf8_encode 字符编码转换疑难解析与最佳实践  J*a列表元素格式化输出教程  哔哩哔哩黑名单怎么查看 

 2025-11-15

了解您产品搜索量及市场趋势,制定营销计划

同行竞争及网站分析保障您的广告效果

点击免费数据支持

提交您的需求,1小时内享受我们的专业解答。

运城市盐湖区信雨科技有限公司


运城市盐湖区信雨科技有限公司

运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。

 8156699

 13765294890

 8156699@qq.com

Notice

We and selected third parties use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.
You can consent to the use of such technologies by closing this notice, by interacting with any link or button outside of this notice or by continuing to browse otherwise.