C#后缀表达式解析计算字符串公式

x33g5p2x  于9个月前 转载在 C#  
字(10.9k)|赞(0)|评价(0)|浏览(391)

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

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

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

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

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

  1. public enumBracketEnum
  2. {
  3. /// <summary>
  4. ///Undefined
  5. /// </summary>
  6. Undefined = 0,
  7. /// <summary>
  8. ///左括号
  9. /// </summary>
  10. Open,
  11. /// <summary>
  12. ///右括号
  13. /// </summary>
  14. Close
  15. }

View Code

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

  1. public enumNodeTypeEnum
  2. {
  3. /// <summary>
  4. ///Null
  5. /// </summary>
  6. Null = 0,
  7. /// <summary>
  8. ///操作数
  9. /// </summary>
  10. Number,
  11. /// <summary>
  12. ///操作符
  13. /// </summary>
  14. Operator,
  15. /// <summary>
  16. ///括号
  17. /// </summary>
  18. Bracket,
  19. }

View Code

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

  1. public enumOperatorEnum
  2. {
  3. /// <summary>
  4. ///Undefined
  5. /// </summary>
  6. Undefined = 0,
  7. /// <summary>
  8. ///+
  9. /// </summary>
  10. Plus,
  11. /// <summary>
  12. ///-
  13. /// </summary>
  14. Minus,
  15. /// <summary>
  16. ///*
  17. /// </summary>
  18. Multiply,
  19. /// <summary>
  20. ////
  21. /// </summary>
  22. Divide,
  23. /// <summary>
  24. ///^
  25. /// </summary>
  26. Power,
  27. }

View Code

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

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

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

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

  1. public classBaseNode
  2. {
  3. publicBaseNode(NodeTypeEnum nodeType)
  4. {
  5. NodeType =nodeType;
  6. }
  7. /// <summary>
  8. ///节点类型
  9. /// </summary>
  10. public NodeTypeEnum NodeType { get; set; }
  11. }

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

  1. public classBracketNode : BaseNode
  2. {
  3. /// <summary>
  4. ///括号值
  5. /// </summary>
  6. public BracketEnum Bracket { get; }
  7. /// <summary>
  8. ///公式括号节点
  9. /// </summary>
  10. public BracketNode(BracketEnum bracket) : base(NodeTypeEnum.Bracket)
  11. {
  12. Bracket =bracket;
  13. }
  14. }

View Code

  1. public classNumberNode : BaseNode
  2. {
  3. /// <summary>
  4. ///数字值
  5. /// </summary>
  6. public double Number { get; }
  7. public NumberNode(double number) : base(NodeTypeEnum.Number)
  8. {
  9. Number =number;
  10. }
  11. }

View Code

  1. public classOperatorNode : BaseNode
  2. {
  3. /// <summary>
  4. ///操作字符串枚举
  5. /// </summary>
  6. public OperatorEnum OperatorKey { get; }
  7. /// <summary>
  8. ///优先级
  9. /// </summary>
  10. public int Priority { get; }
  11. public OperatorNode(OperatorEnum operatorKey) : base(NodeTypeEnum.Operator)
  12. {
  13. OperatorKey =operatorKey;
  14. Priority =GetPriority();
  15. }
  16. private intGetPriority()
  17. {
  18. var priority = OperatorKey switch{
  19. OperatorEnum.Power => 6,
  20. OperatorEnum.Multiply => 5,
  21. OperatorEnum.Divide => 5,
  22. OperatorEnum.Plus => 4,
  23. OperatorEnum.Minus => 4,
  24. _ => 0};
  25. returnpriority;
  26. }
  27. }

