讲师中心 微信公众号
AI工具推荐 视频效率加速

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

阿墨同学_8838

阿墨同学_8838

发布时间:2025-11-15 12:22:01

|

252人浏览过

|

来源于php中文网

原创

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 的优点在于,它在求值时无需考虑运算符优先级,只需按照从左到右的顺序处理即可,这大大简化了计算逻辑。

立即学习“PHP免费学习笔记(深入)”;

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

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

算法的核心规则如下:

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

实现步骤与代码解析

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

Model Switcher
Model Switcher

根据任务复杂度自动切换不同的AI模型。支持OpenClaw设置中的所有可用模型。当检测到复杂...

下载

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

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

<?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速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
蛙蛙写作

一款AI论文写作工具,主要用于超级AI智能写作助手,适合需要提升相关任务效率的用户。

PixPix
PixPix Hot

PixPix是一款面向电商视觉生产的AI商品图生成工具。

AionClaw
AionClaw Hot

AionClaw是一款面向办公、创作和编程任务的AI桌面智能体。

Laper
Laper Hot

Laper是专为编剧、导演和制片人推出的 AI 原生剧本创作工具。

火山引擎

火山引擎是一款面向企业的云计算与AI服务平台。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

PixTV
PixTV Hot

PixTV是一款面向AIGC内容创作的AI视频生成工具。

相关专题

更多
java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

6104

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

3004

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

1792

2025.10.17

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

1758

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

2744

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

6104

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

5769

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

7414

2024.03.22

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

0

2026.10.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
墨刀帮助中心
墨刀帮助中心

共0课时 | 0人学习

MyEclipse学习中心
MyEclipse学习中心

共0课时 | 0人学习

Apache Subversion 官方手册
Apache Subversion 官方手册

共0课时 | 0人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn