2176 字
11 分钟
表达式求值与二叉树遍历教程

本文从零讲解中缀、前缀、后缀三种表达式,以及它们与二叉树三种遍历的对应关系,并给出双栈法、调度场算法、后缀/前缀求值等经典算法。全文用一个例子贯穿:

中缀:3 + 4 * 2 / (1 - 5)^2
后缀:3 4 2 * 1 5 - 2 ^ / +
前缀:+ 3 / * 4 2 ^ - 1 5 2

最终结果都是 3.5。


1. 三种表达式是什么#

1.1 中缀表达式#

人最熟悉的写法,运算符在两个操作数中间:

3 + 4 * 2

特点:有优先级、有括号,计算机直接求值麻烦。

1.2 后缀表达式(逆波兰式 RPN)#

运算符在操作数后面:

3 4 2 * +

特点:没有括号,运算顺序唯一,计算机最容易求值。

1.3 前缀表达式(波兰式 PN)#

运算符在操作数前面:

+ 3 * 4 2

特点:同样没有括号,求值时从右往左扫描即可。


2. 表达式树:三种表达式的统一来源#

任何中缀表达式都可以画成一棵二叉树:

  • 叶子节点:数字
  • 内部节点:运算符
  • 左子树、右子树:参与运算的子表达式

以 3 + 4 * 2 / (1 - 5)^2 为例,表达式树为:

+
/ \
3 /
/ \
* ^
/ \ / \
4 2 - 2
/ \
1 5

2.1 三种遍历对应三种表达式#

遍历方式顺序得到的表达式例子
前序遍历根 → 左 → 右前缀表达式 PN+ 3 / * 4 2 ^ - 1 5 2
中序遍历左 → 根 → 右中缀表达式(需补括号)3 + 4 * 2 / (1 - 5)^2
后序遍历左 → 右 → 根后缀表达式 RPN3 4 2 * 1 5 - 2 ^ / +

一句话总结:同一棵表达式树,三种遍历顺序,得到三种表达式。


3. 中缀表达式求值:双栈法#

双栈法直接扫描中缀表达式,一边读一边算。

3.1 规则#

  • 一个数字栈 num,一个运算符栈 op
  • 从左到右扫描
  • 遇到数字 → 压 num
  • 遇到左括号 → 压 op
  • 遇到右括号 → 弹 op 直到左括号,每弹一个运算符就算一次
  • 遇到运算符 → 比较优先级:
    • 栈顶优先级 高于 当前,或 同级且当前不是 ^,就弹栈顶计算
    • 否则当前运算符入 op
  • 扫描完,把 op 里剩下的全部弹出算完

3.2 模拟 3 + 4 * 2 / (1 - 5)^2#