View Code

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

  1. public static classBracketReader
  2. {
  3. /// <summary>
  4. ///左右括号字符
  5. /// </summary>
  6. private const char OPEN_BRACKET_CHAR = '(';
  7. private const char CLOSE_BRACKET_CHAR = ')';
  8. /// <summary>
  9. ///尝试获取左括号
  10. /// </summary>
  11. /// <param name="nodes">公式节点信息</param>
  12. /// <param name="formula">公式字符</param>
  13. /// <param name="index">公式读取的下标</param>
  14. /// <returns></returns>
  15. public static bool TryProceedOpenBracket(List<BaseNode> nodes, ReadOnlySpan<char> formula, ref intindex)
  16. {
  17. if(formula[index].Equals(OPEN_BRACKET_CHAR))
  18. {
  19. nodes.Add(newBracketNode(BracketEnum.Open));
  20. return true;
  21. }
  22. return false;
  23. }
  24. /// <summary>
  25. ///尝试获取右括号
  26. /// </summary>
  27. /// <param name="nodes">公式节点信息</param>
  28. /// <param name="formula">公式字符</param>
  29. /// <param name="index">公式读取的下标</param>
  30. /// <returns></returns>
  31. public static bool TryProceedCloseBracket(List<BaseNode> nodes, ReadOnlySpan<char> formula, ref intindex)
  32. {
  33. if(formula[index].Equals(CLOSE_BRACKET_CHAR))
  34. {
  35. nodes.Add(newBracketNode(BracketEnum.Close));
  36. return true;
  37. }
  38. return false;
  39. }
  40. }

View Code

  1. public static classNumberReader
  2. {
  3. /// <summary>
  4. ///尝试读取数字
  5. /// </summary>
  6. public static bool TryProceedNumber(List<BaseNode> nodes, ReadOnlySpan<char> formula, ref intindex)
  7. {
  8. double value = 0;
  9. var isTry = false;//是否转换成功
  10. var isNegative = formula[index] == '-';//是否是负数
  11. var localIndex = isNegative ? index + 1: index;
  12. //循环判断数字
  13. for (int i = localIndex; i < formula.Length; i++)
  14. {
  15. var ch =formula[i];
  16. var isLastChar = i + 1 ==formula.Length;
  17. if(IsFloatingNumber(ch))
  18. {
  19. //如果最后一个并且成功
  20. if (isLastChar && double.TryParse(formula.Slice(index, formula.Length - index), outvalue))
  21. {
  22. index =i;
  23. isTry = true;
  24. break;
  25. }
  26. }
  27. else if(double.TryParse(formula.Slice(index, i - index), outvalue))
  28. {
  29. //如果不是数字比如是字母,则直接判断之前的数字
  30. index = i - 1;
  31. isTry = true;
  32. break;
  33. }
  34. else{
  35. break;
  36. }
  37. }
  38. if(isTry)
  39. {
  40. nodes.Add(newNumberNode(value));
  41. }
  42. returnisTry;
  43. }
  44. /// <summary>
  45. ///判断是不是数字或者.
  46. /// </summary>
  47. /// <param name="ch">字符</param>
  48. /// <returns></returns>
  49. private static bool IsFloatingNumber(charch)
  50. {
  51. //是不是十进制数
  52. var isDigit = char.IsDigit(ch);
  53. return isDigit || ch == '.';
  54. }
  55. }

View Code

  1. /// <summary>
  2. ///操作符解读
  3. /// </summary>
  4. public static classOperatorReader
  5. {
  6. private static readonly string[] _operators = new[] { "+", "-", "*", "/", "^"};
  7. /// <summary>
  8. ///尝试获取操作符
  9. /// </summary>
  10. public static bool TryProceedOperator(List<BaseNode> nodes, ReadOnlySpan<char> formula, ref intindex)
  11. {
  12. if(_operators.Contains(formula[index].ToString()))
  13. {
  14. nodes.Add(newOperatorNode(GetOperatorKey(formula[index].ToString())));
  15. return true;
  16. }
  17. return false;
  18. }
  19. /// <summary>
  20. ///获取对应枚举
  21. /// </summary>
  22. /// <param name="name"></param>
  23. /// <returns></returns>
  24. private static OperatorEnum GetOperatorKey(stringname)
  25. {
  26. return name switch{
  27. "+" =>OperatorEnum.Plus,
  28. "-" =>OperatorEnum.Minus,
  29. "*" =>OperatorEnum.Multiply,
  30. "/" =>OperatorEnum.Divide,
  31. "^" =>OperatorEnum.Power,
  32. _ =>OperatorEnum.Undefined
  33. };
  34. }
  35. }

