一、题目概述
本题是栈数据结构的入门经典应用题,核心考察对「后进先出(LIFO)」特性的理解与应用。
题目描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。 有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例
| 输入 | 输出 | 说明 |
|---|---|---|
"()" |
true | 单对括号正常闭合 |
"()[]{}" |
true | 多类型括号依次闭合 |
"(]" |
false | 括号类型不匹配 |
"([])" |
true | 括号嵌套闭合,顺序正确 |
"([)]" |
false | 嵌套顺序错误,交叉闭合 |
提示
1 <= s.length <= 10^4s仅由括号()[]{}组成
二、解题思路:为什么用栈?
括号匹配的核心规则是:后出现的左括号,要最先被闭合。这恰好和栈「后进先出」的特性完全吻合。
算法核心步骤
- 遍历字符串 ,逐个读取字符:
- 遇到左括号
({[:将其压入栈中,等待匹配。 - 遇到右括号
)}]:- 若此时栈为空:说明没有对应的左括号,直接判定无效。
- 取出栈顶元素,判断是否与当前右括号类型匹配:
- 不匹配:直接返回
false。 - 匹配:弹出栈顶元素,继续遍历。
- 不匹配:直接返回
- 遇到左括号
- 遍历结束后校验 :
- 若栈为空:所有左括号都被成功匹配,返回
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、33, 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 个字符。
七、刷题踩坑复盘
本题逻辑不复杂,但也容易在细节上出错,以下是实际编码中的高频坑点:
- 分支条件逻辑写反 :误用
||判断非左括号,导致所有字符都进入右括号分支,左括号永远无法入栈。 - 返回逻辑颠倒 :误以为栈中有元素代表匹配成功,实际上栈非空说明存在未闭合的左括号,应返回
false。 - 忽略内存释放:提前返回时忘记销毁栈,造成内存泄漏。
- 空栈直接取栈顶 :未做栈空判断就调用
StackTop,遇到首字符为右括号的用例直接触发断言崩溃。
八、总结
有效的括号是栈的最经典应用,本质是利用栈的后进先出特性,维护「待匹配的左括号」序列。通过手写完整的顺序栈实现,不仅能掌握这道题的解法,更能彻底理解栈的底层存储、扩容、增删操作的实现原理。
C 语言手写栈锻炼的是数据结构的底层功力,C++ STL 栈体现的是工程开发的效率,两者各有价值。后续遇到逆序输出、表达式求值、消除重复项类问题,都可以优先考虑用栈来解决。
完整分文件源码见仓库:数据结构/LeetCode 20. 有效的括号 · Luminous/Code_2026 - 码云 - 开源中国