聊聊前序、中序、后序表达式

news/2025/9/24 15:51:46/文章来源:https://www.cnblogs.com/hont/p/19109354

在游戏开发中,我们经常需要在配置表中定义各种公式,比如 a * (b + c),用来计算技能伤害、属性加成等。如果直接让程序在运行时解析并执行这些公式,就需要处理运算符优先级和括号等复杂问题。

这时,后序表达式就派上了用场。我们将中序表达式 a * (b + c) 转换为后序表达式 a b c + *,这样程序只需要一个栈就能高效计算,无需担心优先级和括号。

而要深入理解后序表达式,就不得不提到与之相关的完整概念体系:前序表达式、中序表达式和后序表达式。这三种表达式各有特点,共同构成了计算机处理数学表达式的理论基础。

 

1.中序(中缀)&前序表达式

中序表达式就是日常书写的公式,但这种日常公式对计算机并不友好,有括号、优先级等需要考虑,计算机直接读取较为麻烦

因此波兰数学家扬·武卡谢维奇发明了"波兰表示法"(前序表达式)

所以前序表达式又叫做 波兰式

 

例如LISP的语法风格就是基于前序表达式的:

;; 中序: 1 + 2 * 3
;; 前序: 
+ 1 * 2 3    ; => 7;; 中序: (1 + 2) * 3  
;; 前序:
* + 1 2 3   ; => 9

而我们现在使用较多的则是后续表达式,因为中序表达式转换为后续表达式内存开销较少,计算更为友好。

2.后序表达式(逆波兰式)

几个逆波兰式的例子:

中序:1 + 2
后序:1 2 +中序:1 + 2 * 3
后序:1 2 3 * +中序:(1 + 2) * 3
后序:1 2 + 3 *

 

通常可以将表达式从中序转逆序后存放在内存里,供计算时使用。也可以通过双栈法直接计算出结果。

常见的中序转逆序算法如下。

2.1 中序转后序表达式

示例公式:(A + B) * C

首先创建栈结构存放符号

从左向右读取原公式,读到非符号输出,读到符号以如下规则操作:

  • 若栈为空,读到符号可以直接入栈
  • * / 优先级高于 + -,将读取到的符号与栈顶符号做比较,若<=栈顶符号,则出栈(循环操作,直到条件不满足),并输出
  • 若读取到左括号(,则连同括号一起入栈。若读取到右括号),则循环出栈,直到找到左括号位置
  • 若所有内容读取完,则输出栈的内容

注:输出是指将值添加至返回值字符串或其他数据格式

 