View Code

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

  1. /// <summary>
  2. ///解析公式为节点
  3. /// </summary>
  4. /// <param name="formula">公式字符串</param>
  5. /// <returns></returns>
  6. public static List<BaseNode> AnalysisFormulaToNodes(stringformula)
  7. {
  8. var nodes = new List<BaseNode>();
  9. for(var index = 0;index< formula.Length; index++)
  10. {
  11. if (NumberReader.TryProceedNumber(nodes, formula.AsSpan(), refindex))
  12. continue;
  13. if (OperatorReader.TryProceedOperator(nodes, formula.AsSpan(), refindex))
  14. continue;
  15. if (BracketReader.TryProceedOpenBracket(nodes, formula.AsSpan(), refindex))
  16. continue;
  17. if (BracketReader.TryProceedCloseBracket(nodes, formula.AsSpan(), refindex))
  18. continue;
  19. }
  20. returnnodes;
  21. }

** 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栈。

具体实现代码如下:

  1. /// <summary>
  2. ///转为后缀表达式
  3. /// </summary>
  4. /// <param name="nodes"></param>
  5. /// <returns></returns>
  6. public static List<BaseNode> GetRPN(List<BaseNode>nodes)
  7. {
  8. var rpnNodes = new List<BaseNode>();
  9. var tempNodes = new Stack<BaseNode>();
  10. foreach(var t innodes)
  11. {
  12. //1、如果是操作数直接入栈
  13. if(t.NodeType ==NodeTypeEnum.Number)
  14. {
  15. rpnNodes.Add(t);
  16. continue;
  17. }
  18. //2、若取出的字符是运算符,则循环比较S1栈顶的运算符(包括左括号)优先级,如果栈顶的运算符优先级大于等于该运算符的优先级,则S1栈顶运算符弹出加入到S2中直至不满足条件为止,最后将该运算符送入S1中。
  19. if (t.NodeType ==NodeTypeEnum.Operator)
  20. {
  21. while (tempNodes.Count > 0)
  22. {
  23. var peekOperatorNode = tempNodes.Peek() asOperatorNode;
  24. if (peekOperatorNode != null && peekOperatorNode.Priority >= (t asOperatorNode).Priority)
  25. {
  26. rpnNodes.Add(tempNodes.Pop());
  27. }
  28. else{
  29. break;
  30. }
  31. }
  32. tempNodes.Push(t);
  33. continue;
  34. }
  35. //3、若取出的字符是“(”,则直接送入S1栈顶
  36. if(t.NodeType ==NodeTypeEnum.Bracket)
  37. {
  38. if((t as BracketNode).Bracket ==BracketEnum.Open)
  39. {
  40. tempNodes.Push(t);
  41. continue;
  42. }
  43. }
  44. //4、若取出的字符是“)”,则将距离S1栈栈顶最近的“(”之间的运算符,逐个出栈,依次送入S2栈,此时抛弃“(”。
  45. if (t.NodeType ==NodeTypeEnum.Bracket)
  46. {
  47. if ((t as BracketNode).Bracket ==BracketEnum.Close)
  48. {
  49. while (tempNodes.Count > 0)
  50. {
  51. var peekBracketNode = tempNodes.Peek() asBracketNode;
  52. if (tempNodes.Peek().NodeType == NodeTypeEnum.Bracket && peekBracketNode != null && peekBracketNode.Bracket ==BracketEnum.Open)
  53. {
  54. break;
  55. }
  56. else{
  57. rpnNodes.Add(tempNodes.Pop());
  58. }
  59. }
  60. tempNodes.Pop();
  61. continue;
  62. }
  63. }
  64. //5、重复上述步骤
  65. }
  66. if(tempNodes.Count > 0)
  67. {
  68. rpnNodes.Add(tempNodes.Pop());
  69. }
  70. returnrpnNodes;
  71. }

View Code

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出栈,执行dc的操作,得到结果e,再将e入栈(0位置)

