当前位置: 首页 > news >正文

【数据结构与算法】LeetCode 20.有效的括号

文章目录

  • 有效的括号问题详解
    • 问题描述
    • 问题背景与重要性
    • 问题分析
      • 为什么选择栈结构?
      • 算法选择的原因
    • 算法思路
      • 算法正确性证明
    • 代码实现解析
      • 栈数据结构定义
      • 栈操作函数详细解析
      • 核心算法函数
    • 程序源码
    • 复杂度分析
      • 复杂度证明
    • 示例演示
      • 有效字符串示例
      • 无效字符串示例
    • 边界情况处理
    • 实际应用场景
    • 扩展思考
      • 支持更多括号类型
      • 错误定位与反馈
      • 非栈解决方案
      • 性能优化
    • 总结

有效的括号问题详解

问题描述

给定一个只包含括号字符 '(', ')', '{', '}', '[', ']' 的字符串 s,判断该字符串是否有效。

有效字符串需要满足以下三个条件:

  1. 左括号必须用相同类型的右括号闭合

  2. 左括号必须以正确的顺序闭合

  3. 每个右括号都有一个对应的相同类型的左括号

    在这里插入图片描述

问题背景与重要性

括号匹配是计算机科学中的基础问题,它出现在许多实际应用场景中。从编程语言的语法解析到数学表达式的计算,再到配置文件和数据格式(如JSON、XML)的验证,括号匹配都扮演着重要角色。

问题分析

这是一个经典的栈应用问题。括号匹配需要遵循"后进先出"的原则,这与栈的特性完全吻合。

为什么选择栈结构?

括号匹配的核心要求是:最后打开的括号必须最先关闭。这与栈的"后进先出"(LIFO)特性完美匹配。

想象一下日常生活中的例子:

  • 我们穿脱衣服的顺序是最后穿上的衣服最先脱下
  • 叠放盘子时,最后放上去的盘子最先被取用
  • 文档编辑中的"撤销"功能也是后进先出的典型应用

括号匹配也是同样的道理。当我们遇到嵌套的括号时,最后打开的括号必须最先关闭,这正是栈数据结构的典型应用场景。

算法选择的原因

为什么使用栈而不是其他数据结构?

  • 数组:需要维护额外的指针来跟踪最近打开的括号,实质上就是在模拟栈
  • 队列:先进先出的特性与括号匹配的后进先出需求相反
  • 链表:可以实现栈的功能,但代码复杂度较高

因此,直接使用栈是最自然和高效的选择。

算法思路

  1. 初始化一个空栈
  2. 遍历字符串中的每个字符
    • 如果是左括号('(', '[', '{'),将其压入栈中
    • 如果是右括号(')', ']', '}'),则:
      • 检查栈是否为空(空栈表示没有与之匹配的左括号)
      • 弹出栈顶元素并与当前右括号比较是否匹配
      • 如果不匹配,则字符串无效
  3. 遍历完成后
    • 如果栈为空,说明所有括号都正确匹配
    • 如果栈不为空,说明有未匹配的左括号,字符串无效

算法正确性证明

该算法的正确性可以通过循环不变式来证明:

  • 初始化:栈为空,表示尚未遇到任何未匹配的左括号
  • 保持:每次处理右括号时,都与最近未匹配的左括号(栈顶)进行比较,确保匹配的正确性
  • 终止:遍历完成后,栈为空当且仅当所有括号都正确匹配

代码实现解析

栈数据结构定义

typedef char STDataType;typedef struct Stack
{STDataType* a;    // 动态数组存储栈元素int top;          // 栈顶指针int capacity;     // 栈容量
}ST;

栈操作函数详细解析

  1. 初始化栈:设置初始状态,指针和容量都为0

    void STInit(ST* pst)
    {assert(pst);pst->a = 0;pst->capacity = 0;pst->top = 0;
    }
    
  2. 销毁栈:释放动态分配的内存,防止内存泄漏

    void STDestroy(ST* pst)
    {assert(pst);free(pst->a);pst->a = NULL;pst->top = pst->capacity = 0;
    }
    
  3. 入栈操作:检查容量并扩容,然后添加元素

    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++;
    }
    
  4. 出栈操作:简单地将栈顶指针减1

    void STPop(ST* pst)
    {assert(pst);assert(pst->top > 0); // 确保栈不为空pst->top--;
    }
    
  5. 获取栈顶元素:返回栈顶指针前一个位置的元素

    STDataType STTop(ST* pst)
    {assert(pst);assert(pst->top > 0); // 确保栈不为空return pst->a[pst->top - 1];
    }
    
  6. 判断栈空:检查栈顶指针是否为0

    bool STEmpty(ST* pst)
    {assert(pst);return pst->top == 0;
    }
    
  7. 获取栈大小:返回栈顶指针的值

    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)。平均情况下,空间复杂度取决于括号的嵌套深度,而不是字符串总长度。

示例演示

有效字符串示例

输入: “()[]{}”

