当我们拿到一个字符串比如:20+31*(100+1)的时候用口算就能算出结果为3151,因为这是中缀表达式对于人类的思维很简单,但是对于计算机就比较复杂了。相对的后缀表达式适合计算机进行计算。

我们就从简单到复杂,逐步实现对公式的解析(下述的代码没有经过严格验证,可能会存在极端情况的BUG,作为一种思路仅供参考,商用环境还需细细修改)。

实现简单的数字的加减乘除

我们从实现简单的数字的加减乘除开始主要是提供一个思路有需要可以自己修改扩展(推荐一个项目写的感觉就不错https://github.com/KovtunV/NoStringEvaluating),那么我们只需要关注加减乘除等操作符、左右括号和操作数(整数、小数和负数),所以我们先建立三个枚举类BracketEnumNodeTypeEnumOperatorEnum如下:

BracketEnum是括号枚举,也就是左右括号"()"

public enum BracketEnum
{
/// <summary>
/// Undefined
/// </summary>
Undefined = 0,
/// <summary>
/// 左括号
/// </summary>
Open,
/// <summary>
/// 右括号
/// </summary>
Close
}

NodeTypeEnum是节点类型枚举,就简单分为操作符、操作数和括号

public enum NodeTypeEnum
{
/// <summary>
/// Null
/// </summary>
Null = 0,
/// <summary>
/// 操作数
/// </summary>
Number,
/// <summary>
/// 操作符
/// </summary>
Operator,
/// <summary>
/// 括号
/// </summary>
Bracket,
}

OperatorEnum是操作符枚举,主要就是加减乘除这些简单的

public enum OperatorEnum
{
/// <summary>
/// Undefined
/// </summary>
Undefined = 0,
/// <summary>
/// +
/// </summary>
Plus,
/// <summary>
/// -
/// </summary>
Minus,
/// <summary>
/// *
/// </summary>
Multiply,
/// <summary>
/// /
/// </summary>
Divide,
/// <summary>
/// ^
/// </summary>
Power,
}

然后我们需要做以下三步:

  1. 解析公式将字符转化为便于操作的节点信息
  2. 进行解析为后缀表达式
  3. 进行计算

 1、解析公式转为节点信息

根据我们的NodeTypeEnum节点类型枚举我们需要三个不同的节点信息类方便我们的操作,我们先创建基类BaseNode以后的节点类都继承它

public class BaseNode
{
public BaseNode(NodeTypeEnum nodeType)
{
NodeType = nodeType;
}
/// <summary>
/// 节点类型
/// </summary>
public NodeTypeEnum NodeType { get; set; }
}

然后我们分别创建BracketNodeNumberNodeOperatorNode类,分别是括号节点信息、操作数节点新和操作符节点信息,它们各有自己的具体实现,如下:

public class BracketNode : BaseNode
{
/// <summary>
/// 括号值
/// </summary>
public BracketEnum Bracket { get; }
/// <summary>
/// 公式括号节点
/// </summary>
public BracketNode(BracketEnum bracket) : base(NodeTypeEnum.Bracket)
{
Bracket = bracket;
}
}

public class NumberNode : BaseNode
{
/// <summary>
/// 数字值
/// </summary>
public double Number { get; }
public NumberNode(double number) : base(NodeTypeEnum.Number)
{
Number = number;
}
}

public class OperatorNode : BaseNode
{
/// <summary>
/// 操作字符串枚举
/// </summary>
public OperatorEnum OperatorKey { get; }
/// <summary>
/// 优先级
/// </summary>
public int Priority { get; }
public OperatorNode(OperatorEnum operatorKey) : base(NodeTypeEnum.Operator)
{
OperatorKey = operatorKey;
Priority = GetPriority();
}
private int GetPriority()
{
var priority = OperatorKey switch
{
OperatorEnum.Power => 6,
OperatorEnum.Multiply => 5,
OperatorEnum.Divide => 5,
OperatorEnum.Plus => 4,
OperatorEnum.Minus => 4,
_ => 0
};
return priority;
}
}

有了节点信息类,那我们肯定还要有对应的解析类分别是BracketReader(括号解析)NumberReader(操作数解析)OperatorReader(操作符解析),解析类就是为了将公式字符串解析为对应的节点信息具体如下:

public static class BracketReader
{
/// <summary>
/// 左右括号字符
/// </summary>
private const char OPEN_BRACKET_CHAR = '(';
private const char CLOSE_BRACKET_CHAR = ')';
/// <summary>
/// 尝试获取左括号
/// </summary>
/// <param name="nodes">公式节点信息</param>
/// <param name="formula">公式字符</param>
/// <param name="index">公式读取的下标</param>
/// <returns></returns>
public static bool TryProceedOpenBracket(List<BaseNode> nodes, ReadOnlySpan<char> formula, ref int index)
{
if (formula[index].Equals(OPEN_BRACKET_CHAR))
{
nodes.Add(new BracketNode(BracketEnum.Open));
return true;
}
return false;
}
/// <summary>
/// 尝试获取右括号
/// </summary>
/// <param name="nodes">公式节点信息</param>
/// <param name="formula">公式字符</param>
/// <param name="index">公式读取的下标</param>
/// <returns></returns>
public static bool TryProceedCloseBracket(List<BaseNode> nodes, ReadOnlySpan<char> formula, ref int index)
{
if (formula[index].Equals(CLOSE_BRACKET_CHAR))
{
nodes.Add(new BracketNode(BracketEnum.Close));
return true;
}
return false;
}
}

    public static class NumberReader
{
/// <summary>
/// 尝试读取数字
/// </summary>
public static bool TryProceedNumber(List<BaseNode> nodes, ReadOnlySpan<char> formula, ref int index)
{
double value = 0;
var isTry = false;//是否转换成功
var isNegative = formula[index] == '-';//是否是负数
var localIndex = isNegative ? index + 1 : index;
//循环判断数字
for (int i = localIndex; i < formula.Length; i++)
{
var ch = formula[i];
var isLastChar = i + 1 == formula.Length; if (IsFloatingNumber(ch))
{
//如果最后一个并且成功
if (isLastChar && double.TryParse(formula.Slice(index, formula.Length - index), out value))
{
index = i;
isTry = true;
break;
}
}
else if(double.TryParse(formula.Slice(index, i - index), out value))
{
//如果不是数字比如是字母,则直接判断之前的数字
index = i - 1;
isTry = true;
break;
}
else
{
break;
}
}
if (isTry)
{
nodes.Add(new NumberNode(value));
}
return isTry;
}
/// <summary>
/// 判断是不是数字或者.
/// </summary>
/// <param name="ch">字符</param>
/// <returns></returns>
private static bool IsFloatingNumber(char ch)
{
//是不是十进制数
var isDigit = char.IsDigit(ch);
return isDigit || ch == '.';
}
}

    /// <summary>
/// 操作符解读
/// </summary>
public static class OperatorReader
{
private static readonly string[] _operators = new[] { "+", "-", "*", "/", "^" }; /// <summary>
/// 尝试获取操作符
/// </summary>
public static bool TryProceedOperator(List<BaseNode> nodes, ReadOnlySpan<char> formula, ref int index)
{
if (_operators.Contains(formula[index].ToString()))
{
nodes.Add(new OperatorNode(GetOperatorKey(formula[index].ToString())));
return true;
}
return false;
}
/// <summary>
/// 获取对应枚举
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
private static OperatorEnum GetOperatorKey(string name)
{
return name switch
{
"+" => OperatorEnum.Plus,
"-" => OperatorEnum.Minus,
"*" => OperatorEnum.Multiply,
"/" => OperatorEnum.Divide,
"^" => OperatorEnum.Power, _ => OperatorEnum.Undefined
};
}
}

有了以上的准备,我们就可以将公式转为我们的节点信息了如下

        /// <summary>
/// 解析公式为节点
/// </summary>
/// <param name="formula">公式字符串</param>
/// <returns></returns>
public static List<BaseNode> AnalysisFormulaToNodes(string formula)
{
var nodes = new List<BaseNode>();
for(var index = 0;index< formula.Length; index++)
{
if (NumberReader.TryProceedNumber(nodes, formula.AsSpan(), ref index))
continue;
if (OperatorReader.TryProceedOperator(nodes, formula.AsSpan(), ref index))
continue;
if (BracketReader.TryProceedOpenBracket(nodes, formula.AsSpan(), ref index))
continue;
if (BracketReader.TryProceedCloseBracket(nodes, formula.AsSpan(), ref index))
continue;
}
return nodes;
}

 2、转为后缀表达式

转为后缀表达式需要执行以下条件:

首先需要分配2个栈,一个作为临时存储运算符的栈S1(含一个结束符号),一个作为存放结果(逆波兰式)的栈S2(空栈),S1栈可先放入优先级最低的运算符#,注意,中缀式应以此最低优先级的运算符结束。可指定其他字符,不一定非#不可。从中缀式的左端开始取字符,逐序进行如下步骤:
(1)若取出的字符是操作数,则分析出完整的运算数,该操作数直接送入S2栈。
(2)若取出的字符是运算符,则将该运算符与S1栈栈顶元素比较,如果该运算符(不包括括号运算符)优先级高于S1栈栈顶运算符(包括左括号)优先级,则将该运算符进S1栈,否则,将S1栈的栈顶运算符弹出,送入S2栈中,直至S1栈栈顶运算符(包括左括号)低于(不包括等于)该运算符优先级时停止弹出运算符,最后将该运算符送入S1栈。
(3)若取出的字符是“(”,则直接送入S1栈顶。
(4)若取出的字符是“)”,则将距离S1栈栈顶最近的“(”之间的运算符,逐个出栈,依次送入S2栈,此时抛弃“(”。
(5)重复上面的1~4步,直至处理完所有的输入字符。
(6)若取出的字符是“#”,则将S1栈内所有运算符(不包括“#”),逐个出栈,依次送入S2栈。
具体实现代码如下:

        /// <summary>
/// 转为后缀表达式
/// </summary>
/// <param name="nodes"></param>
/// <returns></returns>
public static List<BaseNode> GetRPN(List<BaseNode> nodes)
{
var rpnNodes = new List<BaseNode>();
var tempNodes = new Stack<BaseNode>();
foreach(var t in nodes)
{
//1、如果是操作数直接入栈
if(t.NodeType == NodeTypeEnum.Number)
{
rpnNodes.Add(t);
continue;
}
//2、若取出的字符是运算符,则循环比较S1栈顶的运算符(包括左括号)优先级,如果栈顶的运算符优先级大于等于该运算符的优先级,则S1栈顶运算符弹出加入到S2中直至不满足条件为止,最后将该运算符送入S1中。
if (t.NodeType == NodeTypeEnum.Operator)
{
while (tempNodes.Count > 0)
{
var peekOperatorNode = tempNodes.Peek() as OperatorNode;
if (peekOperatorNode != null && peekOperatorNode.Priority >= (t as OperatorNode).Priority)
{
rpnNodes.Add(tempNodes.Pop());
}
else
{
break;
} }
tempNodes.Push(t);
continue;
}
//3、若取出的字符是“(”,则直接送入S1栈顶
if(t.NodeType == NodeTypeEnum.Bracket)
{
if((t as BracketNode).Bracket == BracketEnum.Open)
{
tempNodes.Push(t);
continue;
}
}
//4、若取出的字符是“)”,则将距离S1栈栈顶最近的“(”之间的运算符,逐个出栈,依次送入S2栈,此时抛弃“(”。
if (t.NodeType == NodeTypeEnum.Bracket)
{
if ((t as BracketNode).Bracket == BracketEnum.Close)
{
while (tempNodes.Count > 0)
{
var peekBracketNode = tempNodes.Peek() as BracketNode;
if (tempNodes.Peek().NodeType == NodeTypeEnum.Bracket && peekBracketNode != null && peekBracketNode.Bracket == BracketEnum.Open)
{
break;
}
else
{
rpnNodes.Add(tempNodes.Pop());
}
}
tempNodes.Pop();
continue;
}
}
//5、重复上述步骤
}
if(tempNodes.Count > 0)
{
rpnNodes.Add(tempNodes.Pop());
}
return rpnNodes;
}

3、计算后缀表达式

以(a+b)*c为例子进行说明:
(a+b)*c的逆波兰式为ab+c*,假设计算机把ab+c*按从左到右的顺序压入栈中,并且按照遇到运算符就把栈顶两个元素出栈,执行运算,得到的结果再入栈的原则来进行处理,那么ab+c*的执行结果如下:
1)a入栈(0位置)
2)b入栈(1位置)
3)遇到运算符“+”,将a和b出栈,执行a+b的操作,得到结果d=a+b,再将d入栈(0位置)
4)c入栈(1位置)
5)遇到运算符“*”,将d和c出栈,执行d*c的操作,得到结果e,再将e入栈(0位置)
经过以上运算,计算机就可以得到(a+b)*c的运算结果e了。
具体实现代码如下:
        /// <summary>