步骤读到动作num 栈op 栈
13数字入栈[3][]
2+入栈[3][+]
34数字入栈[3,4][+]
4*+ 优先级低,不弹;* 入栈[3,4][+,*]
52数字入栈[3,4,2][+,*]
6/弹 * 算 4*2=8;+ 低不弹;/ 入栈[3,8][+,/]
7(入栈[3,8][+,/,(]
81数字入栈[3,8,1][+,/,(]
9-栈顶是 (,不弹;- 入栈[3,8,1][+,/,(,-]
105数字入栈[3,8,1,5][+,/,(,-]
11)弹 - 算 1-5=-4;遇 ( 弹掉[3,8,-4][+,/]
12^/ 优先级低,不弹;^ 入栈[3,8,-4][+,/,^]
132数字入栈[3,8,-4,2][+,/,^]
14结束弹 ^ 算 (-4)^2=16[3,8,16][+,/]
15结束弹 / 算 8/16=0.5[3,0.5][+]
16结束弹 + 算 3+0.5=3.5[3.5][]

结果 3.5。

3.3 双栈法与后序遍历的关系#

双栈法每次弹栈计算,相当于在表达式树中合并一棵小子树:

  • num 栈存的是已经算完的子树结果
  • op 栈存的是还没轮到算的子树根
  • 弹出一个运算符,就表示它的左右子树都已完成,可以访问这个根

所以:

双栈法 = 一边隐式建表达式树,一边做后序遍历求值。


4. 中缀转后缀:调度场算法#

调度场算法由 Dijkstra 提出,专门用来把中缀转成后缀。

4.1 规则#

准备一个输出队列和一个运算符栈:

  • 遇到数字 → 加入输出队列
  • 遇到左括号 → 压入运算符栈
  • 遇到右括号 → 不断弹出运算符栈顶加入输出队列,直到遇到左括号,然后把左括号弹掉
  • 遇到运算符 →
    • 只要栈不空,且栈顶不是左括号,且栈顶优先级 高于 当前,或 同级且当前是左结合,就弹出栈顶加入输出
    • 然后把当前运算符压入栈
  • 扫描结束 → 把运算符栈里剩下的全部弹出加入输出

4.2 模拟 3 + 4 * 2 / (1 - 5)^2#

步骤读到动作输出队列运算符栈
13数字输出3[]
2+压栈3[+]
34数字输出3 4[+]
4*+ 低,不弹;压 *3 4[+,*]
52数字输出3 4 2[+,*]
6/弹 * 输出;+ 低不弹;压 /3 4 2 *[+,/]
7(压栈3 4 2 *[+,/,(]
81数字输出3 4 2 * 1[+,/,(]
9-栈顶是 (,不弹;压 -3 4 2 * 1[+,/,(,-]
105数字输出3 4 2 * 1 5[+,/,(,-]
11)弹 - 输出;弹掉 (3 4 2 * 1 5 -[+,/]
12^/ 低,不弹;压 ^3 4 2 * 1 5 -[+,/,^]
132数字输出3 4 2 * 1 5 - 2[+,/,^]
14结束弹 ^、/、+3 4 2 * 1 5 - 2 ^ / +[]

得到后缀表达式:

3 4 2 * 1 5 - 2 ^ / +

5. 后缀表达式求值#

后缀求值只需要一个数字栈。

5.1 规则#

  • 从左到右扫描
  • 遇到数字 → 压栈
  • 遇到运算符 → 弹出两个数:
    • 先弹出的是 右操作数 b
    • 后弹出的是 左操作数 a
    • 计算 a op b,结果压回栈
  • 最后栈里只剩一个数,就是结果

5.2 模拟 3 4 2 * 1 5 - 2 ^ / +#

步骤读到动作栈
13压栈[3]
24压栈[3,4]
32压栈[3,4,2]
4*弹 b=2, a=4,算 4*2=8[3,8]
51压栈[3,8,1]
65压栈[3,8,1,5]
7-弹 b=5, a=1,算 1-5=-4[3,8,-4]
82压栈[3,8,-4,2]
9^弹 b=2, a=-4,算 (-4)^2=16[3,8,16]
10/弹 b=16, a=8,算 8/16=0.5[3,0.5]
11+弹 b=0.5, a=3,算 3+0.5=3.5[3.5]

结果 3.5。


6. 前缀表达式求值#

前缀求值同样一个数字栈,但 从右往左扫描。

6.1 规则#

  • 从右到左扫描
  • 遇到数字 → 压栈
  • 遇到运算符 → 弹出两个数:
    • 先弹出的是 左操作数 a
    • 后弹出的是 右操作数 b
    • 计算 a op b,结果压回栈
  • 最后栈里只剩一个数,就是结果

6.2 模拟 + 3 / * 4 2 ^ - 1 5 2#

从右往左读的顺序是:

2 5 1 - ^ 2 4 * / 3 +
步骤读到动作栈
12压栈[2]
25压栈[2,5]
31压栈[2,5,1]
4-弹 a=1, b=5,算 1-5=-4[2,-4]
5^弹 a=-4, b=2,算 (-4)^2=16[16]
62压栈[16,2]
74压栈[16,2,4]
8*弹 a=4, b=2,算 4*2=8[16,8]
9/弹 a=8, b=16,算 8/16=0.5[0.5]
103压栈[0.5,3]
11+弹 a=3, b=0.5,算 3+0.5=3.5[3.5]

结果 3.5。


7. 三种求值方法对比#

表达式扫描方向栈的数量弹栈顺序需要优先级需要括号
中缀左 → 右两个栈(数字 + 运算符)先右后左需要需要
后缀左 → 右一个数字栈先右后左不需要不需要
前缀右 → 左一个数字栈先左后右不需要不需要

记忆口诀:

中缀双栈比优先,后缀从左先弹右,前缀从右先弹左。


8. 递归下降求值(扩展)#

递归下降是编译器前端常用的方法,按文法递归解析。

文法示例:

expr -> term { (+ | -) term }
term -> factor { (* | /) factor }
factor -> number | '(' expr ')' | '-' factor

代码框架:

double parseExpr(string& s) {
double val = parseTerm(s);
while (pos < s.size() && (s[pos] == '+' || s[pos] == '-')) {
char op = s[pos++];
double rhs = parseTerm(s);
if (op == '+') val += rhs;
else val -= rhs;
}
return val;
}
double parseTerm(string& s) {
double val = parseFactor(s);
while (pos < s.size() && (s[pos] == '*' || s[pos] == '/')) {
char op = s[pos++];
double rhs = parseFactor(s);
if (op == '*') val *= rhs;
else val /= rhs;
}
return val;
}
double parseFactor(string& s) {
if (s[pos] == '(') {
pos++;
double val = parseExpr(s);
pos++;
return val;
}
if (s[pos] == '-') {
pos++;
return -parseFactor(s);
}
double val = 0;
while (pos < s.size() && isdigit(s[pos])) {
val = val * 10 + (s[pos] - '0');
pos++;
}
return val;
}

特点:结构清晰,易扩展,但需要处理左递归。


9. 总结#

  • 中缀表达式:人熟悉,计算机麻烦,用双栈法求值。
  • 后缀表达式:一个栈,从左到右,先弹右后弹左。
  • 前缀表达式:一个栈,从右到左,先弹左后弹右。
  • 表达式树:三种遍历对应三种表达式。
    • 前序 → 前缀
    • 中序 → 中缀
    • 后序 → 后缀
  • 双栈法:本质是隐式建树 + 后序遍历求值。
  • 调度场算法:中缀转后缀的标准算法。

10. 推荐练习#

  • 408 真题:2014 年中缀转后缀、2025 年后缀求值入栈次数、括号匹配。
  • LeetCode:150(逆波兰表达式求值)、224(基本计算器)、227(基本计算器 II)、772(基本计算器 III)、1597(构造二叉表达式树)。
  • 考研机试:上海交通大学 KY102、天津大学 2025 机试、浙大复试机试。
  • 参考书:王道《数据结构考研复习指导》第 3 章栈和队列;《算法竞赛入门经典》表达式求值章节;OI-wiki 表达式求值页面。

本文可作为表达式求值专题的教程笔记,配合代码实践和手写模拟,效果更佳。

表达式求值与二叉树遍历教程
https://lyuumine.cn/posts/expression-evaluation-and-binary-tree/
作者
LxuMzn
发布于
2026-10-13
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类