当提到表达式解析技术时,很多人第一反应可能是复杂且精细的递归下降方法。这种方法主要用于构建抽象语法树(AST),虽然功能强大,能够处理复杂的语法结构,但它通常需要较高的编程技巧和对语法分析的深入理解。对于初学者来说,这种方法可能显得有些复杂。因此,我们的目标是从简洁实用的角度出发,分享一种更适合初学者的表达式求值解析方法,即后缀表达式,也称为逆波兰表示法(Reverse Polish Notation, RPN)。RPN的优点在于可以直接通过栈操作进行高效的求值,而不需要构建复杂的语法树。

我们将从零开始,用纯C#语言实现这种后缀表达式的转换和求值方法。这种方法不仅易于理解和实现,而且在性能上也非常高效。通过这个过程,你将能够快速掌握表达式解析的基本概念和实现技巧,为后续学习更复杂的解析技术打下坚实的基础。

一、用程序写一个计算器

在编程入门阶段,实现一个简单的计算器是一个非常有价值的学习案例。以下是一个简单的示例:

  • 输入: 1
  • 输入: +
  • 输入: 2

    如果程序能够返回3,那么恭喜你,你已经掌握了变量的基本类型(如数字和字符串的转换)、基本操作符的使用(如+号求和),以及基本的输入输出操作。

    如果你能够轻松实现上述简单的计算器功能,那么不妨更进一步,思考如何实现更复杂的表达式计算,例如:
CSharp
1+2*(5-1)/(2+3)^2

应该如何用程序来实现呢?

这种表达式的解析和求值是一种常见的需求。在许多场景中,我们希望能够输入公式而不仅仅是一个数值,以此来实现更多的动态配置。例如,用户可以自定义数据分析的规则和逻辑,自定义报表数据的展示,或者在开发过程中提高动态配置的灵活性,减少硬编码,增强扩展性,实现某些插件化功能。

接下来,我们将从零开始,用纯C#代码实现数值表达式的处理。所有的代码均为纯C#实现,100%无第三方依赖,免费开源,方便学习交流。希望大家关注并点赞。

二、表达式基础概念

在计算机科学中,表达式的求值是一个非常常见的任务。无论是简单的计算器程序,还是复杂的编译器设计,都离不开对表达式的解析和计算。我们首先来了解几种常见的表达式类型。

2.1 中缀表达式

我们观察公式:

"(4+2)*3"

在这个公式中,操作符(如+和)位于操作数(如4、2、3)的中间,将要计算的前后数值连接起来。我们称这类公式为中缀表达式。中缀表达式是一种通用的算术或逻辑公式表示方法,操作符(如加号+、乘号等)位于它所连接的两个操作数之间。我们日常使用的数学表达式大多是中缀表达式。这种对两个操作数进行运算的称为双目运算符。如果操作符只负责运算一个操作数(如-1中的负号-),我们称为单目运算符。

2.2 后缀(RPN)表达式

与中缀表达式不同,后缀表达式中的操作符位于操作数之后。例如,上述中缀表达式转换为后缀表达式为:

"3 4 2 + *"

后缀表达式的计算步骤是从左往右依次检查字符。如果是数字,则跳至下一位置;如果是操作符,则按操作符要求向前面位置取数进行计算,并将结果存储在当前位置。最终留下的就是结果。

以"3 4 2 + *"为例,我们从左往右进行计算:

  • 第一位是3,跳至下一位置
  • 第二位是4,跳至下一位置
  • 第三位是2,跳至下一位置
  • 第四位是+,为双目操作符,向前面第二和第三位置取数4、2,执行+运算,得到6
  • 合并第二、第三、第四位置为位置二
  • 存储计算结果6至位置2,此时原式变为 "3 6 *"
  • 第三位是,为双目操作符,向新的表达式前面第一和第二位置取数3和6,执行运算,得到18

后缀表达式既不需要括号明确运算顺序,也不需要预先知道操作符优先级。这种无歧义的特点,特别适合计算机的存储与执行。除了中缀和后缀表达式外,还有前缀表达式,它们在各自的特定领域有其优势。

接下来,我们将详细介绍如何将中缀表达式转换为后缀表达式,以及如何对后缀表达式进行求值。

三、中缀表达式转后缀表达式(RPN)

3.1 算符优先级定义

