Skip to content

408

数据结构

DS-03-05 栈和队列的应用(表达式求值/括号匹配/递归)


一、定位信息

项目内容
所属圈层核心层
考点热度H级(高频重点) — 表达式求值(中缀转后缀、后缀表达式计算)是408大题的经典考点,括号匹配和递归也常出现在选择题中,综合分值可达8–12分
前置知识回顾需掌握"栈的基本概念与实现"(DS-03-01),理解栈的后进先出特性;需具备基本的数学表达式常识(运算符优先级、结合性)
知识网络定位本单元是栈的直接应用,体现了栈在实际问题中的核心价值;递归与栈的关系为后续理解深度优先搜索(DFS)、回溯算法等奠定基础

二、知识点讲解

2.1 表达式的三种形式

同一个数学表达式可以有三种等价的书写形式:

形式运算符位置示例
中缀表达式运算符在操作数之间a+b×ca + b \times c
前缀表达式(波兰式)运算符在操作数之前+  a  ×  b  c+ \; a \; \times \; b \; c
后缀表达式(逆波兰式)运算符在操作数之后a  b  c  ×  +a \; b \; c \; \times \; +

直观理解

  • 中缀是我们日常书写方式,但计算机难以直接处理(需要考虑优先级和括号)
  • 后缀表达式不需要括号,运算符的顺序本身就隐含了优先级,计算机可以从左到右直接求值

2.2 中缀表达式转后缀表达式(栈的应用)

算法思路:使用一个运算符栈,从左到右扫描中缀表达式:

  1. 操作数:直接输出
  2. 左括号 (:压入栈
  3. 右括号 ):依次弹出栈顶运算符并输出,直到遇到左括号(左括号弹出但不输出)
  4. 运算符:比较当前运算符与栈顶运算符的优先级:
    • 若当前优先级 > 栈顶优先级:压入栈
    • 若当前优先级 栈顶优先级:弹出栈顶并输出,再将当前运算符压入栈
  5. 扫描结束:将栈中剩余运算符依次弹出并输出

运算符优先级(从高到低):