经过以上运算,计算机就可以得到(a+b)*c的运算结果e了。
具体实现代码如下:

  1. /// <summary>
  2. ///计算后缀表达式
  3. /// </summary>
  4. /// <param name="nodes"></param>
  5. /// <returns></returns>
  6. public static double CalculationRPN(List<BaseNode>nodes)
  7. {
  8. double result = 0;
  9. Stack<BaseNode> stack = new Stack<BaseNode>();
  10. foreach(var t innodes)
  11. {
  12. if(t.NodeType ==NodeTypeEnum.Number)
  13. {
  14. //操作数直接入栈
  15. stack.Push(t);
  16. }
  17. else if(t.NodeType ==NodeTypeEnum.Operator)
  18. {
  19. //操作符弹出栈顶两个进行计算
  20. var a =stack.Pop();
  21. var b =stack.Pop();
  22. var operate = t asOperatorNode;
  23. var value = operate.OperatorKey switch{
  24. //数学操作符
  25. OperatorEnum.Multiply =>OperatorService.Multiply(a, b),
  26. OperatorEnum.Divide =>OperatorService.Divide(a, b),
  27. OperatorEnum.Plus =>OperatorService.Plus(a, b),
  28. OperatorEnum.Minus =>OperatorService.Minus(a, b),
  29. OperatorEnum.Power =>OperatorService.Power(a, b),
  30. };
  31. stack.Push(newNumberNode(value));
  32. }
  33. }
  34. result = (stack.Pop() asNumberNode).Number;
  35. returnresult;
  36. }

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

  1. /// <summary>
  2. ///操作符服务
  3. /// </summary>
  4. public static classOperatorService
  5. {
  6. #region Math
  7. public static double Multiply(in BaseNode a, inBaseNode b)
  8. {
  9. var (result, _a, _b) =IsNumber(a, b);
  10. if(result)
  11. {
  12. return _a *_b;
  13. }
  14. return default;
  15. }
  16. public static double Divide(in BaseNode a, inBaseNode b)
  17. {
  18. var (result, _a, _b) =IsNumber(a, b);
  19. if(result)
  20. {
  21. return _a /_b;
  22. }
  23. return default;
  24. }
  25. public static double Plus(in BaseNode a, inBaseNode b)
  26. {
  27. var (result, _a, _b) =IsNumber(a, b);
  28. if(result)
  29. {
  30. return _a +_b;
  31. }
  32. return default;
  33. }
  34. public static double Minus(in BaseNode a, inBaseNode b)
  35. {
  36. var (result, _a, _b) =IsNumber(a, b);
  37. if(result)
  38. {
  39. return _a -_b;
  40. }
  41. return default;
  42. }
  43. public static double Power(in BaseNode a, inBaseNode b)
  44. {
  45. var (result, _a, _b) =IsNumber(a, b);
  46. if(result)
  47. {
  48. returnMath.Pow(_a, _b);
  49. }
  50. return default;
  51. }
  52. /// <summary>
  53. ///判断是不是数字类型,并返回数字
  54. /// </summary>
  55. /// <param name="a"></param>
  56. /// <returns></returns>
  57. private static (bool,double,double) IsNumber(BaseNode a, inBaseNode b)
  58. {
  59. if(a.NodeType == NodeTypeEnum.Number && b.NodeType ==NodeTypeEnum.Number)
  60. {
  61. var _a = a asNumberNode;
  62. var _b = b asNumberNode;
  63. return (true, _a.Number, _b.Number);
  64. }
  65. return (false, default, default);
  66. }
  67. #endregion}

View Code

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

  1. /// <summary>
  2. ///计算
  3. /// </summary>
  4. /// <param name="formula">公式字符串</param>
  5. /// <returns></returns>
  6. public static double Calculation(stringformula)
  7. {
  8. //1、获取公式节点
  9. var nodes =AnalysisFormulaToNodes(formula);
  10. //2、转后缀表达式
  11. var rpnNodes =GetRPN(nodes);
  12. //3、计算对后缀表达式求值
  13. var result =CalculationRPN(rpnNodes);
  14. returnresult;
  15. }

相关文章

最新文章

更多