为了理解中缀表达式,我们首先需要理解操作符的两个属性:操作目数和优先级。

  • 操作目数:操作符的操作目数决定了中缀表达式中操作符可以操作多少个操作数。如果操作目数与实际操作数字不匹配,则可视为表达式出错。
  • 优先级:操作符的优先级决定了中缀表达式中的运算规则。例如,乘除法优先于加减法,因为乘除操作符的优先级更高。类似地,我们可以定义幂运算的优先级比乘除操作符更高,布尔运算符的优先级最低等。如果操作优先级相同,我们默认按照从左到右的顺序执行计算。

    此外,括号具有特殊的优先级。在外部操作符识别时,括号需要单独判定,但在运算时,其优先级低于其他操作符。

在C#中,我们可以定义一个Dictionary<char, int>来存储操作符的优先级:

private static Dictionary<char, int> operatorPrecedence
= new()
{
{'+', 1},
{'-', 1},
{'*', 2},
{'/', 2},
{'^', 3},
{'(', 0}, // 特殊优先级,外部单独判定
{')', 0} // 特殊优先级,外部单独判定
};

接下来,我们需要实现两个函数,分别用于判定一个字符是否为操作符,以及获取操作符的优先级:

// 判断是否是操作符
private static bool IsOperator(char c)
{
return operatorPrecedence.ContainsKey(c);
} //获取操作符优先级
private static int GetPrecedence(char c)
{
return operatorPrecedence[c];
}
3.2 转换流程

在开始转换之前,我们需要准备一个栈operatorStack,用来临时存放操作符(如+、-、*、/),以及一个列表outputList,用来存放最终的后缀表达式:

  Stack<char> operatorStack = new Stack<char>();
List<string> outputList = new List<string>();

然后,我们开始逐个处理表达式中的字符:

  • 如果当前字符是数字 0-9,直接将其添加到outputList中。
  • 如果当前字符是左括号 (,直接将其压入operatorStack栈中。
  • 如果当前字符是右括号 ),从operatorStack栈中弹出操作符,并将其添加到outputList中,直到遇到左括号。遇到左括号后,将其从栈中弹出,但不添加到outputList中。
  • 如果当前字符是操作符(如+、-、*、/),比较当前操作符和栈顶操作符的优先级。如果栈顶操作符的优先级大于或等于当前操作符的优先级,将栈顶操作符弹出,并将其添加到outputList中。重复这个过程,直到栈顶操作符的优先级小于当前操作符的优先级。然后,将当前操作符压入operatorStack栈中。

遍历完表达式后,operatorStack栈中可能还有剩余的操作符。将这些操作符依次弹出,并添加到outputList中。最终,outputList中的内容就是后缀表达式。

3.3 手动试算

让我们试算一下 "(4+2)*3":

  • 第一个字符是"(", 那么压入operatorStack栈中,此时状态:
