栈和队列专题(一):LeetCode 20. 有效的括号

一、题目概述

本题是栈数据结构的入门经典应用题,核心考察对「后进先出(LIFO)」特性的理解与应用。

题目描述

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。 有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例

输入 输出 说明
"()" true 单对括号正常闭合
"()[]{}" true 多类型括号依次闭合
"(]" false 括号类型不匹配
"([])" true 括号嵌套闭合,顺序正确
"([)]" false 嵌套顺序错误,交叉闭合

提示

  • 1 <= s.length <= 10^4
  • s 仅由括号 ()[]{} 组成

二、解题思路:为什么用栈?

括号匹配的核心规则是:后出现的左括号,要最先被闭合。这恰好和栈「后进先出」的特性完全吻合。

算法核心步骤

  1. 遍历字符串 ,逐个读取字符:
    • 遇到左括号 ( { [:将其压入栈中,等待匹配。
    • 遇到右括号 ) } ]
      • 若此时栈为空:说明没有对应的左括号,直接判定无效。
      • 取出栈顶元素,判断是否与当前右括号类型匹配:
        • 不匹配:直接返回 false
        • 匹配:弹出栈顶元素,继续遍历。
  2. 遍历结束后校验
    • 若栈为空:所有左括号都被成功匹配,返回 true
    • 若栈非空:存在多余的左括号未闭合,返回 false

拓展思考:入栈顺序是 1234,出栈顺序一定是 4321 吗?

很多初学者刚接触栈时,会有一个典型误区:认为元素按顺序全部入栈后再全部出栈,才是栈的唯一用法。

答案是:不一定

栈的「后进先出」规则,约束的是栈内元素的弹出顺序 ,但并不要求必须一次性把所有元素全部入栈后才能弹出。在实际操作中,完全可以边进边出,也就是入栈一部分、弹出一部分、再入栈、再弹出。

以入栈序列 1, 2, 3, 4 为例:

  • 如果全部入栈再弹出:出栈顺序只能是 4, 3, 2, 1
  • 如果边进边出,可以得到多种合法出栈序列,例如:
    • 1, 2, 3, 4:进一个弹一个
    • 2, 1, 4, 3:1、2 入栈,弹出 2、1;3、4 入栈,弹出 4、3
    • 3, 2, 4, 1:1、2、3 入栈,弹出 3、2;4 入栈,弹出 4、1

回到括号匹配这道题,正是利用了「边进边出」的特性:遇到左括号就入栈,遇到右括号就立刻弹出栈顶进行匹配,而不是把所有左括号都存到最后再统一处理。源码工程

源码工程

本文代码采用分文件工程化组织:Stack.h(头文件声明)、Stack.c(栈实现)、Test.c(算法与测试用例)。 完整可编译源码已经上传 Gitee 仓库,包含全套测试用例,可直接 clone 下载本地编译运行:数据结构/LeetCode 20. 有效的括号 · Luminous/Code_2026 - 码云 - 开源中国


三、完整代码实现(C语言 手写顺序栈)

C 语言标准库中没有内置的栈容器,因此做这道题需要我们手动实现栈的数据结构。LeetCode 平台可以用数组快速模拟栈,但为了彻底理解栈的底层实现,这里我们手写完整的动态扩容顺序栈,从数据结构定义到接口实现全覆盖,更适合数据结构基础练习。

cpp 复制代码
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>

// 栈存储的数据类型
typedef char STDataType;

// 顺序栈结构体
typedef struct Stack
{
	STDataType* a;   // 动态数组指针
	int top;         // 栈顶标记,指向栈顶元素的下一个位置
	int capacity;    // 栈的总容量
}Stack;

// 初始化栈
void StackInit(Stack* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

// 入栈
void StackPush(Stack* ps, STDataType data)
{
	assert(ps);
	// 空间不足时触发扩容
	if (ps->capacity == ps->top)
	{
		int num = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * num);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = num;
	}
	ps->a[ps->top] = data;
	ps->top++;
}

// 出栈
void StackPop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0); // 空栈禁止出栈
	ps->top--;
}

// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0); // 空栈无栈顶元素
	return ps->a[ps->top - 1];
}

// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->top;
}

// 销毁栈,释放内存
void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

// 解题主函数
bool isValid(char* s)
{
    int n = 0;
    Stack st;
	StackInit(&st);

    while (s[n])
    {
        // 左括号:入栈
        if ((s[n] == '(') ||
            (s[n] == '{') ||
            (s[n] == '['))
        {
           StackPush(&st, s[n]);
        }
        // 右括号:匹配校验
        else
        {
            // 栈空直接匹配失败
            if (StackSize(&st) == 0)
            {
                StackDestroy(&st);
                return false;
            }
            STDataType topCh = StackTop(&st);
            // 类型不匹配,直接失败
            if ((s[n] == ')' && topCh != '(') ||
                (s[n] == '}' && topCh != '{') ||
                (s[n] == ']' && topCh != '['))
            {
                StackDestroy(&st);
                return false;
            }
            // 匹配成功,弹出栈顶
            else
            {
                StackPop(&st);
            }
        }
        n++;
    }

    // 栈为空则全部匹配成功
    bool ret = (StackSize(&st) == 0);
    StackDestroy(&st);
    return ret;
}

// 本地测试
int main()
{
    printf("%d\n", isValid("()[]{}"));   // 1
    printf("%d\n", isValid("(]"));       // 0
    printf("%d\n", isValid(")"));        // 0
    printf("%d\n", isValid("((()))"));   // 1
    printf("%d\n", isValid("([)]"));     // 0
    return 0;
}

四、C++ 版本实现(使用 STL stack)

