找回密码
 立即注册
首页 业界区 业界 聊聊前序、中序、后序表达式

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

姬宜欣 2025-9-25 21:06:42
在游戏开发中,我们经常需要在配置表中定义各种公式,比如 a * (b + c),用来计算技能伤害、属性加成等。如果直接让程序在运行时解析并执行这些公式,就需要处理运算符优先级和括号等复杂问题。
这时,后序表达式就派上了用场。我们将中序表达式 a * (b + c) 转换为后序表达式 a b c + *,这样程序只需要一个栈就能高效计算,无需担心优先级和括号。
而要深入理解后序表达式,就不得不提到与之相关的完整概念体系:前序表达式、中序表达式和后序表达式。这三种表达式各有特点,共同构成了计算机处理数学表达式的理论基础。
 
1.中序(中缀)&前序表达式

中序表达式就是日常书写的公式,但这种日常公式对计算机并不友好,有括号、优先级等需要考虑,计算机直接读取较为麻烦
因此波兰数学家扬·武卡谢维奇发明了"波兰表示法"(前序表达式)
所以前序表达式又叫做 波兰式
 
例如LISP的语法风格就是基于前序表达式的:
  1. ;; 中序: 1 + 2 * 3
  2. ;; 前序:
  3. + 1 * 2 3    ; => 7
  4. ;; 中序: (1 + 2) * 3  
  5. ;; 前序:
  6. * + 1 2 3   ; => 9
复制代码
而我们现在使用较多的则是后序表达式,因为中序表达式转换为后序表达式内存开销较少,计算更为友好。
2.后序表达式(逆波兰式)

几个逆波兰式的例子:
  1. 中序:1 + 2
  2. 后序:1 2 +
  3. 中序:1 + 2 * 3
  4. 后序:1 2 3 * +
  5. 中序:(1 + 2) * 3
  6. 后序:1 2 + 3 *
复制代码
 
通常可以将表达式从中序转逆序后存放在内存里,供计算时使用。也可以通过双栈法直接计算出结果。
常见的中序转逆序算法如下。
2.1 中序转后序表达式

示例公式:(A + B) * C
首先创建栈结构存放符号
从左向右读取原公式,读到非符号输出,读到符号以如下规则操作:
<ul>若栈为空,读到符号可以直接入栈
* / 优先级高于 + -,将读取到的符号与栈顶符号做比较,若 1,            '*' or '/' => 2,            '(' => 0,    // 左括号在栈内优先级最低            _ => -1      // 其他情况(包括右括号)        };    }    var rpnBuilder = new StringBuilder();    var stack = new Stack();    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)  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();}[/code] 这样就可以将公式转换为后序表达式先存放在内存中了。
2.2 求解后序表达式

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

    • 遇到操作数(数字/变量) → 压入栈。
    • 遇到运算符 → 从栈中弹出两个操作数:

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

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

代码如下:
  1. ConvertRpn("1 + 2 * 3 + (4 * e + f) * g", out var rpnStr);
  2. Debug.Log(rpnStr);//1 2 3 * + 4 e * f + g * +