operatorStack:(
outputList: 空
  • 第二个字符是4,直接加入outputList,此时状态:
operatorStack:(
outputList: 4
  • 第三个字符是+,比较栈顶操作符 (, +优先级更高直接入栈,此时状态:
operatorStack:( +
outputList: 4
  • 第四个字符是2,,直接加入outputList,此时状态:
operatorStack:( +
outputList: 4 2
  • 第五个字符是),从operatorStack弹出操作符到outputList,直到左括号,此时状态:
operatorStack:空
outputList: 4 2 +
  • 第六个字符是*,此时栈为空,直接入栈,此时状态:
operatorStack:*
outputList: 4 2 +
  • 第七个字符是3,直接加入outputList,此时状态:
operatorStack:*
outputList: 4 2 + 3
  • 字符遍历结束,operatorStack依此出栈加入outputList,此时状态:
operatorStack:空
outputList: 4 2 + 3 *
3.4 详细的代码实现

按上述逻辑设计后,具体代码实现如下:

public static List<string> InfixToPostfix(string expression)
{
Stack<char> operatorStack = new Stack<char>();
List<string> outputList = new List<string>(); foreach (char c in expression) //遍历每个字符
{
if (char.IsDigit(c)) // 如果是数字,直接输出
{
outputList.Add(c.ToString());
}
else if (c == '(') // 左括号,直接压入栈
{
operatorStack.Push(c);
}
else if (c == ')') // 右括号,弹出操作符直到遇到左括号
{
while (operatorStack.Count > 0 && operatorStack.Peek() != '(')
{
outputList.Add(operatorStack.Pop().ToString());
}
if (operatorStack.Count > 0 && operatorStack.Peek() == '(')
{
operatorStack.Pop(); // 弹出左括号
}
}
else if (IsOperator(c)) // 操作符
{
while (operatorStack.Count > 0 && GetPrecedence(operatorStack.Peek()) >= GetPrecedence(c))
{
outputList.Add(operatorStack.Pop().ToString());
}
operatorStack.Push(c);
}
} // 将栈中剩余的操作符依次弹出并输出
while (operatorStack.Count > 0)
{
outputList.Add(operatorStack.Pop().ToString());
} return outputList;
}

最后返回的List就是我们的后缀表达式了。

四、后缀表达式求解(RPN)

后缀表达式的求解非常简单。首先准备一个栈用来存放数字,然后逐个读取表达式的每个部分。如果是数字,就压入栈;如果是操作符,就从栈中弹出对应的操作数进行计算,然后将结果压回栈。计算完成后,栈里剩下的最后一个数字就是表达式的结果。

以下是具体的代码实现:

public static double EvaluatePostfix(IList<string> postfix)
{
Stack<double> stack = new Stack<double>(); foreach (string token in postfix)
{
if (double.TryParse(token, out double number)) // 如果是数字,压入栈
{
stack.Push(number);
}
else // 如果是操作符
{
double b = stack.Pop(); // 弹出第二个操作数
double a = stack.Pop(); // 弹出第一个操作数 switch (token)
{
case "+":
stack.Push(a + b);
break;
case "-":
stack.Push(a - b);
break;
case "*":
stack.Push(a * b);
break;
case "^":
stack.Push(Math.Pow(a,b));
break;
case "/":
if (b == 0)
throw new DivideByZeroException("除数不能为零");
stack.Push(a / b);
break;
default:
throw new ArgumentException("无效的操作符: " + token);
}
}
} return stack.Pop(); // 栈中剩下的最后一个元素就是结果
}

输入之前的案例效果如下:

输入: 1+2*(5-1)/(2+3)^2
输出: 1 2 5 1 - * 2 3 + 2 ^ / +
求值: 1.32

五、最后

通过上述内容,我们从零开始,用纯C#语言实现了一个简单的表达式解析器。我们首先介绍了中缀表达式和后缀表达式的基本概念,然后详细讨论了如何将中缀表达式转换为后缀表达式,以及如何对后缀表达式进行求值。这种方法不仅易于理解和实现,而且在性能上也非常高效,特别适合初学者学习和实践。希望这篇文章对你有所帮助,也欢迎大家在学习过程中提出问题和建议,欢迎随时交流!

本文中代码项目已在仓库完全开源了!点个 Star ️支持一下!代码仓库地址 https://github.com/LdotJdot/RPN,其他项目可关注微信公众号,发送消息 “TDS”,"Json"即可查看

感谢您的耐心阅读,希望各位从零开始的新朋友和老朋友有所收获!如果你对这篇文章的内容有任何建议或想法,欢迎随时交流!关注微信公众号‘萤火初芒’

从零开始:C#实现计算表达式解析与求值——以后缀表达式为例的更多相关文章

  1. leetcode算法学习----逆波兰表达式求值(后缀表达式)

    下面题目是LeetCode算法:逆波兰表达式求值(java实现) 逆波兰表达式即后缀表达式. 题目:  有效的运算符包括 +, -, *, / .每个运算对象可以是整数,也可以是另一个逆波兰表达式.同 ...

  2. [Java]手动构建表达式二叉树,求值,求后序表达式

    Inlet类,这颗二叉树是”人力运维“的: package com.hy; public class Inlet { public static void main(String[] args) th ...

  3. 计算后缀表达式的过程(C#)

    计算后缀表达式的过程是一个很好玩的过程,而且很简单哦!这里呢,有个计算的技巧,就是:遇到数字直接入栈,遇到运算符就计算! 后缀表达式也叫逆波兰表达式,求值过程可以用到栈来辅助存储: 假定待求值的后缀表 ...

  4. C++之字符串表达式求值

    关于字符串表达式求值,应该是程序猿们机试或者面试时候常见问题之一,昨天参加国内某IT的机试,压轴便为此题,今天抽空对其进行了研究. 算术表达式中最常见的表示法形式有 中缀.前缀和 后缀表示法.中缀表示 ...

  5. 【部分原创】标准C语言的优先级、结合性、求值顺序、未定义行为和非确定行为浅析

    零. 优先级    在C++ Primer一书中,对于运算符的优先级是这样描述的:     Precedence specifies how the operands are grouped. It ...

  6. 在C#开发中使用第三方组件LambdaParser、DynamicExpresso、Z.Expressions,实现动态解析/求值字符串表达式

    在进行项目开发的时候,刚好需要用到对字符串表达式进行求值的处理场景,因此寻找了几个符合要求的第三方组件LambdaParser.DynamicExpresso.Z.Expressions,它们各自功能 ...

  7. Java堆栈的应用2----------中缀表达式转为后缀表达式的计算Java实现

    1.堆栈-Stack 堆栈(也简称作栈)是一种特殊的线性表,堆栈的数据元素以及数据元素间的逻辑关系和线性表完全相同,其差别是线性表允许在任意位置进行插入和删除操作,而堆栈只允许在固定一端进行插入和删除 ...

  8. [Java]算术表达式求值之三(中序表达式转二叉树方案 支持小数)

    Entry类 这个类对表达式的合法性进行了粗筛: package com.hy; import java.io.BufferedReader; import java.io.IOException; ...

  9. 表达式计算 java 后缀表达式

    题目: 问题描述 输入一个只包含加减乖除和括号的合法表达式,求表达式的值.其中除表示整除. 输入格式 输入一行,包含一个表达式. 输出格式 输出这个表达式的值. 样例输入 1-2+3*(4-5) 样例 ...

  10. BUAA-OO-表达式解析与求导

    BUAA-OO-表达式解析与求导 解析 按照常规,解析这一部分我们分为词法分析与语法分析.当然由于待解析的字符串较简单,词法分析器和语法分析器不必单独实现. 词法分析器 按照常规,我们先手写一个词法分 ...

随机推荐

  1. java 两个线程交替打印到100

    简介 使用 volatile code /** * Created by lee on 2021/7/5. */ public class Counter { static volatile int ...

  2. java 聊天 两个进程互相通信开两个线程

    简介 RT code server package com.kuang; import java.io.BufferedReader; import java.io.IOException; impo ...

  3. TSNE 初步认识

    简介 文章链接 https://www.jmlr.org/papers/volume9/vandermaaten08a/vandermaaten08a.pdf 讲解的比较好的链接 https://zh ...

  4. POLIR-Politics-Political Economics: "零知识" VS "零风险+负成本" : "宣扬'创新'与'极致'"是对组织"最有利"的方式?美国🇺🇸政府不是选最有利的。

    POLIR-Politics-Political Economics: 名词解释 零知识: 前人没有的"知识(与经验)" 源于Google运用"AlphaGo(AI人工智 ...

  5. Math-Derivative导数-夹逼定理:常用的不等式放大缩小 + log对数的妙用:将 嵌套的 指数运算 转换为 指数的乘法 与 将 幂函数 的 乘法 转换为 指数的加法运算

    https://zhuanlan.zhihu.com/p/396423540 1. (A**b)**(c) = A**(b * c) # 将 指数运算的嵌套 转换 为 指数的 乘法运算 2.(A**b ...

  6. CnPack IDE 专家包(CnWizards)-九五小庞

    CnPack 是由互联网上一群 中国程序员 开发的 开放源码 的 自由软件 项目,当前主要的工作成果包括 CnWizards 专家包.CnPack 组件包以及 CVSTracNT 错误跟踪系统等.Cn ...

  7. 雨林木风win11网络正常电脑却连不上网的问题

    近来有雨林木风系统用户在使用Windows 11系统的时候,网络显示正常,但是却连不上网的情况,这应该怎么办呢?接下来,windows11官网小编就把解决方法分享给大家.方法如下:1.首先我们可以按下 ...

  8. Win11系统电脑开机只显示鼠标的问题

    有的电脑基地的小伙伴在使用win1纯净版系统的时候,在电脑开机的时候屏幕只显示鼠标的问题,那么碰到这种情况应该怎么办呢?下面技术员小编就来分享具体的解决方法吧.Win11 纯净版系统电脑开机只显示鼠标 ...

  9. Java8 stream collect groupBy分组的简单例子

    实体类People,有个返回list的buildPeopleList方法,方便测试. import lombok.AllArgsConstructor; import lombok.Builder; ...

  10. PostgreSQL TRUNCATE TABLE

    PostgreSQL 中 TRUNCATE TABLE 用于删除表的数据,但不删除表结构. 也可以用 DROP TABLE 删除表,但是这个命令会连表的结构一起删除,如果想插入数据,需要重新建立这张表 ...