第28章:字符串的括号匹配:栈的应用('()'、'[]'、'{}')

各位同学,今天我们来聊一个非常经典的问题——括号匹配。说白了,就是检查一段字符串里的左括号和右括号是不是成对出现的,而且顺序要对。

你可能会想:“这有什么难的?数个数不就行了?”嗯,我当年刚学编程时也是这么想的。直到有一次,我在一个配置文件解析器里遇到了嵌套的括号……那叫一个惨。数个数只能判断数量是否相等,但完全管不了顺序。比如 ([)] 这种,数量是对的,但顺序是错的。真正的括号匹配,必须用栈。

为什么是栈?

栈是一种“后进先出”的数据结构。你想想看,括号的嵌套规则是什么?最后一个出现的左括号,必须最先被匹配掉。这不就是栈的特性吗?

举个例子:{ [ ( ) ] }

  • 遇到 {,入栈
  • 遇到 [,入栈
  • 遇到 (,入栈
  • 遇到 ),检查栈顶是不是 (,是则出栈
  • 遇到 ],检查栈顶是不是 [,是则出栈
  • 遇到 },检查栈顶是不是 {,是则出栈

最后栈为空,说明匹配成功。如果中间发现栈顶不匹配,或者遍历完了栈还不为空,那就是匹配失败。

核心思想:左括号入栈,右括号匹配栈顶。匹配成功则出栈,失败则报错。

代码实现

我直接上代码。这是我最常用的一个版本,简洁且健壮。

#include <stdio.h>
#include <string.h>
#include <stdbool.h>

#define MAX_LEN 1000

// 栈结构
typedef struct {
    char data[MAX_LEN];
    int top;
} Stack;

void initStack(Stack *s) {
    s->top = -1;
}

bool push(Stack *s, char c) {
    if (s->top >= MAX_LEN - 1) return false;
    s->data[++s->top] = c;
    return true;
}

char pop(Stack *s) {
    if (s->top == -1) return '\0';
    return s->data[s->top--];
}

char peek(Stack *s) {
    if (s->top == -1) return '\0';
    return s->data[s->top];
}

bool isMatchingPair(char left, char right) {
    return (left == '(' && right == ')') ||
           (left == '[' && right == ']') ||
           (left == '{' && right == '}');
}

bool isValid(char *s) {
    Stack stack;
    initStack(&stack);
    
    for (int i = 0; s[i] != '\0'; i++) {
        char c = s[i];
        if (c == '(' || c == '[' || c == '{') {
            push(&stack, c);
        } else if (c == ')' || c == ']' || c == '}') {
            if (stack.top == -1) return false;  // 栈空,说明没有左括号匹配
            char top = pop(&stack);
            if (!isMatchingPair(top, c)) return false;  // 不匹配
        }
        // 其他字符忽略
    }
    
    return stack.top == -1;  // 栈空才合法
}

int main() {
    char test1[] = "({[]})";
    char test2[] = "([)]";
    char test3[] = "((())";
    
    printf("%s: %s\n", test1, isValid(test1) ? "合法" : "不合法");
    printf("%s: %s\n", test2, isValid(test2) ? "合法" : "不合法");
    printf("%s: %s\n", test3, isValid(test3) ? "合法" : "不合法");
    
    return 0;
}

这段代码我用了很多年。你注意看 isMatchingPair 函数,它把三种括号的匹配关系封装起来了。这样主逻辑就非常清晰:遇到左括号就压栈,遇到右括号就检查栈顶。

个人习惯:我一般会把栈的操作单独封装成函数,而不是直接操作数组。这样代码可读性高,也方便以后改成动态栈。

避坑指南

我曾经在一个生产环境里踩过一个坑。当时我写了一个表达式解析器,括号匹配的逻辑看起来没问题,但测试时总是报错。排查了半天,发现是字符串里混入了中文括号(全角符号)。'('')' 跟 ASCII 的 '('')' 长得像,但 ASCII 码完全不同。

所以,如果你处理的是用户输入的字符串,一定要先做字符过滤或编码检查。否则,栈里存了一个全角左括号,遇到半角右括号,匹配肯定失败。

注意:栈的深度也要考虑。如果字符串特别长,嵌套特别深,比如几千层括号,栈可能会溢出。我建议要么用动态数组,要么设置一个合理的最大深度,并在入栈时检查。

流程图:括号匹配的核心逻辑

下面我用一张 SVG 图来展示整个流程。你跟着箭头走一遍,就能理解栈是怎么工作的了。

开始 初始化空栈 遍历字符串每个字符 是左括号? 入栈 是右括号? 栈空? 返回 false 匹配栈顶? 出栈 返回 false 遍历结束,栈空则 true

复杂度分析

指标 说明
时间复杂度 O(n) 每个字符只处理一次,入栈出栈都是 O(1)
空间复杂度 O(n) 最坏情况下,所有字符都是左括号,全部入栈

这个算法的时间复杂度是线性的,空间复杂度也是线性的。你想想看,如果字符串有 100 万个字符,最坏情况下栈里要存 100 万个左括号。所以,如果你在嵌入式系统里用这个算法,一定要考虑栈的容量限制。

扩展思考

除了括号匹配,栈还能用来做很多事情。比如表达式求值、HTML 标签匹配、函数调用栈模拟等等。我当年做一个 JSON 解析器时,就用栈来匹配花括号和方括号,原理一模一样。

另外,如果你需要支持多种括号类型,比如 <> 或者自定义的成对符号,只需要在 isMatchingPair 里加几个条件就行了。代码的扩展性很好。

一个小技巧:如果你不想自己写栈,可以用 C 标准库里的 uthash 或者自己实现一个简单的动态数组。我个人习惯用固定数组加一个 top 指针,简单可靠。

好了,关于括号匹配的栈实现,我就讲到这里。记住,栈是解决这类嵌套匹配问题的最优工具。下次你遇到类似需求,别犹豫,直接上栈。

公众号:蓝海资料掘金营,微信 deep3321