执行过程:

  1. ‘(’ 入栈 → 栈: [‘(’]
  2. ‘)’ 与栈顶 ‘(’ 匹配 → 弹出 → 栈: []
  3. ‘[’ 入栈 → 栈: [‘[’]
  4. ‘]’ 与栈顶 ‘[’ 匹配 → 弹出 → 栈: []
  5. ‘{’ 入栈 → 栈: [‘{’]
  6. ‘}’ 与栈顶 ‘{’ 匹配 → 弹出 → 栈: []
  7. 栈为空 → 返回 true

输入: “{[]}”

执行过程:

  1. ‘{’ 入栈 → 栈: [‘{’]
  2. ‘[’ 入栈 → 栈: [‘{’, ‘[’]
  3. ‘]’ 与栈顶 ‘[’ 匹配 → 弹出 → 栈: [‘{’]
  4. ‘}’ 与栈顶 ‘{’ 匹配 → 弹出 → 栈: []
  5. 栈为空 → 返回 true

无效字符串示例

输入: “([)]”

执行过程:

  1. ‘(’ 入栈 → 栈: [‘(’]
  2. ‘[’ 入栈 → 栈: [‘(’, ‘[’]
  3. ‘)’ 与栈顶 ‘[’ 不匹配 → 返回 false

输入: “((())”

执行过程:

  1. ‘(’ 入栈 → 栈: [‘(’]
  2. ‘(’ 入栈 → 栈: [‘(’, ‘(’]
  3. ‘(’ 入栈 → 栈: [‘(’, ‘(’, ‘(’]
  4. ‘)’ 与栈顶 ‘(’ 匹配 → 弹出 → 栈: [‘(’, ‘(’]
  5. ‘)’ 与栈顶 ‘(’ 匹配 → 弹出 → 栈: [‘(’]
  6. 遍历结束,栈不为空 → 返回 false

边界情况处理

算法考虑了多种边界情况:

  1. 空字符串:栈初始为空,最终也为空,返回true
  2. 只有左括号:遍历结束后栈不为空,返回false
  3. 只有右括号:遇到右括号时栈为空,立即返回false
  4. 交错括号:如"([)]",会检测到不匹配的情况
  5. 空指针输入:增加了对空指针的检查,返回true(与空字符串一致)

实际应用场景

括号匹配算法不仅在编程问题中出现,还有许多实际应用:

  1. 编译器语法检查:检查代码中的括号是否匹配,是语法分析的重要组成部分
  2. 表达式求值:确保算术表达式中的括号正确嵌套,是计算器程序的核心功能
  3. JSON/XML解析:验证文档结构是否正确,是数据交换格式处理的基础
  4. 文本编辑器:提供括号高亮和匹配功能,改善编程体验
  5. 配置文件的解析:许多配置文件使用嵌套结构,需要括号匹配验证
  6. 正则表达式引擎:处理正则表达式中的分组和捕获

扩展思考

支持更多括号类型

当前算法只支持三种括号类型,但可以轻松扩展以支持更多类型的括号,如尖括号< >、引号" "' '等。只需要修改判断左括号和匹配条件的部分即可。

错误定位与反馈

在实际应用中,我们可能不仅需要知道字符串是否有效,还需要知道哪里出错。可以修改算法以提供更详细的错误信息,如错误位置和错误类型(缺少右括号、缺少左括号或括号不匹配)。

非栈解决方案

虽然栈是最自然的解决方案,但也可以使用其他方法:

  • 递归下降:对于特别深的嵌套,递归可能更直观,但有栈溢出风险
  • 计数器方法:对于只有一种括号的情况,可以使用简单的计数器,但对于多种括号不适用

性能优化

对于非常长的字符串,可以考虑:

  • 提前终止:当发现不匹配时立即返回,不需要处理剩余字符
  • 内存池:预先分配足够大的栈空间,避免多次扩容
  • 并行处理:对于超长字符串,可以尝试分块并行处理(但括号匹配本质是顺序相关的,并行化困难)

总结

通过栈数据结构解决括号匹配问题是一个经典且高效的算法。该算法的时间复杂度和空间复杂度都是线性的,适用于大多数实际场景。理解这个问题的解决方案不仅有助于解决类似的栈应用问题,还能加深对数据结构选择和应用的理解。

掌握这种算法思维,能够帮助我们更好地解决许多需要"后进先出"处理顺序的问题,是编程中一种重要的思维方式。括号匹配问题虽然简单,但蕴含的栈思想却是许多复杂算法的基础,如深度优先搜索、递归函数的调用栈、回溯算法等。

http://www.dtcms.com/a/355709.html

相关文章:

  • 跨网络通信:路由器如何实现全球互联
  • QT控件:【控件概述】【QWidget核心体系】【按钮类控件】【显示类控件】
  • 实战回忆录从webshell开始突破边界
  • 3D血管图像处理与连接系统
  • Odoo AI 智能查询系统
  • 【LeetCode每日一题】160.相交链表 206. 反转链表
  • AI智能供应链深度解析:从库存优化到风险预警,解锁企业降本提效新路径
  • 基于deepseek的Spring boot入门
  • Zabbix监控Dell R系列硬件(PowerEdge R740等)
  • git 小白入门教学
  • TypeScript:symbol类型
  • vscode自动格式化,取消保存时自动格式化也不好使
  • open webui源码分析11-四个特征之记忆
  • 【MySQL 为什么默认会给 id 建索引? MySQL 主键索引 = 聚簇索引?】
  • 数据结构:排序算法的评判标准(Criteria Used For Analysing Sorts)
  • 打靶日记-SQLi-LABS(一)
  • C++防御性编程策略
  • RGW层Op的组织
  • 并发编程——05 并发锁机制之深入理解synchronized
  • 优雅地实现ChatGPT式的打字机效果:Spring Boot 流式响应
  • Jtekt深沟球轴承外圈防跑圈开发
  • Python Imaging Library (PIL) 全面指南:PIL基础入门-图像颜色模式转换与应用
  • [网鼎杯 2018]Fakebook
  • 基础IO详解
  • 【前端教程】JavaScript 基础总结
  • 教育类《河北教育》杂志简介
  • Day03_苍穹外卖——公共字段自动填充菜品相关功能
  • 河南萌新联赛2025第(七)场:郑州轻工业大学
  • 【数据结构与算法】(LeetCode)141.环形链表 142.环形链表Ⅱ
  • 数据分析学习笔记4:加州房价预测