【数据结构与算法】LeetCode 20.有效的括号
文章目录
- 有效的括号问题详解
有效的括号问题详解
问题描述
给定一个只包含括号字符 \'(\'
, \')\'
, \'{\'
, \'}\'
, \'[\'
, \']\'
的字符串 s
,判断该字符串是否有效。
有效字符串需要满足以下三个条件:
-
左括号必须用相同类型的右括号闭合
-
左括号必须以正确的顺序闭合
-
每个右括号都有一个对应的相同类型的左括号
问题背景与重要性
括号匹配是计算机科学中的基础问题,它出现在许多实际应用场景中。从编程语言的语法解析到数学表达式的计算,再到配置文件和数据格式(如JSON、XML)的验证,括号匹配都扮演着重要角色。
问题分析
这是一个经典的栈应用问题。括号匹配需要遵循\"后进先出\"的原则,这与栈的特性完全吻合。
为什么选择栈结构?
括号匹配的核心要求是:最后打开的括号必须最先关闭。这与栈的\"后进先出\"(LIFO)特性完美匹配。
想象一下日常生活中的例子:
- 我们穿脱衣服的顺序是最后穿上的衣服最先脱下
- 叠放盘子时,最后放上去的盘子最先被取用
- 文档编辑中的\"撤销\"功能也是后进先出的典型应用
括号匹配也是同样的道理。当我们遇到嵌套的括号时,最后打开的括号必须最先关闭,这正是栈数据结构的典型应用场景。
算法选择的原因
为什么使用栈而不是其他数据结构?
- 数组:需要维护额外的指针来跟踪最近打开的括号,实质上就是在模拟栈
- 队列:先进先出的特性与括号匹配的后进先出需求相反
- 链表:可以实现栈的功能,但代码复杂度较高
因此,直接使用栈是最自然和高效的选择。
算法思路
- 初始化一个空栈
- 遍历字符串中的每个字符
- 如果是左括号(
\'(\'
,\'[\'
,\'{\'
),将其压入栈中 - 如果是右括号(
\')\'
,\']\'
,\'}\'
),则:- 检查栈是否为空(空栈表示没有与之匹配的左括号)
- 弹出栈顶元素并与当前右括号比较是否匹配
- 如果不匹配,则字符串无效
- 如果是左括号(
- 遍历完成后
- 如果栈为空,说明所有括号都正确匹配
- 如果栈不为空,说明有未匹配的左括号,字符串无效
算法正确性证明
该算法的正确性可以通过循环不变式来证明:
- 初始化:栈为空,表示尚未遇到任何未匹配的左括号
- 保持:每次处理右括号时,都与最近未匹配的左括号(栈顶)进行比较,确保匹配的正确性
- 终止:遍历完成后,栈为空当且仅当所有括号都正确匹配
代码实现解析
栈数据结构定义
typedef char STDataType;typedef struct Stack{ STDataType* a; // 动态数组存储栈元素 int top; // 栈顶指针 int capacity; // 栈容量}ST;
栈操作函数详细解析
-
初始化栈:设置初始状态,指针和容量都为0
void STInit(ST* pst){ assert(pst); pst->a = 0; pst->capacity = 0; pst->top = 0;}
-
销毁栈:释放动态分配的内存,防止内存泄漏
void STDestroy(ST* pst){ assert(pst); free(pst->a); pst->a = NULL; pst->top = pst->capacity = 0;}
-
入栈操作:检查容量并扩容,然后添加元素
void STPush(ST* pst, STDataType x){ assert(pst); // 扩容逻辑 if (pst->top == pst->capacity) { int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2; STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror(\"realloc fail\"); return; } pst->a = tmp; pst->capacity = newcapacity; } pst->a[pst->top] = x; pst->top++;}
-
出栈操作:简单地将栈顶指针减1
void STPop(ST* pst){ assert(pst); assert(pst->top > 0); // 确保栈不为空 pst->top--;}
-
获取栈顶元素:返回栈顶指针前一个位置的元素
STDataType STTop(ST* pst){ assert(pst); assert(pst->top > 0); // 确保栈不为空 return pst->a[pst->top - 1];}
-
判断栈空:检查栈顶指针是否为0
bool STEmpty(ST* pst){ assert(pst); return pst->top == 0;}
-
获取栈大小:返回栈顶指针的值
int STSize(ST* pst){ assert(pst); return pst->top;}
核心算法函数
bool isValid(char* s) { // 处理空指针情况 if (s == NULL) { return true; // 空指针视为有效,与空字符串处理一致 } ST st; STInit(&st); // 初始化栈 while(*s) // 遍历字符串 { // 左括号入栈 if(*s==\'(\'||*s==\'[\'||*s==\'{\') { STPush(&st,*s); } else // 处理右括号 { // 栈为空说明没有匹配的左括号 if(STEmpty(&st)) { STDestroy(&st); return false; } char top = STTop(&st); // 获取栈顶元素 STPop(&st); // 弹出栈顶元素 // 检查括号是否匹配 if((top==\'(\'&&*s!=\')\') ||(top==\'[\'&&*s!=\']\') ||(top==\'{\'&&*s!=\'}\')) { STDestroy(&st); return false; } } s++; // 移动到下一个字符 } // 检查栈是否为空(所有左括号都已匹配) bool ret = STEmpty(&st); STDestroy(&st); return ret;}
程序源码
typedef char STDataType;typedef struct Stack{STDataType* a;int top;int capacity;}ST;//初始化和销毁void STInit(ST* pst);void STDestroy(ST* pst);//入栈出栈void STPush(ST* pst, STDataType x);void STPop(ST* pst);//取栈顶数据STDataType STTop(ST* pst);//判空bool STEmpty(ST* pst);//获取数据个数int STSize(ST* pst);void STInit(ST* pst){assert(pst);pst->a = 0;pst->capacity = 0;pst->top = 0;}void STDestroy(ST* pst){assert(pst);free(pst->a);pst->a = NULL;pst->top = pst->capacity = 0;}//入栈出栈void STPush(ST* pst, STDataType x){assert(pst);//扩容if (pst->top == pst->capacity){int newcapcacity =pst->capacity==0 ? 4 : pst->capacity * 2;//起始空间为0则申请4个空间 不为0则二倍STDataType* tmp = (STDataType*)realloc(pst->a, newcapcacity * sizeof(STDataType));//pst->a为NULL时,realloc相当与mallocif (tmp == NULL){perror(\"realloc fail\");}pst->a = tmp;pst->capacity = newcapcacity;}pst->a[pst->top] = x;pst->top++;//top指向栈顶下一个元素}void STPop(ST* pst){assert(pst);pst->top--;}//取栈顶数据STDataType STTop(ST* pst){assert(pst);assert(pst->top > 0);return pst->a[pst->top - 1];}//判空bool STEmpty(ST* pst){assert(pst);return pst->top == 0;//==0就是空}//获取数据个数int STSize(ST* pst){assert(pst);return pst->top;}bool isValid(char* s) { ST st; STInit(&st); while(*s) { //左括号入栈 if(*s==\'(\'||*s==\'[\'||*s==\'{\') { STPush(&st,*s); } else//右括号 取栈顶左括号与右括号作比较 { //如果栈为空 说明不匹配 if(STEmpty(&st)) { STDestroy(&st); return false; } char top=STTop(&st); STPop(&st); //如果判断匹配,还要继续比较,所以判断不匹配的情况 //栈不为空才能取 if((top==\'(\'&&*s!=\')\') ||(top==\'[\'&&*s!=\']\') ||(top==\'{\'&&*s!=\'}\')) { STDestroy(&st); return false; } } s++; } bool ret=STEmpty(&st); STDestroy(&st); return ret; }
复杂度分析
- 时间复杂度:O(n),其中n是字符串长度。每个字符最多被处理一次(入栈或与栈顶比较)。
- 空间复杂度:O(n),最坏情况下需要将所有左括号都压入栈中。平均情况下,空间复杂度取决于括号的嵌套深度。
复杂度证明
时间复杂度为O(n)是显然的,因为算法只对字符串进行了一次线性扫描。
空间复杂度最坏情况下为O(n),当输入字符串全是左括号时,需要将全部n个字符压入栈中。最好情况下为O(1),当字符串是空或只有右括号时(会立即返回false)。平均情况下,空间复杂度取决于括号的嵌套深度,而不是字符串总长度。
示例演示
有效字符串示例
输入: “()[]{}”
执行过程:
- ‘(’ 入栈 → 栈: [‘(’]
- ‘)’ 与栈顶 ‘(’ 匹配 → 弹出 → 栈: []
- ‘[’ 入栈 → 栈: [‘[’]
- ‘]’ 与栈顶 ‘[’ 匹配 → 弹出 → 栈: []
- ‘{’ 入栈 → 栈: [‘{’]
- ‘}’ 与栈顶 ‘{’ 匹配 → 弹出 → 栈: []
- 栈为空 → 返回 true
输入: “{[]}”
执行过程:
- ‘{’ 入栈 → 栈: [‘{’]
- ‘[’ 入栈 → 栈: [‘{’, ‘[’]
- ‘]’ 与栈顶 ‘[’ 匹配 → 弹出 → 栈: [‘{’]
- ‘}’ 与栈顶 ‘{’ 匹配 → 弹出 → 栈: []
- 栈为空 → 返回 true
无效字符串示例
输入: “([)]”
执行过程:
- ‘(’ 入栈 → 栈: [‘(’]
- ‘[’ 入栈 → 栈: [‘(’, ‘[’]
- ‘)’ 与栈顶 ‘[’ 不匹配 → 返回 false
输入: “((())”
执行过程:
- ‘(’ 入栈 → 栈: [‘(’]
- ‘(’ 入栈 → 栈: [‘(’, ‘(’]
- ‘(’ 入栈 → 栈: [‘(’, ‘(’, ‘(’]
- ‘)’ 与栈顶 ‘(’ 匹配 → 弹出 → 栈: [‘(’, ‘(’]
- ‘)’ 与栈顶 ‘(’ 匹配 → 弹出 → 栈: [‘(’]
- 遍历结束,栈不为空 → 返回 false
边界情况处理
算法考虑了多种边界情况:
- 空字符串:栈初始为空,最终也为空,返回true
- 只有左括号:遍历结束后栈不为空,返回false
- 只有右括号:遇到右括号时栈为空,立即返回false
- 交错括号:如\"([)]\",会检测到不匹配的情况
- 空指针输入:增加了对空指针的检查,返回true(与空字符串一致)
实际应用场景
括号匹配算法不仅在编程问题中出现,还有许多实际应用:
- 编译器语法检查:检查代码中的括号是否匹配,是语法分析的重要组成部分
- 表达式求值:确保算术表达式中的括号正确嵌套,是计算器程序的核心功能
- JSON/XML解析:验证文档结构是否正确,是数据交换格式处理的基础
- 文本编辑器:提供括号高亮和匹配功能,改善编程体验
- 配置文件的解析:许多配置文件使用嵌套结构,需要括号匹配验证
- 正则表达式引擎:处理正则表达式中的分组和捕获
扩展思考
支持更多括号类型
当前算法只支持三种括号类型,但可以轻松扩展以支持更多类型的括号,如尖括号、引号
\" \"
或\' \'
等。只需要修改判断左括号和匹配条件的部分即可。
错误定位与反馈
在实际应用中,我们可能不仅需要知道字符串是否有效,还需要知道哪里出错。可以修改算法以提供更详细的错误信息,如错误位置和错误类型(缺少右括号、缺少左括号或括号不匹配)。
非栈解决方案
虽然栈是最自然的解决方案,但也可以使用其他方法:
- 递归下降:对于特别深的嵌套,递归可能更直观,但有栈溢出风险
- 计数器方法:对于只有一种括号的情况,可以使用简单的计数器,但对于多种括号不适用
性能优化
对于非常长的字符串,可以考虑:
- 提前终止:当发现不匹配时立即返回,不需要处理剩余字符
- 内存池:预先分配足够大的栈空间,避免多次扩容
- 并行处理:对于超长字符串,可以尝试分块并行处理(但括号匹配本质是顺序相关的,并行化困难)
总结
通过栈数据结构解决括号匹配问题是一个经典且高效的算法。该算法的时间复杂度和空间复杂度都是线性的,适用于大多数实际场景。理解这个问题的解决方案不仅有助于解决类似的栈应用问题,还能加深对数据结构选择和应用的理解。
掌握这种算法思维,能够帮助我们更好地解决许多需要\"后进先出\"处理顺序的问题,是编程中一种重要的思维方式。括号匹配问题虽然简单,但蕴含的栈思想却是许多复杂算法的基础,如深度优先搜索、递归函数的调用栈、回溯算法等。