如果使用 C++,标准模板库(STL)中提供了现成的 std::stack 容器适配器,不需要手动实现栈的底层逻辑,代码会简洁很多。核心算法思路完全一致,只是直接调用封装好的 push()pop()top()empty() 等接口即可。

cpp 复制代码
#include <iostream>
#include <stack>
#include <string>
using namespace std;

class Solution {
public:
    bool isValid(string s) {
        stack<char> st;
        
        for (char ch : s) {
            // 左括号入栈
            if (ch == '(' || ch == '{' || ch == '[') {
                st.push(ch);
            }
            // 右括号匹配
            else {
                // 栈空则无左括号匹配
                if (st.empty()) {
                    return false;
                }
                char topCh = st.top();
                // 类型不匹配
                if ((ch == ')' && topCh != '(') ||
                    (ch == '}' && topCh != '{') ||
                    (ch == ']' && topCh != '[')) {
                    return false;
                }
                // 匹配成功,弹出栈顶
                st.pop();
            }
        }
        
        // 栈为空则全部匹配成功
        return st.empty();
    }
};

// 本地测试
int main() {
    Solution sol;
    cout << sol.isValid("()[]{}") << endl;   // 1
    cout << sol.isValid("(]") << endl;       // 0
    cout << sol.isValid(")") << endl;        // 0
    cout << sol.isValid("((()))") << endl;   // 1
    cout << sol.isValid("([)]") << endl;     // 0
    return 0;
}

对比可以看出:

  • C 语言:需要从零实现栈的结构体、初始化、入栈、出栈、取栈顶、销毁等全套接口,更贴近底层,适合学习数据结构原理。
  • C++:直接使用 STL 封装好的栈,代码量更少,专注于算法逻辑本身,工程开发效率更高。

五、代码核心细节拆解

1. 顺序栈的设计细节

  • top 的含义 :代码中 top 初始值为 0,指向栈顶元素的下一个位置 。因此:
    • 栈的元素个数 = top
    • 栈顶元素下标 = top - 1
    • 入栈操作:a[top] = data; top++
  • 动态扩容策略:初始容量为 0,首次入栈扩容为 4,后续每次空间不足时扩容为原来的 2 倍,均摊时间复杂度为 O(1)。
  • 断言保护 :对空栈执行出栈、取栈顶操作时,通过 assert 主动拦截非法操作,便于调试定位问题。

2. 解题函数的健壮性处理

  • 空栈防御 :遇到右括号先判断栈是否为空,避免空栈调用 StackTop 触发断言崩溃。
  • 内存泄漏规避 :所有提前 return false 的分支,都先调用 StackDestroy 释放动态申请的数组内存,再返回结果。
  • 最终统一校验:遍历结束后统一判断栈是否为空,避免左括号多余的情况。

六、复杂度分析

  • 时间复杂度:O(n) 字符串中每个字符最多入栈、出栈各 1 次,整体操作次数与字符串长度线性相关。
  • 空间复杂度:O(n) 最坏情况下字符串全为左括号,栈中需要存储全部 n 个字符。

七、刷题踩坑复盘

本题逻辑不复杂,但也容易在细节上出错,以下是实际编码中的高频坑点:

  1. 分支条件逻辑写反 :误用 || 判断非左括号,导致所有字符都进入右括号分支,左括号永远无法入栈。
  2. 返回逻辑颠倒 :误以为栈中有元素代表匹配成功,实际上栈非空说明存在未闭合的左括号,应返回 false
  3. 忽略内存释放:提前返回时忘记销毁栈,造成内存泄漏。
  4. 空栈直接取栈顶 :未做栈空判断就调用 StackTop,遇到首字符为右括号的用例直接触发断言崩溃。

八、总结

有效的括号是栈的最经典应用,本质是利用栈的后进先出特性,维护「待匹配的左括号」序列。通过手写完整的顺序栈实现,不仅能掌握这道题的解法,更能彻底理解栈的底层存储、扩容、增删操作的实现原理。

C 语言手写栈锻炼的是数据结构的底层功力,C++ STL 栈体现的是工程开发的效率,两者各有价值。后续遇到逆序输出、表达式求值、消除重复项类问题,都可以优先考虑用栈来解决。

完整分文件源码见仓库:数据结构/LeetCode 20. 有效的括号 · Luminous/Code_2026 - 码云 - 开源中国

相关推荐
2501_926978332 小时前
AGI 的四种瓶颈:资源型还是发现型--以及DSH的位置
人工智能·经验分享·笔记·ai写作
liuqs3323 小时前
nat123安卓和nat123全端口选型指南,映射工具怎么挑
网络·笔记
xian_wwq3 小时前
【学习笔记】光伏“四可”装置纵密模块--配置方法和原理
笔记·学习
Nil2084 小时前
leetcode 160相交链表
算法·leetcode·链表
Herbert_hwt5 小时前
C语言零基础入门:循环控制与数据类型详解
c语言·数据结构·算法
日拱一卒的小田6 小时前
ZYNQ学习笔记3-ZYNQ的IIC控制器1
笔记·单片机·学习
晚风醉蝶7 小时前
1-6-插入排序-InsertionSort
java·数据结构·排序算法
Tyler_TXZ7 小时前
C++C语言之——二叉树
c语言·开发语言·数据结构·c++·二叉树
ZC跨境爬虫7 小时前
LeetCode 108. 将有序数组转换为二叉搜索树(递归构建详解 + Java Python 实现)
java·python·leetcode
Tisfy7 小时前
LeetCode 3090.每个字符最多出现两次的最长子字符串:二重循环 / 滑动窗口
算法·leetcode·字符串·题解·模拟·双指针·滑动窗口