Skip to content

括号匹配

2026 大纲 三(六)栈、队列和数组的应用(另见《表达式求值》《栈在递归中的应用》《双端队列》)。

为什么是栈,而不是计数器

括号的嵌套结构要求"最后出现的左括号最先被匹配",这与栈的弹出顺序完全一致。

严蔚敏教材给了一个很好的直觉解释:把每个未匹配的左括号看成一份"期待"。读入第一个左括号后,程序期待着与它匹配的右括号;但如果等来的是第二个左括号,说明"第二份期待的急迫性高于第一份",第一份只能暂时靠边;第三个左括号来了,第二份又要让位……每读入一个左括号,栈中所有未消解的期待急迫性都降一级;每读入一个右括号,栈顶那份最急迫的期待或者被消解,或者说明输入不合法

"急迫性最高的最先被处理"就是 LIFO,所以承载它的只能是栈。

🔴 只有一种括号时,栈可以退化成一个计数器(遇 ( 加一、遇 ) 减一,中途为负是右多、结束不为零是左多);但两种以上的括号必须用栈。原因很具体:计数器只记得住"还欠几个右括号",记不住"当前最内层欠的是哪一种",所以判不出 {[)} 这类类型交叉栈比计数器多记住的,是嵌套的"类型序列",不只是"个数"。

三种失败,检测时机各不相同