/// 计算后缀表达式
/// </summary>
/// <param name="nodes"></param>
/// <returns></returns>
public static double CalculationRPN(List<BaseNode> nodes)
{
double result = 0;
Stack<BaseNode> stack = new Stack<BaseNode>();
foreach(var t in nodes)
{
if(t.NodeType == NodeTypeEnum.Number)
{
//操作数直接入栈
stack.Push(t);
}
else if(t.NodeType == NodeTypeEnum.Operator)
{
//操作符弹出栈顶两个进行计算
var a = stack.Pop();
var b = stack.Pop();
var operate = t as OperatorNode;
var value = operate.OperatorKey switch
{
// 数学操作符
OperatorEnum.Multiply => OperatorService.Multiply(a, b),
OperatorEnum.Divide => OperatorService.Divide(a, b),
OperatorEnum.Plus => OperatorService.Plus(a, b),
OperatorEnum.Minus => OperatorService.Minus(a, b),
OperatorEnum.Power => OperatorService.Power(a, b),
}; stack.Push(new NumberNode(value));
}
}
result = (stack.Pop() as NumberNode).Number;
return result;
}

数学操作符执行代码如下主要为了进行加减乘除简单的计算:

    /// <summary>
/// 操作符服务
/// </summary>
public static class OperatorService
{
#region Math public static double Multiply(in BaseNode a, in BaseNode b)
{
var (result, _a, _b) = IsNumber(a, b);
if (result)
{
return _a * _b;
}
return default;
} public static double Divide(in BaseNode a, in BaseNode b)
{
var (result, _a, _b) = IsNumber(a, b);
if (result)
{
return _a / _b;
}
return default;
} public static double Plus(in BaseNode a, in BaseNode b)
{
var (result, _a, _b) = IsNumber(a, b);
if (result)
{
return _a + _b;
}
return default;
} public static double Minus(in BaseNode a, in BaseNode b)
{
var (result, _a, _b) = IsNumber(a, b);
if (result)
{
return _a - _b;
}
return default;
} public static double Power(in BaseNode a, in BaseNode b)
{
var (result, _a, _b) = IsNumber(a, b);
if (result)
{
return Math.Pow(_a, _b);
}
return default;
}
/// <summary>
/// 判断是不是数字类型,并返回数字
/// </summary>
/// <param name="a"></param>
/// <returns></returns>
private static (bool,double,double) IsNumber(BaseNode a, in BaseNode b)
{
if(a.NodeType == NodeTypeEnum.Number && b.NodeType == NodeTypeEnum.Number)
{
var _a = a as NumberNode;
var _b = b as NumberNode;
return (true, _a.Number, _b.Number);
}
return (false, default, default);
}
#endregion
}