复制代码
  1. void ConvertRpn(string inStr, out string rpnStr)
  2. {
  3.     int GetPriority(char ch)
  4.     {
  5.         return ch switch
  6.         {
  7.             '+' or '-' => 1,
  8.             '*' or '/' => 2,
  9.             '(' => 0,    // 左括号在栈内优先级最低
  10.             _ => -1      // 其他情况(包括右括号)
  11.         };
  12.     }
  13.     var rpnBuilder = new StringBuilder();
  14.     var stack = new Stack<char>();
  15.     var numBuilder = new StringBuilder();
  16.     foreach (var ch in inStr)
  17.     {
  18.         if (Char.IsWhiteSpace(ch))
  19.         {
  20.             // 遇到空格时,如果正在构建数字,就完成这个数字
  21.             if (numBuilder.Length > 0)
  22.             {
  23.                 rpnBuilder.Append(numBuilder.ToString()).Append(' ');
  24.                 numBuilder.Clear();
  25.             }
  26.             continue;
  27.         }
  28.         if (char.IsDigit(ch) || ch == '.')
  29.         {
  30.             // 数字或小数点:添加到数字构建器
  31.             numBuilder.Append(ch);
  32.             continue;
  33.         }
  34.         else
  35.         {
  36.             // 遇到操作符前,先完成当前数字的构建
  37.             if (numBuilder.Length > 0)
  38.             {
  39.                 rpnBuilder.Append(numBuilder.ToString()).Append(' ');
  40.                 numBuilder.Clear();
  41.             }
  42.         }
  43.         switch (ch)
  44.         {
  45.             case '(':
  46.                 stack.Push(ch);
  47.                 break;
  48.             case ')':
  49.                 // 弹出直到遇到左括号
  50.                 while (stack.Count > 0 && stack.Peek() != '(')
  51.                 {
  52.                     rpnBuilder.Append(stack.Pop()).Append(' ');
  53.                 }
  54.                 if (stack.Count > 0 && stack.Peek() == '(')
  55.                 {
  56.                     stack.Pop(); // 弹出左括号但不输出
  57.                 }
  58.                 break;
  59.             case '+':
  60.             case '-':
  61.             case '*':
  62.             case '/':
  63.                 // 处理操作符优先级
  64.                 while (stack.Count > 0 && stack.Peek() != '(' &&
  65.                         GetPriority(ch) <= GetPriority(stack.Peek()))
  66.                 {
  67.                     rpnBuilder.Append(stack.Pop()).Append(' ');
  68.                 }
  69.                 stack.Push(ch);
  70.                 break;
  71.             default:
  72.                 rpnBuilder.Append(ch).Append(' ');
  73.                 break;
  74.         }
  75.     }
  76.     // 处理最后一个数字(如果存在)
  77.     if (numBuilder.Length > 0)
  78.     {
  79.         rpnBuilder.Append(numBuilder.ToString()).Append(' ');
  80.     }
  81.     // 弹出栈中剩余操作符
  82.     while (stack.Count > 0)
  83.     {
  84.         rpnBuilder.Append(stack.Pop()).Append(' ');
  85.     }
  86.     // 移除末尾多余的空格
  87.     if (rpnBuilder.Length > 0 && rpnBuilder[rpnBuilder.Length - 1] == ' ')
  88.     {
  89.         rpnBuilder.Length--;
  90.     }
  91.     rpnStr = rpnBuilder.ToString();
  92. }
复制代码
 
2.3 双栈法直接求值

使用双栈法,可以不用转换为后序表达式再求值,而是直接求值。
核心思想是使用2个栈,数值栈和字符栈。当字符栈弹出时,顺便就计算当前步骤结果并且放回数值栈。
代码如下:
  1. rpnStr = "1 2 3 * + 4 e * f + g * +";
  2. var result = CalcPrn(rpnStr, new Dictionary<char, float> { ['e'] = 10f, ['f'] = 12f, ['g'] = 14f });
  3. Debug.Log(result);// 735
复制代码
  1. float CalcPrn(string rpnStr, Dictionary<char, float> replace)
  2. {
  3.     var rpnBuilder = new StringBuilder();
  4.     var stack = new Stack<object>();
  5.     var numBuilder = new StringBuilder();
  6.     foreach (var ch in rpnStr)
  7.     {
  8.         if (Char.IsWhiteSpace(ch))
  9.         {
  10.             if (numBuilder.Length > 0)
  11.             {
  12.                 stack.Push(float.Parse(numBuilder.ToString()));
  13.                 numBuilder.Clear();
  14.             }
  15.             continue;
  16.         }
  17.         if (char.IsDigit(ch) || ch == '.')
  18.         {
  19.             numBuilder.Append(ch);
  20.             continue;
  21.         }
  22.         else
  23.         {
  24.             if (numBuilder.Length > 0)
  25.             {
  26.                 stack.Push(float.Parse(numBuilder.ToString()));
  27.                 numBuilder.Clear();
  28.             }
  29.         }
  30.         if (ch is '+' or '-' or '*' or '/')
  31.         {
  32.             var x = (float)stack.Pop();
  33.             var y = (float)stack.Pop();
  34.             switch (ch)
  35.             {
  36.                 case '+':
  37.                     stack.Push(x + y);
  38.                     break;
  39.                 case '-':
  40.                     stack.Push(x - y);
  41.                     break;
  42.                 case '*':
  43.                     stack.Push(x * y);
  44.                     break;
  45.                 case '/':
  46.                     stack.Push(x / y);
  47.                     break;
  48.             }
  49.         }
  50.         else // 变量
  51.         {
  52.             stack.Push(replace[ch]);
  53.         }
  54.     }
  55.     return (float)stack.Pop();
  56. }
复制代码
 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册