参照上面写的示例公式,操作流程如下:

  1. (  入栈
    栈:(
    输出:空

  2. A  输出
    输出:A

  3. +  入栈
    栈:(, +
    输出:A

  4. B 输出
    输出:A B

  5. ) 弹出直到遇到 ( → 弹出 +
    栈:空
    输出:A B +

  6. * 栈空 , 入栈
    栈:*
    输出:A B +

  7. C 输出
    输出:A B + C

结束后弹栈:输出 A B + C *

 

通常游戏配表中的公式可运用到此方法,以带变量公式为例,进行代码演示

1 + 2 * 3 + (4 * e + f) * g

 

代码如下:

ConvertRpn("1 + 2 * 3 + (4 * e + f) * g", out var rpnStr);
Debug.Log(rpnStr);//1 2 3 * + 4 e * f + g * +
void ConvertRpn(string inStr, out string rpnStr)
{int GetPriority(char ch){return ch switch{'+' or '-' => 1,'*' or '/' => 2,'(' => 0,    // 左括号在栈内优先级最低_ => -1      // 其他情况(包括右括号)
        };}var rpnBuilder = new StringBuilder();var stack = new Stack<char>();var numBuilder = new StringBuilder();foreach (var ch in inStr){if (Char.IsWhiteSpace(ch)){// 遇到空格时,如果正在构建数字,就完成这个数字if (numBuilder.Length > 0){rpnBuilder.Append(numBuilder.ToString()).Append(' ');numBuilder.Clear();}continue;}if (char.IsDigit(ch) || ch == '.'){// 数字或小数点:添加到数字构建器
            numBuilder.Append(ch);continue;}else{// 遇到操作符前,先完成当前数字的构建if (numBuilder.Length > 0){rpnBuilder.Append(numBuilder.ToString()).Append(' ');numBuilder.Clear();}}switch (ch){case '(':stack.Push(ch);break;case ')':// 弹出直到遇到左括号while (stack.Count > 0 && stack.Peek() != '('){rpnBuilder.Append(stack.Pop()).Append(' ');}if (stack.Count > 0 && stack.Peek() == '('){stack.Pop(); // 弹出左括号但不输出
                }break;case '+':case '-':case '*':case '/':// 处理操作符优先级while (stack.Count > 0 && stack.Peek() != '(' &&GetPriority(ch) <= GetPriority(stack.Peek())){rpnBuilder.Append(stack.Pop()).Append(' ');}stack.Push(ch);break;default:rpnBuilder.Append(ch).Append(' ');break;}}// 处理最后一个数字(如果存在)if (numBuilder.Length > 0){rpnBuilder.Append(numBuilder.ToString()).Append(' ');}// 弹出栈中剩余操作符while (stack.Count > 0){rpnBuilder.Append(stack.Pop()).Append(' ');}// 移除末尾多余的空格if (rpnBuilder.Length > 0 && rpnBuilder[rpnBuilder.Length - 1] == ' '){rpnBuilder.Length--;}rpnStr = rpnBuilder.ToString();
}

 这样就可以将公式转换为后序表达式先存放在内存中了。

2.2 求解后序表达式

求解后续表达式,同样需要一个栈结构来辅助。

规则是:

    1. 遇到操作数(数字/变量) → 压入栈。

    2. 遇到运算符 → 从栈中弹出两个操作数:

      • 先弹出的是右操作数

      • 再弹出的是左操作数
        然后进行运算,把结果压回栈。

    3. 扫描完后,栈顶就是最终结果

代码如下:

rpnStr = "1 2 3 * + 4 e * f + g * +";
var result = CalcPrn(rpnStr, new Dictionary<char, float> { ['e'] = 10f, ['f'] = 12f, ['g'] = 14f });
Debug.Log(result);// 735
float CalcPrn(string rpnStr, Dictionary<char, float> replace)
{var rpnBuilder = new StringBuilder();var stack = new Stack<object>();var numBuilder = new StringBuilder();foreach (var ch in rpnStr){if (Char.IsWhiteSpace(ch)){if (numBuilder.Length > 0){stack.Push(float.Parse(numBuilder.ToString()));numBuilder.Clear();}continue;}if (char.IsDigit(ch) || ch == '.'){numBuilder.Append(ch);continue;}else{if (numBuilder.Length > 0){stack.Push(float.Parse(numBuilder.ToString()));numBuilder.Clear();}}if (ch is '+' or '-' or '*' or '/'){var x = (float)stack.Pop();var y = (float)stack.Pop();switch (ch){case '+':stack.Push(x + y);break;case '-':stack.Push(x - y);break;case '*':stack.Push(x * y);break;case '/':stack.Push(x / y);break;}}else // 变量
        {stack.Push(replace[ch]);}}return (float)stack.Pop();
}

 

2.3 双栈法直接求值

使用双栈法,可以不用转换为后序表达式再求值,而是直接求值。

核心思想是使用2个栈,数值栈和字符栈。当字符栈弹出时,顺便就计算当前步骤结果并且放回数值栈。

代码如下:

var result = CalcPrnDoubleStack("1 + 2 * 3 + (4 * e + f) * g", new Dictionary<char, float> { ['e'] = 10f, ['f'] = 12f, ['g'] = 14f });
Debug.Log(result);// 735
 float CalcPrnDoubleStack(string inStr, Dictionary<char, float> replace){int GetPriority(char ch){return ch switch{'+' or '-' => 1,'*' or '/' => 2,'(' => 0,    // 左括号在栈内优先级最低_ => -1      // 其他情况(包括右括号)
         };}float Calc(float x, float y, char token){return token switch{'+' => x + y,'-' => x - y,'*' => x * y,'/' => x / y,_ => default};}var stackNum = new Stack<float>();var stackToken = new Stack<char>();var numBuilder = new StringBuilder();foreach (var ch in inStr){if (Char.IsWhiteSpace(ch)){// 遇到空格时,如果正在构建数字,就完成这个数字if (numBuilder.Length > 0){stackNum.Push(float.Parse(numBuilder.ToString()));numBuilder.Clear();}continue;}if (char.IsDigit(ch) || ch == '.'){// 数字或小数点:添加到数字构建器
             numBuilder.Append(ch);continue;}else{// 遇到操作符前,先完成当前数字的构建if (numBuilder.Length > 0){stackNum.Push(float.Parse(numBuilder.ToString()));numBuilder.Clear();}}switch (ch){case '(':stackToken.Push(ch);break;case ')':while (stackToken.Count > 0 && stackToken.Peek() != '('){var token = stackToken.Pop();var y = stackNum.Pop();var x = stackNum.Pop();stackNum.Push(Calc(x, y, token));}stackToken.Pop();break;case '+':case '-':case '*':case '/':while (stackToken.Count > 0 && stackToken.Peek() != '(' &&GetPriority(ch) <= GetPriority(stackToken.Peek())){var token = stackToken.Pop();var y = stackNum.Pop();var x = stackNum.Pop();stackNum.Push(Calc(x, y, token));}stackToken.Push(ch);break;default:stackNum.Push(replace[ch]);break;}}while (stackToken.Count > 0){var token = stackToken.Pop();var y = stackNum.Pop();var x = stackNum.Pop();stackNum.Push(Calc(x, y, token));}return stackNum.Pop();}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/915887.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

flink书籍 - --

本站整理下载:链接:https://pan.baidu.com/s/1P1bK-zdSuknnBuoAS6ZeXA 提取码:hfy9

天津企业网站建站网站开发技术有哪些

红队内网攻防渗透 1. 内网隧道技术1.1 Frp内网穿透C2上线1.1.1 双网内网穿透C2上线1.1.1.1 服务端配置1.1.1.2 客户端配置1.1.2 内网穿透信息收集1.1.2.1、建立Socks节点(入站没限制采用)1.1.2.2 主动转发数据(出站没限制采用)1.2 Nps内网穿透工具1.2.1 NPS内网穿透C2上线1…

网站排名的重要性培训手机软件开发

创建Dockerfile文件&#xff0c;放到项目根目录下和pom.xml同级别 仅需修改为自己项目端口号即可&#xff0c;其他的无需改动 FROM openjdk:11.0.11-jre-slimCOPY target/*.jar .EXPOSE 8080ENTRYPOINT java -jar *.jar构建语句(注意末尾的点 . ) docker build -t container…

Asp.Net Core 鉴权授权

原文链接: https://blog.csdn.net/Fanbin168/article/details/112431155 以前我们做登陆判断是一般情况是通过实现IAuthorizationFilter 这个过滤器来做的public class CustomAuthorizationFilterAttribute : Attribu…

遇到一款无人机,上面有安全模式和强力模式,十分迷惑二者区别,问了技术说是和碰撞指数有关,涨知识

遇到一款无人机,上面有安全模式和强力模式,十分迷惑二者区别,问了技术说是和碰撞指数有关,涨知识这个碰撞指数对应的是飞机的一个加速度值,飞机加速度发生突变的程度来判断碰撞强度的,到一定的碰撞强度时自动降落…

124

在当今数字化浪潮下,企业面临着信息难以曝光的困扰,精准的内容呈现变得至关重要。而AI语义预检生成的内容,就是解决这一难题的利器。OpenAI与立讯精密的合作,标志着AI技术向消费级硬件领域加速渗透,国内供应链企业…

我的笔记记录方案

俗话说得好,好记性不如烂笔头,可见记好笔记是很重要的。那么我们该如何在电脑上记好笔记呢?下面我就来分享一下,我自己的记笔记方案。 记笔记工具 我使用 Typora 记笔记,这需要使用 MarkDown 语法。 关于 MarkDow…

AT_arc156_d [ARC156D] Xor Sum 5

这种东西能出到 5 也是神人了。 首先你需要观察到一个性质就是,这是异或,对于一个不回文的 \(X\) 来说,将其反转得到的和于其一样,可以抵消为 \(0\),因此我们只需要算回文的就好了。此时我们放宽限制,对于所有只…

iOS Provisioning Profile 证书 描述文件

环境:macOS Sequoia15.7 Xcode16.4(16F6) iOS APP开发根据实际情况有可能用到三种描述文件,怕失忆忘记故做此笔记。 创建证书签名请求下一步创建证书时要用到证书签名请求文件,如果之前创建的证书在有效期内,则忽…

告别多工具切换的低效时代:PandaWiki如何无缝集成企业现有工作流?

告别多工具切换的低效时代:PandaWiki如何无缝集成企业现有工作流?在数字化转型的浪潮中,企业知识管理面临着前所未有的挑战。是否厌倦了在钉钉、飞书、企业微信等多个平台间频繁切换?是否困扰于知识库与现有工作流…

计算快速付氏变换FFT前需要加窗函数

FFT前的窗函数选择 在进行快速傅里叶变换(FFT)前对信号加窗,是为了抑制频谱泄漏——这一问题源于信号的非整数周期截断(实际处理中很难保证信号恰好是整数周期),会导致原本集中在单一频率点的能量"扩散&quo…

直播预告| PostgreSQL 与 IvorySQL 在云原生时代的演进与实践

9月29日【IvorySQL】视频号 直播预告| PostgreSQL 与 IvorySQL 在云原生时代的演进与实践 观看直播即有机会获取 IvorySQL 周边礼品,欢迎大家预约哦!观看直播即有机会获取 IvorySQL 周边礼品。欢迎大家预约哦! 直…

催收网站开发上海网络推广培训学校

作者 |Sharmistha Chatterjee翻译 | 火火酱~&#xff0c;责编 | 晋兆雨出品 | CSDN云计算头图 | 付费下载于视觉中国介绍现在&#xff0c;对于各类机构而言&#xff0c;需要收集的数据越来越多&#xff0c;并且时常需要检测不寻常或异常的时间序列。例如&#xff0c;雅虎就拥有…

网站开发年终总结网站为何站长统计

云栖号资讯&#xff1a;【点击查看更多行业资讯】 在这里您可以找到不同行业的第一手的上云资讯&#xff0c;还在等什么&#xff0c;快来&#xff01; 现在&#xff0c;是时候谈一谈 2020 年及以后的软件工程状况了。本文以 GitHub Octoverse 数据为基础&#xff0c;加上我作为…

长春网站建设加q479185700wordpress如何更改导航栏的样式

本文主要介绍go-swagger的安装和使用&#xff0c;首先介绍如何安装swagger&#xff0c;测试是否成功&#xff1b;然后列出常用的注释和给出使用例子&#xff1b;最后生成接口文档&#xff0c;并在浏览器上测试 文章目录 安装注释说明常用注释参考例子 文档生成格式化文档生成do…

中小型电子商务网站怎么做类似站酷的网站

一、需求 使用uniapp开发小程序时&#xff0c;需要调取【记录日活动统计】的接口&#xff0c;而这个接口需要传递一个ip给后台&#xff0c; 那么前端如何获取ip呢&#xff1f;下面代码里可以实现 二、代码实现 1.在项目的manifest.json中配置一下网络权限&#xff1a; &quo…

江苏太仓建设局网站备案网站注意事项

说明&#xff1a; 1&#xff09;访问应用业务&#xff0c;读取不到数据&#xff0c;show databases;查看数据库报错 2&#xff09;重启docker服务&#xff0c;服务启动失败&#xff0c;查看日志报错如下图所示 3&#xff09;报错信息&#xff1a;chmod /data/docker: read-only…

压垮项目经理的“三座大山”:时间、成本、质量的生存法则与破局工具

在项目管理的铁三角中,时间、成本、质量的博弈是每一位项目经理的日常。盲目牺牲任何一方,都可能导致项目崩盘。本文深度拆解如何运用科学方法与现代工具,在这三者的约束中找到最佳平衡点,并介绍如何借助【PJMan项…

最新微信机器人开发教程

最新微信机器人开发教程随着人工智能和自动化技术的快速发展,微信机器人已经成为越来越多人的选择。它们可以帮助我们自动回复 消息、管理群组、发送定时消息等,极大地提高了我们的工作效率。而WTAPI,作为一款开源的…

金蝶AAS (Apusic Application Server) v10 部署SuperMap iServer 2025 详细教程

一、软件版本操作系统: CentOS Linux release 8.3.2011JDK:11.0.18(从iServer11.2.1开始,由于升级pac4j安全框架,JDK需要升级到11版本,如果不用iServer,可以还是使用JDK8版本)金蝶AAS:AAS-V10-sp2SuperMap iSer…