C语言栈刷题:LeetCode 20 有效的括号完整解题笔记
前言
LeetCode 20 是栈结构最经典入门题型,是理解后进先出LIFO特性的必练题目。括号天然的由内向外闭合规则完美适配栈逻辑,初学者极易出现空指针断言崩溃、内存泄漏、括号匹配错乱等问题。本文结合手写调试笔记,完整给出可落地的动态char栈实现、解题代码、易错点,适配期末复习、代码复盘、面试手写。
一、原题描述
给定只包含 (、)、[、]、{、} 的字符串 s,判断括号是否有效。
有效必须全部满足三条规则:
- 左括号必须和同类型右括号闭合;
- 括号必须按照正确嵌套顺序闭合;
- 任意右括号都存在对应的同类型左括号。
测试样例汇总
| 输入 | 输出 | 说明 |
|---|---|---|
"()" |
true | 正常成对匹配 |
"()[]{}" |
true | 多类括号依次正常配对 |
"(]" |
false | 左右括号类型不匹配 |
"([)]" |
false | 嵌套顺序错乱,不符合闭合逻辑 |
"[" |
false | 存在多余左括号,无闭合右括号 |
"]" |
false | 开头就是右括号,无前置左括号 |
二、思路一:常规遍历暴力判断(不推荐)
时间复杂度:O(n2)O(n^2)O(n2) |空间复杂度:O(1)O(1)O(1)
解题逻辑
不断遍历字符串,成对删除匹配的括号,反复循环直到无法删除。若最终字符串清空则合法,否则非法。
优缺点
✅ 不需要栈结构,逻辑靠字符串替换实现
❌ 多层循环嵌套效率极低,代码繁琐,边界情况极多,不符合本题栈考察目标,面试完全不推荐
三、思路二:char类型动态栈(最优标准解法)
时间复杂度:O(n)O(n)O(n) |空间复杂度:O(n)O(n)O(n),栈存储左括号
栈结构整体设计
将栈存储元素改为char适配字符,采用动态数组实现,支持自动二倍扩容;完整封装初始化、销毁、入栈、出栈、取栈顶、判空全套接口。
指针分工规则
- 遍历指针
pi:逐个扫描整个字符串的每一个字符; - 栈:专门缓存所有读到的左括号,利用后进先出保证内层括号优先闭合。
执行流程规则
- 遍历字符为左括号
( / [ / {:直接入栈保存; - 遍历字符为右括号:
① 栈为空 → 无左括号可以匹配,直接判定失败;
② 读取栈顶左括号,和当前右括号类型不匹配 → 判定失败;
③ 匹配成功,弹出栈顶元素; - 全部字符遍历完毕:栈必须为空。栈内剩余元素代表存在未闭合左括号,判定无效。
完整分层代码
1. 栈结构体与全部接口实现
c
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef char STDataType;
typedef struct Stack
{
STDataType* arr;
int top;
int capacity;
}ST;
//初始化
void StackInit(ST* ps)
{
assert(ps);
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//销毁栈,释放堆内存
void StackDestroy(ST* ps)
{
assert(ps);
if (ps->arr)
free(ps->arr);
ps->arr = NULL;
ps->top = ps->capacity = 0;
}
//入栈
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
//空栈初始分配4,后续二倍扩容
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
ps->arr[ps->top++] = x;
}
//判断栈是否为空
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
//出栈
void StackPop(ST* ps)
{
assert(!StackEmpty(ps));
--ps->top;
}
//获取栈顶元素
STDataType StackTop(ST* ps)
{
assert(!StackEmpty(ps));
return ps->arr[ps->top - 1];
}
//获取栈有效元素数量
int StackSize(ST* ps)
{
return ps->top;
}
2. 核心校验函数 isValid
c
bool isValid(char* s) {
ST st;
StackInit(&st);
char* pi=s;
while(*pi!='\0')
{
if(*pi=='('||*pi=='['||*pi=='{')
{
StackPush(&st,*pi);
}
else{
//右括号出现,栈空直接失败
if(StackEmpty(&st))
{
StackDestroy(&st);
return false;
}
char top=StackTop(&st);
//三类括号任意一组不匹配则返回false
if(top=='(' && *pi!=')'||
top=='{' && *pi!='}'||
top=='[' && *pi!=']')
{
StackDestroy(&st);
return false;
}
//匹配成功弹出栈顶
StackPop(&st);
}
pi++;
}
//遍历结束,栈空代表全部闭合
bool ret =StackEmpty(&st)?true:false;
StackDestroy(&st);
return ret;
}
3. main测试调用示例
c
int main()
{
char s1[] = "()[]{}";
char s2[] = "(]";
char s3[] = "]";
printf("%d\n", isValid(s1)); // 输出1 true
printf("%d\n", isValid(s2)); // 输出0 false
printf("%d\n", isValid(s3)); // 输出0 false
return 0;
}
四、三种方案全维度对比表
| 解题方案 | 时间复杂度 | 空间复杂度 | 核心优点 | 主要缺点 | 面试推荐度 |
|---|---|---|---|---|---|
| 暴力循环反复删除 | O(n2)O(n^2)O(n2) | O(1)O(1)O(1) | 不用栈结构,上手门槛低 | 效率差、代码冗余,不贴合考点 | ⭐ |
| 静态数组模拟栈 | O(n)O(n)O(n) | O(n)O(n)O(n) | 不用动态内存,无内存泄漏 | 数组长度写死,适配超长字符串受限 | ⭐⭐ |
| 动态char栈标准写法 | O(n)O(n)O(n) | O(n)O(n)O(n) | 适配任意长度,贴合栈考点,工程规范 | 需要完整封装栈接口 | ⭐⭐⭐⭐⭐ |
五、刷题总结&高频考点梳理
1. 核心考点
栈后进先出特性、动态内存栈封装、分支内存管理、边界条件判断。
2. 记忆口诀
左括号入栈存起来,右括号负责挨个查;
右括号遇空直接错,括号不匹配也错;
字符串走完栈不空,剩余左括号依然错;
所有出口销毁栈,杜绝内存泄漏。
3. 题型复用
栈匹配套路通用所有嵌套闭合题型:HTML标签闭合、多层括号运算、嵌套符号校验。
4. 面试应答要点
面试官重点考察栈LIFO逻辑:括号必须内层先闭合,栈天然满足;同时会检查内存管理,必须所有return分支执行StackDestroy;口述"]"、"["两个边界用例的完整执行流程。
5. 五大必踩坑提醒
- 栈空访问栈顶触发断言 :右括号开头没有提前
StackEmpty拦截,StackTop触发断言崩溃,必须先判空再取栈顶; - 遍历完不校验栈空 :输入
"["全部左括号入栈,栈有残留,必须判定非法; - 栈类型未改为char :默认int栈存储字符会存在类型隐患,必须
typedef char STDataType; - 部分分支忘记销毁栈 :多return出口遗漏
StackDestroy,动态数组造成内存泄漏; - 匹配逻辑颠倒:只能用栈顶左括号匹配当前右括号,反向判断极易出现匹配错乱。