优先级运算符
3(最高)((入栈时优先级最高)
2*, /
1+, -
0(最低)((栈内时优先级最低)

关键细节:左括号 ( 在栈外(等待入栈时)优先级最高,但在栈内(与其他运算符比较时)优先级最低。这样可以保证括号内的运算符不会被提前弹出。

c
// 将中缀表达式infix转换为后缀表达式postfix
// infix以'\0'结尾,运算数和运算符之间用空格分隔
void InfixToPostfix(char *infix, char *postfix) {
    SqStack S;                // 运算符栈
    InitStack(S);
    int i = 0, k = 0;        // i扫描infix,k写入postfix
    char ch;

    while (infix[i] != '\0') {
        ch = infix[i];

        if (ch是操作数) {
            // 操作数直接输出到postfix
            while (infix[i]是数字或字母)
                postfix[k++] = infix[i++];
            postfix[k++] = ' '; // 空格分隔
        }
        else if (ch == '(') {
            Push(S, ch);        // 左括号入栈
            i++;
        }
        else if (ch == ')') {
            // 右括号:弹出直到遇到左括号
            ElemType top;
            while (!StackEmpty(S)) {
                Pop(S, top);
                if (top == '(') break; // 左括号弹出但不输出
                postfix[k++] = top;
                postfix[k++] = ' ';
            }
            i++;
        }
        else if (ch是运算符) {
            // 比较优先级,弹出优先级>=当前的栈顶运算符
            ElemType top;
            while (!StackEmpty(S)) {
                GetTop(S, top);
                if (top == '(') break;       // 遇到左括号停止
                if (Priority(top) >= Priority(ch)) {
                    Pop(S, top);             // 弹出并输出
                    postfix[k++] = top;
                    postfix[k++] = ' ';
                } else {
                    break;                   // 栈顶优先级更低,停止
                }
            }
            Push(S, ch);                     // 当前运算符入栈
            i++;
        }
    }

    // 扫描结束,弹出栈中剩余运算符
    ElemType top;
    while (!StackEmpty(S)) {
        Pop(S, top);
        postfix[k++] = top;
        postfix[k++] = ' ';
    }
    postfix[k] = '\0';       // 字符串结束符
}

示例:将 9+(31)×3+10/29 + (3 - 1) \times 3 + 10 / 2 转为后缀表达式

步骤读入输出
199
2++9
3(+(9
43+(9 3
5-+(-9 3
61+(-9 3 1
7)+9 3 1 -
8*+*9 3 1 -
93+*9 3 1 - 3
10++9 3 1 - 3 * +
1110+9 3 1 - 3 * + 10
12/+/9 3 1 - 3 * + 10
132+/9 3 1 - 3 * + 10 2
结束9 3 1 - 3 * + 10 2 / +

后缀表达式:9 3 1 - 3 * + 10 2 / +

2.3 后缀表达式求值(栈的应用)

算法思路:使用一个操作数栈,从左到右扫描后缀表达式:

  1. 操作数:压入栈
  2. 运算符:依次弹出两个操作数(先弹出的是右操作数),执行运算,将结果压入栈
  3. 扫描结束:栈中唯一元素即为表达式的值
c
// 后缀表达式求值
// postfix中操作数和运算符用空格分隔
int EvaluatePostfix(char *postfix) {
    SqStack S;                // 操作数栈
    InitStack(S);
    int i = 0;
    int operand1, operand2;   // 两个操作数

    while (postfix[i] != '\0') {
        if (postfix[i]是数字) {
            // 解析多位数并入栈
            int num = 0;
            while (postfix[i] >= '0' && postfix[i] <= '9') {
                num = num * 10 + (postfix[i] - '0');
                i++;
            }
            Push(S, num);     // 操作数入栈
        }
        else if (postfix[i]是运算符) {
            Pop(S, operand2); // 先弹出的是右操作数
            Pop(S, operand1); // 后弹出的是左操作数
            switch (postfix[i]) {
                case '+': Push(S, operand1 + operand2); break;
                case '-': Push(S, operand1 - operand2); break;
                case '*': Push(S, operand1 * operand2); break;
                case '/': Push(S, operand1 / operand2); break;
            }
            i++;
        }
        else {
            i++;              // 跳过空格
        }
    }

    int result;
    Pop(S, result);           // 栈中唯一元素即为结果
    return result;
}

示例:求后缀表达式 9 3 1 - 3 * + 10 2 / + 的值

步骤读入操作
19入栈[9]
23入栈[9, 3]
31入栈[9, 3, 1]
4-3-1=2,入栈[9, 2]
53入栈[9, 2, 3]
6*2*3=6,入栈[9, 6]
7+9+6=15,入栈[15]
810入栈[15, 10]
92入栈[15, 10, 2]
10/10/2=5,入栈[15, 5]
11+15+5=20,入栈[20]

结果:20

2.4 括号匹配

问题:给定一个括号序列(如 ({[()]})),判断括号是否匹配。

算法思路:使用一个括号栈

  1. 遇到左括号([{):压入栈
  2. 遇到右括号)]}):
    • 若栈空:不匹配
    • 若栈顶左括号与当前右括号类型匹配:弹出栈顶,继续
    • 若不匹配:返回失败
  3. 扫描结束后,若栈空则匹配成功,否则有未匹配的左括号
c
bool BracketMatch(char *str) {
    SqStack S;
    InitStack(S);
    for (int i = 0; str[i] != '\0'; i++) {
        if (str[i] == '(' || str[i] == '[' || str[i] == '{') {
            Push(S, str[i]);              // 左括号入栈
        }
        else if (str[i] == ')' || str[i] == ']' || str[i] == '}') {
            if (StackEmpty(S)) return false; // 栈空,右括号无匹配
            ElemType top;
            Pop(S, top);
            // 检查括号类型是否匹配
            if (str[i] == ')' && top != '(') return false;
            if (str[i] == ']' && top != '[') return false;
            if (str[i] == '}' && top != '{') return false;
        }
    }
    return StackEmpty(S);                 // 栈空则全部匹配
}

2.5 递归与栈的关系

递归 是函数直接或间接调用自身的过程。递归的执行依赖于系统调用栈(函数调用栈)

每次函数调用时,系统将以下信息压入调用栈:

  • 返回地址:调用完成后回到哪里继续执行
  • 局部变量:函数内的局部变量
  • 参数:传递给函数的参数
  • 返回值:函数的返回值

这称为一个栈帧(Stack Frame)

示例:阶乘函数

c
int Factorial(int n) {
    if (n <= 1) return 1;     // 递归出口(基线条件)
    return n * Factorial(n - 1); // 递归调用
}

调用 Factorial(4) 时调用栈的变化:

调用 Factorial(4):栈 [F(4)],需要 4 × F(3)
调用 Factorial(3):栈 [F(4), F(3)],需要 3 × F(2)
调用 Factorial(2):栈 [F(4), F(3), F(2)],需要 2 × F(1)
调用 Factorial(1):栈 [F(4), F(3), F(2), F(1)],返回 1
返回 Factorial(2):栈 [F(4), F(3), F(2)],返回 2×1=2
返回 Factorial(3):栈 [F(4), F(3)],返回 3×2=6
返回 Factorial(4):栈 [F(4)],返回 4×6=24

递归转非递归:可以用显式栈模拟系统调用栈,将递归算法转换为非递归算法。这在408大题中偶尔出现。

递归的关键要素

  1. 递归出口(基线条件):不再递归的条件,防止无限递归
  2. 递归体:将问题分解为更小的子问题
  3. 趋向出口:每次递归调用必须向递归出口靠近

三、记忆与理解辅助

技巧1:后缀表达式求值口诀

"遇到数就入栈,遇到符号就运算;先出的是右操作数,后出的是左操作数"

技巧2:中缀转后缀的优先级处理口诀

"栈外高优先入栈,栈外低先弹再入;遇到括号特殊处理,左括号入栈优先级最高"

技巧3:栈和队列应用场景对比表

应用场景使用的数据结构原因
表达式求值栈(操作数栈+运算符栈)运算符需要后进先出处理优先级
括号匹配最近的左括号应最先与右括号匹配
函数递归系统调用栈最后调用的函数最先返回
浏览器前进/后退两个栈后退栈和前进栈配合使用
BFS层序遍历队列先访问的节点其邻居先入队
打印任务排队队列先提交的任务先打印

技巧4:中缀、前缀、后缀的转换规则

转换方法
中缀→后缀运算符栈,按优先级弹出
中缀→前缀类似后缀,但从右到左扫描,结果反转
后缀求值操作数栈,遇运算符弹两个数计算
前缀求值操作数栈,从右到左扫描,遇运算符弹两个数计算

四、例题与精解

例题1(基础)

题目:将中缀表达式 A×B+C/DEA \times B + C / D - E 转换为后缀表达式。

命题意图:考查中缀转后缀的基本方法,涉及不同优先级运算符的处理。

审题分析

  • 已知:中缀表达式 A×B+C/DEA \times B + C / D - E
  • 求解:对应的后缀表达式
  • 运算符优先级:×,/\times, /(高) > +,+, -(低)

解题思路: 按中缀转后缀算法,逐一处理每个符号。

完整步骤

步骤读入栈(栈底→栈顶)输出
1AA
2**A
3B*A B
4++A B *
5C+A B * C
6/+/A B * C
7D+/A B * C D
8--A B * C D / +
9E-A B * C D / + E
结束A B * C D / + E -

后缀表达式A  B    C  D  /  +  E  A \; B \; * \; C \; D \; / \; + \; E \; -

验证:按后缀表达式求值

  1. A×B=ABA \times B = AB
  2. C/D=CDC / D = CD
  3. AB+CD=AB+CDAB + CD = AB+CD
  4. (AB+CD)E=AB+CDE(AB+CD) - E = AB+CD-E

方法反思

  • 遇到 + 时,栈顶是 *(优先级更高),所以先弹出 *
  • 遇到 - 时,栈顶是 /(优先级更高),先弹出 /,再弹出 +(同级也要弹出)
  • 同级运算符的处理:左结合的运算符遇到同级时要先弹出栈顶

例题2(中等)

题目:已知后缀表达式为 3 4 + 5 × 6 -,写出求值过程并给出最终结果。然后说明如果使用栈来实现后缀表达式求值,为什么必须"先弹出的是右操作数"。

命题意图:考查后缀表达式求值的操作过程,以及对栈在表达式求值中关键作用的深入理解。

审题分析

  • 已知:后缀表达式 3 4 + 5 × 6 -
  • 求解:求值过程和最终结果;解释为什么先弹出的是右操作数

解题思路: 按后缀求值算法逐步执行,并对减法和除法的非交换性进行分析。

完整步骤

求值过程

步骤读入操作
13入栈[3]
24入栈[3, 4]
3+弹出4和3,计算3+4=7,入栈[7]
45入栈[7, 5]
5×弹出5和7,计算7×5=35,入栈[35]
66入栈[35, 6]
7-弹出6和35,计算35-6=29,入栈[29]

最终结果29

为什么先弹出的是右操作数?

考虑后缀表达式 a b -,它对应中缀表达式 aba - b

栈中状态:[a, b](a先入栈在底,b后入栈在顶)

  • 第一次 Pop 得到 b(栈顶)
  • 第二次 Pop 得到 a(栈底)

如果我们要计算 aba - b,则:

  • 第一次弹出的 b 是右操作数
  • 第二次弹出的 a 是左操作数
  • 计算 ab=第二次弹出的第一次弹出的a - b = \text{第二次弹出的} - \text{第一次弹出的}

同理,对于 a b /(对应 a/ba / b):

  • 第一次弹出 b(右操作数),第二次弹出 a(左操作数)
  • 计算 a/ba / b

结论:对于非交换运算(减法和除法),必须正确区分左操作数和右操作数。栈的后进先出特性决定了先弹出的是后入栈的,即后缀表达式中靠后的操作数,也就是右操作数

方法反思

  • 后缀表达式求值的关键:记住"先出为右,后出为左"
  • 对于加法和乘法(交换律成立),左右顺序不影响结果;但对于减法和除法,顺序至关重要
  • 此理解对于正确实现求值算法至关重要,408大题中可能要求分析此过程

五、考情分析

分析维度内容
考查频次中缀转后缀、后缀表达式求值近5年出现3次以上,括号匹配和递归的栈模拟也有出现
常见题型选择题(判断后缀表达式、括号匹配过程)、大题(表达式求值完整过程、递归转非递归)
分值占比大题可达8–12分(完整的中缀转后缀+求值过程)
命题趋势表达式求值一直是大题高频考点,趋势是要求更完整的过程展示和更复杂的应用场景

:以上频次基于大纲权重与通用命题规律推测,待真题分析后校准。


六、易错点提醒

易错点1

  • 错误表现:中缀转后缀时,同级运算符的处理出错(如 a - b + c 转换错误)
  • 错误原因:不清楚同级运算符是否也需要弹出
  • 正确做法:同级运算符也要弹出(左结合性),即当前运算符优先级 ≤ 栈顶优先级时都弹出。a - b + c 的后缀为 a b - c +,不是 a b c + -

易错点2

  • 错误表现:后缀表达式求值时,减法和除法的操作数顺序搞反
  • 错误原因:忘记"先弹出的是右操作数",导致 aba-b 算成了 bab-a
  • 正确做法:始终记住——operand2 = Pop()(右操作数),operand1 = Pop()(左操作数),结果 = operand1 op operand2

易错点3

  • 错误表现:括号匹配时只检查括号数量不检查类型,如 ([)] 被误判为匹配
  • 错误原因:只数左右括号个数是否相等,没有检查类型对应关系
  • 正确做法:每次右括号出栈时,必须检查栈顶左括号是否与之类型一致()[]{}

易错点4

  • 错误表现:递归转非递归时忘记保存局部变量或返回地址
  • 错误原因:对系统调用栈的机制理解不透彻
  • 正确做法:显式栈中必须保存递归函数的所有状态信息:参数、局部变量、当前执行到哪一步(通常用状态编号表示)

七、来源标注

  • 依据2026考研统考大纲(408计算机学科专业基础综合)
  • 依据《数据结构(C语言版)》严蔚敏版
  • 依据《数据结构》王道考研辅导讲义

考研全科复习资料 - 基于2026考研统考大纲