失败类型示例检测时机触发的判断语句
类型不匹配{[)}弹栈比较时(扫描中途)!isPaired(topChar, ch)
右括号多余())遇到右括号时(扫描中途)top == -1
左括号多余(()扫描结束后return top == -1

前两种在扫描过程中就能立刻返回失败,第三种必须等到扫描结束——读完最后一个字符之前,你无法确定后面还会不会来右括号。所以"匹配成功"的判据是两条同时成立:扫描结束,且栈空。过程中一路无错但结束时栈非空,仍然是失败。

先看一眼

加载可视化中...

拿一个嵌套深的表达式和一个"平铺"的表达式(比如 ((((x))))()()()())各跑一遍,比较栈的最高水位。两者左括号一样多,栈深却差得远——下面那一节就是讲这件事。

算法与代码

从左到右扫描:遇左括号压栈;遇右括号先判栈空(空则右括号多余),再弹栈比类型(不配对则失败);其他字符一律跳过。扫描结束后栈空则成功。

c
#include <stdbool.h>
#define MAX_SIZE 100

// 判断一对括号是否配对;必须定义在 bracketMatch 之前(或先给出函数声明)
bool isPaired(char left, char right) {
    return (left == '(' && right == ')')
        || (left == '[' && right == ']')
        || (left == '{' && right == '}');
}

bool bracketMatch(const char *str) {
    char stack[MAX_SIZE];
    int top = -1;                        // 顺序栈,top 指向栈顶元素

    for (int i = 0; str[i] != '\0'; i++) {
        char ch = str[i];

        if (ch == '(' || ch == '[' || ch == '{') {
            if (top == MAX_SIZE - 1)     // 栈满:嵌套层数超出容量
                return false;            // 顺序栈实现绕不开这一步
            stack[++top] = ch;           // 左括号入栈(前缀 ++,先移后写)
        }
        else if (ch == ')' || ch == ']' || ch == '}') {
            if (top == -1)               // 失败 2:右括号多余(栈空还要 pop)
                return false;
            char topChar = stack[top--]; // 弹栈(后缀 --,先取后移)
            if (!isPaired(topChar, ch))  // 失败 1:类型不匹配
                return false;
        }
        // 其他字符直接跳过
    }
    return top == -1;                    // 失败 3:栈非空说明左括号多余
}

🔴 if (top == -1) return false; 必须在弹栈之前。 先写 stack[top--] 再判断,空栈时会读 stack[-1]——越界读,而且 top 还被改成了 2。这是"空栈时 Pop 违反前置条件"这一条在代码上的落实。

三种失败与两个表达式的逐步跟踪(第一次学、或想手动模拟时展开)

{[()]} 的逐步跟踪

步骤当前字符动作栈内容(栈底→栈顶)
1{左括号,入栈{
2[左括号,入栈{ [
3(左括号,入栈{ [ (
4)右括号,弹出 (,类型配对 ✓{ [
5]右括号,弹出 [,类型配对 ✓{
6}右括号,弹出 {,类型配对 ✓

扫描结束,栈为空 → 匹配成功。栈的最大深度是 3,等于表达式的最大嵌套层数。

三种失败的逐步执行

  • {[)}(类型不匹配){ 入栈 → [ 入栈(栈 { [)→ 读到 ),栈非空, 弹出 [,而 [) 不配对 → 在第 3 个字符处失败。
  • ())(右括号多余)( 入栈 → 读到 ),弹出 ( 配对 ✓,栈空 → 再读到 )栈已空 → 在第 3 个字符处失败。
  • (()(左括号多余)( 入栈 → ( 入栈(栈 ( ()→ 读到 ),弹出配对 ✓(栈 ()→ 扫描结束,栈非空 → 在扫描结束后才发现失败。

含其他字符的完整跟踪。真实表达式里还有操作数与运算符,它们一律跳过。 以 a * (b + [c - d]) / e 为例:

字符类别动作栈(底→顶)
1a其他跳过
2*其他跳过
3(左括号入栈(
4b其他跳过(
5+其他跳过(
6[左括号入栈( [
7c其他跳过( [
8-其他跳过( [
9d其他跳过( [
10]右括号弹出 [,配对 ✓(
11)右括号弹出 (,配对 ✓
12/ e其他跳过

扫描结束、栈空 → 匹配成功。栈的最大深度是 2,出现在第 6 9 步, 等于该表达式的最大括号嵌套层数 ✓。

栈的最大深度 = 最大嵌套层数

这是本篇最有考点价值的一条,而且很容易记错:

🔴 栈的最大深度等于表达式的最大括号嵌套层数,不是左括号的总数。 ()()() 有 3 个左括号,栈深却始终只有 1;而 ((())) 只有 3 个左括号,栈深就是 3。

原因在上面的跟踪表里一目了然:左括号入栈、右括号出栈,栈里任何时刻装的是"当前还没闭合的括号",也就是当前所在的嵌套层数。平铺的括号开一个关一个,水位压根涨不起来。

由此直接得到空间复杂度:O(n),但这个 n最坏情形——全是左括号((((...()时栈深达到 n。像 ()()()...() 这种同样长为 n 的输入,栈深始终不超过 1。"O(n) 空间"说的是存在最坏输入,不是平均要用 n 格。

时间复杂度则是干净的 O(n):每个字符恰好被扫描一次,每个左括号入栈一次、最多出栈一次,入栈出栈都是 O(1),总操作数不超过 3n

换成链栈实现,以及两处代码细节(写代码题时展开)

栈满检查是顺序栈实现必须带的:嵌套层数超过 MAX_SIZE 时若不检查就会越界写。 isPaired 要先定义或先声明:C99 之后不允许隐式声明函数,把它写在 bracketMatch 后面而又不给原型,编译不通过。

把数组换成《链栈》后,唯一消失的是栈满检查:

c
typedef struct SNode {
    char data;
    struct SNode *next;
} SNode;

bool bracketMatch_Link(const char *str) {
    SNode *top = NULL;                       // 不带头结点,NULL 即空栈
    bool ok = true;

    for (int i = 0; str[i] != '\0' && ok; i++) {
        char ch = str[i];
        if (ch == '(' || ch == '[' || ch == '{') {
            SNode *s = (SNode *)malloc(sizeof(SNode));
            if (s == NULL) { ok = false; break; }   // 取代"栈满"的失败来源
            s->data = ch; s->next = top; top = s;   // 头插入栈
        } else if (ch == ')' || ch == ']' || ch == '}') {
            if (top == NULL) { ok = false; break; } // 失败 2:右括号多余
            SNode *p = top;
            char topChar = p->data;
            top = p->next;
            free(p);
            if (!isPaired(topChar, ch)) { ok = false; break; }  // 失败 1
        }
    }
    if (ok && top != NULL) ok = false;       // 失败 3:左括号多余
    while (top != NULL) {                    // 提前退出时也要清干净,避免泄漏
        SNode *p = top; top = p->next; free(p);
    }
    return ok;
}

多出来的那个 while 清栈循环值得注意:数组版直接 return false 就完事, 链栈版中途失败时栈里还挂着若干结点,不释放就泄漏。 这是链式实现相对顺序实现多出来的一项责任,代码题里容易漏。

同一套机制换个对象就是别的问题:只有一种括号时退化成计数器;把括号换成 XML/HTML 标签,配对判断从"字符配对"变成"字符串相等",栈里存标签名;编译器的语法分析则是它的完整版,括号匹配是最简化的模型。

表达式求值的联系:中缀转后缀时"遇到 ) 就不断弹出并输出,直到遇见 (",本质上就是在做一次括号匹配——只不过弹出的东西还要输出。两篇的栈操作可以互相印证。

考点速记

三条会被反复调用的结论:

  1. 栈记住的是"类型序列",计数器只记"个数"——这是"必须用栈"的准确理由。
  2. 三种失败的检测时机不同,左括号多余只能在扫描结束时发现。
  3. 栈的最大深度等于最大嵌套层数,不是左括号总数。

这一节在真题里被考过的形式(下方「真题练习」逐题对应):

  • 给定符号栈的容量,问哪个表达式处理不了。题面会说"符号栈初始为空、容量为 3",然后给四个带括号的表达式。这类题问的不是括号配不配对(四个通常都是配对的),而是最大嵌套层数有没有超过容量——数一数每个表达式里"同时还没闭合"的括号最多有几层,超过容量的那个就是答案。
  • 判断某段括号串是否合法,以及失败发生在第几个字符——按三种失败的检测时机逐字符走。

易错把左括号总数当成栈的最大深度。 (a+b)*(c+d)*(e+f) 有 3 个左括号,容量为 1 的栈就够用了。

易错先弹栈再判空。 空栈时会读到 stack[-1],且 top 变成 2。判空必须在弹栈之前。

易错扫描过程无错就判成功。 还要看结束时栈空不空——左括号多余这一种失败,只有扫描结束后才暴露。

教材出处
  • 括号匹配的"期待急迫性"分析(每读入一个左括号,栈中所有未消解的期待急迫性都降一级; 读入右括号时,或使栈顶最急迫的期待得以消解,或是不合法情况): 严蔚敏《数据结构(C 语言版)》(第 2 版)p56「案例 3.2 括号匹配的检验」
  • 顺序栈的入栈、出栈算法(本篇代码使用的栈操作):同书 p58–p59

相关知识

顺序栈top 约定与入栈出栈写法都来自那里)| 链栈(换成链栈就不需要判栈满)| 栈和队列的基本概念Pop 的前置条件是"栈非空")| 表达式求值("遇 ) 弹到 ( 为止"就是一次括号匹配)| 栈在递归中的应用(语法分析也可用递归下降,隐式用系统栈)

真题练习