最后串在一起就能得到结果啦,就像下面这样

        /// <summary>
/// 计算
/// </summary>
/// <param name="formula">公式字符串</param>
/// <returns></returns>
public static double Calculation(string formula)
{
//1、获取公式节点
var nodes = AnalysisFormulaToNodes(formula);
//2、转后缀表达式
var rpnNodes = GetRPN(nodes);
//3、计算对后缀表达式求值
var result = CalculationRPN(rpnNodes);
return result;
}

C#后缀表达式解析计算字符串公式的更多相关文章

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

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

  2. c语言,中缀表达式转后缀表达式并计算

    //c语言中缀表达式计算 #include <stdio.h> #include <stdlib.h> #include <string.h> #include & ...

  3. C++实现计算器功能(包括计算含未知量的式子),输出后缀表达式

    大概描述        用c++语言在vc中实现部分数学计算功能.其中实现的数学计算功能包括加减乘除运算.开方计算.自然对数运算.以10为底的对数运算.幂计算.正弦余弦计算. 由用户输入要计算的表达式 ...

  4. Python与数据结构[1] -> 栈/Stack[1] -> 中缀表达式与后缀表达式的转换和计算

    中缀表达式与后缀表达式的转换和计算 目录 中缀表达式转换为后缀表达式 后缀表达式的计算 1 中缀表达式转换为后缀表达式 中缀表达式转换为后缀表达式的实现方式为: 依次获取中缀表达式的元素, 若元素为操 ...

  5. RPN-逆波兰计算器-中缀表达式转后缀表达式-javascript

    1.利用栈(Stack)来存储操作数和操作符: 2.包含中缀表达式转后缀表达式的函数,这个是难点,也是关键点: 2.1.将输入字符串转为数组: 2.2.对转换来的字符进行遍历:创建一个数组,用来给存储 ...

  6. 数据结构Java实现06----中缀表达式转换为后缀表达式

    本文主要内容: 表达式的三种形式 中缀表达式与后缀表达式转换算法 一.表达式的三种形式: 中缀表达式:运算符放在两个运算对象中间,如:(2+1)*3.我们从小做数学题时,一直使用的就是中缀表达式. 后 ...

  7. C语言数据结构之栈:中缀表达式的计算

    *注:本人技术不咋的,就是拿代码出来和大家看看,代码漏洞百出,完全没有优化,主要看气质,是吧 学了数据结构——栈,当然少不了习题.习题中最难的也是最有意思的就是这个中缀表达式的计算了(可以算+-*/和 ...

  8. 中缀表达式转后缀表达式(Java代码实现)

    后缀表达式求值 后缀表达式又叫逆波兰表达式,其求值过程可以用到栈来辅助存储.例如要求值的后缀表达式为:1 2 3 + 4 * + 5 -,则求值过程如下: 遍历表达式,遇到数字时直接入栈,栈结构如下 ...

  9. 【后缀表达式求解】No.3.栈-evaluate-reverse-polish-notation题解(Java版)

    牛客网的题目链接 题目描述 Evaluate the value of an arithmetic expression in Reverse Polish Notation. Valid opera ...

  10. P1090 后缀表达式

    题目描述 所谓后缀表达式是指这样的一个表达式:式中不再引用括号,运算符号放在两个运算对象之后,所有计算按运算符号出现的顺序,严格地由左而右新进行(不用考虑运算符的优先级). 如:3(5–2)+7对应的 ...

随机推荐

  1. 第1章-Spring的模块与应用场景

    目录 一.Spring模块 1. 核心模块 2. AOP模块 3. 消息模块 4. 数据访问模块 5. Web模块 6. 测试模块 二.集成功能 1. 目标原则 2. 支持组件 三.应用场景 1. 典 ...

  2. Class文件解析

    1 准备工作 获取class文件byte[] public static byte[] getFileBytes(File file) { try (FileInputStream fileInput ...

  3. 【每日一题】【初始节点初始化,前一个为空】2022年1月7日-NC78 反转链表

    描述给定一个单链表的头结点pHead,长度为n,反转该链表后,返回新链表的表头. 数据范围: n\leq1000n≤1000要求:空间复杂度 O(1)O(1) ,时间复杂度 O(n)O(n) . 如当 ...

  4. 【离线数仓】Day01-用户行为数据采集:数仓概念、需求及架构、数据生成及采集、linux命令及其他组件常见知识

    一.数据仓库概念 二.项目需求及架构设计 1.需求分析 2.项目框架 3.框架版本选型 服务器选型:云主机 服务器规划 三.数据生成模块 1.数据基本格式 公共字段:所有手机都包含 业务字段:埋点上报 ...

  5. static_cast和dynamic_cast

    C++的强制类型转换,除了继承自C语言的写法((目标类型)表达式)之外,还新增了4个关键字,分别是:static_cast.dynamic_cast.const_cast和reinterpret_ca ...

  6. 解决 ERROR: Could not find a version that satisfies the requirement xxx 的问题

    解决 ERROR: Could not find a version that satisfies the requirement xxx 的问题 1.解决 ERROR: Could not find ...

  7. Spring IOC官方文档学习笔记(三)之依赖项

    1.依赖注入 (1) 依赖注入(DI)的概念:某个bean的依赖项,由容器来负责注入维护,而非我们自己手动去维护,以此来达到bean之间解耦的目的,如下 //情况一:不使用依赖注入 public cl ...

  8. 关于jQuery的操作

    jQuery简介  简化了JS  ​ 类似于 后端 JDBC(操作数据库的基本API)   dbutils(封装JDBC)     xxx.jar 前端 JS                     ...

  9. vue3+TS 自定义指令:长按触发绑定的函数

    vue3+TS 自定义指令:长按触发绑定的函数 而然间看到一个在vue2中写的长按触发事件的自定义指定,想着能不能把他copy到我的vue3项目中呢. 编写自定义指令时遇到的几个难点 1.自定义指令的 ...

  10. Centos7下vim最新版本安装

    一直以来用的都是vim,因为之前都是系统自带的vim没有研究过怎么自己安装,今天趁着刚装完新系统,顺便装下vim. 同样vim也有两种安装方法: 一.yum安装,centos下安装软件最简单的方法了, ...