【力扣100题】82.有效的括号

一、题目描述

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

有效字符串需满足:

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

示例:

示例 输入 输出
示例1 s = "()" true
示例2 s = "()\[\]{}" true
示例3 s = "(]" false
示例4 s = "(\[\])" true
示例5 s = "()" false

提示:

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

二、解题思路总览

拿到这道题,首先思考:什么时候括号是"有效的"?

想象一下括号匹配的过程:

  • 遇到左括号 → 它期待一个配对的右括号
  • 遇到右括号 → 必须和最近的那个左括号配对

这正是 栈(Stack) 的经典应用场景:后进先出(LIFO),最近未匹配的那个左括号,恰好应该和当前遇到的右括号配对。

核心思路:

步骤 操作 说明
1 遍历字符串s的每个字符 从左到右依次处理
2 如果是左括号 '(' '[' '{' 入栈,等待配对
3 如果是右括号 ')' ']' '}' 栈顶元素出栈并检查是否匹配
4 遍历结束 栈为空 → true;栈非空 → false

为什么用栈?

对比 不用栈(错误思路) 用栈(正确思路)
匹配方式 盲目查找相同字符 只看最近的一个左括号
顺序要求 无法保证"正确的顺序" 栈天然保证LIFO,匹配顺序正确
时间复杂度 O(n^2) 或更差 O(n)

三、完整代码(方法一:栈 + STL)

cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        int n = s.size();
        // 奇数长度不可能配对成功
        if (n % 2) return false;
        
        stack<char> st;
        for (int i = 0; i < n; i++) {
            // 左括号:入栈
            if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
                st.push(s[i]);
            } else {
                // 右括号:栈空则匹配失败
                if (st.empty()) return false;
                
                char top = st.top();
                // 检查栈顶左括号是否与当前右括号配对
                if ((s[i] == ')' && top == '(') ||
                    (s[i] == ']' && top == '[') ||
                    (s[i] == '}' && top == '{')) {
                    st.pop();  // 配对成功,出栈
                } else {
                    return false;  // 配对失败
                }
            }
        }
        // 栈空说明全部配对成功
        return st.empty() ? true : false;
    }
};

四、其他解法

方法二:数组模拟栈(手动实现,消除STL开销)
cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        int n = s.size();
        if (n % 2) return false;
        
        // 用数组模拟栈,手动管理指针
        char st[n];
        int top = 0;  // 栈顶指针,指向下一个待写入位置
        
        for (int i = 0; i < n; i++) {
            if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
                st[top++] = s[i];  // 入栈
            } else {
                if (top == 0) return false;  // 栈空
                
                char c = st[--top];  // 出栈
                if ((s[i] == ')' && c != '(') ||
                    (s[i] == ']' && c != '[') ||
                    (s[i] == '}' && c != '{')) {
                    return false;
                }
            }
        }
        return top == 0;
    }
};

改进点: 用数组替代 std::stack,避免STL的函数调用开销和额外的内存分配。


方法三:栈 + HashMap 配对(代码更简洁)
cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        int n = s.size();
        if (n % 2) return false;
        
        stack<char> st;
        // 用哈希表存储配对关系,代码更清晰
        unordered_map<char, char> pair = {
            {')', '('},
            {']', '['},
            {'}', '{'}
        };
        
        for (char c : s) {
            if (pair.count(c)) {
                // c 是右括号,检查栈顶是否匹配
                if (st.empty() || st.top() != pair[c]) {
                    return false;
                }
                st.pop();
            } else {
                // c 是左括号,入栈
                st.push(c);
            }
        }
        return st.empty();
    }
};

改进点:unordered_map 替代硬编码的 if-else,配对关系一目了然,扩展性更好(比如增加其他括号类型)。


方法四:计数法(仅适用于相邻括号配对)
cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        int n = s.size();
        if (n % 2) return false;
        
        // 统计左括号数量,用于剪枝
        int cnt_p = 0, cnt_s = 0, cnt_c = 0;
        for (char c : s) {
            if (c == '(') cnt_p++;
            else if (c == '[') cnt_s++;
            else if (c == '{') cnt_c++;
            else if (c == ')') cnt_p--;
            else if (c == ']') cnt_s--;
            else if (c == '}') cnt_c--;
            
            // 剪枝:任意计数器为负,说明右括号多于左括号
            if (cnt_p < 0 || cnt_s < 0 || cnt_c < 0) return false;
        }
        return cnt_p == 0 && cnt_s == 0 && cnt_c == 0;
    }
};

适用范围: 此方法只能检测"相邻括号配对"(如 "()[]{}"),无法处理嵌套和交叉 (如 "([])" 正确返回 true,但 "(]" "([)]" 也会返回 true)。仅作思路拓展,生产代码不推荐


方法五:字符串替换(直观但低效)
cpp 复制代码
class Solution {
public:
    bool isValid(string s) {
        int n = s.size();
        if (n % 2) return false;
        
        // 不断删除相邻的有效括号对,直到无法删除
        while (s.find("()") != string::npos ||
               s.find("[]") != string::npos ||
               s.find("{}") != string::npos) {
            s.erase(s.find("()"), 2);
            s.erase(s.find("[]"), 2);
            s.erase(s.find("{}"), 2);
        }
        return s.empty();
    }
};

问题: find() 是 O(n),erase() 是 O(n),整体时间复杂度退化到 O(n^3) ,不可用于生产环境。仅作思路展示


五、算法流程图

以输入 s = "([])" 为例,逐步展示算法执行过程:

复制代码
Step 0: 初始状态
        s = "( [] )"
        stack: [空]

Step 1: 遇到 '(',左括号入栈
        s = "( [] )"
             ^
        stack: [ ( ]

Step 2: 遇到 '[',左括号入栈
        s = "( [] )"
              ^
        stack: [ ( , [ ]

Step 3: 遇到 ']',右括号
        - 栈顶是 '[',匹配成功
        - pop
        s = "( [] )"
               ^
        stack: [ ( ]

Step 4: 遇到 ')',右括号
        - 栈顶是 '(',匹配成功
        - pop
        s = "( [] )"
                ^
        stack: [空]

Step 5: 遍历结束
        stack为空 → 返回 true

再来看一个失败案例 s = "([)]"

复制代码
Step 0: 初始状态
        s = "( [ ) ]"
        stack: [空]

Step 1: 遇到 '(',入栈
        s = "( [ ) ]"
             ^
        stack: [ ( ]

Step 2: 遇到 '[',入栈
        s = "( [ ) ]"
              ^
        stack: [ ( , [ ]

Step 3: 遇到 ')',右括号
        - 栈顶是 '[',但 ')' 应该匹配 '('
        - 不匹配!返回 false
        s = "( [ ) ]"
               ^
        stack: [ ( , [ ]

最终结果: false

六、逐行解析(方法一)

cpp 复制代码
int n = s.size();
if (n % 2) return false;

剪枝优化: 如果字符串长度是奇数,必然有未配对的括号,直接返回 false。这一步看似简单,但可以跳过大量无效计算。


cpp 复制代码
stack<char> st;

数据结构选择: 使用 std::stack,只关心栈顶元素和入栈/出栈操作。


cpp 复制代码
for (int i = 0; i < n; i++) {
    if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
        st.push(s[i]);
    }

左括号处理: 直接入栈,等待之后可能的配对。


cpp 复制代码
    } else {
        if (st.empty()) return false;

右括号处理的第一步: 如果栈已经空了,说明没有对应的左括号,直接失败。


cpp 复制代码
        char top = st.top();
        if ((s[i] == ')' && top == '(') ||
            (s[i] == ']' && top == '[') ||
            (s[i] == '}' && top == '{')) {
            st.pop();
        } else {
            return false;
        }
    }
}

配对检查: 核心逻辑。当前右括号必须与栈顶的左括号类型一致,才算配对成功。


cpp 复制代码
return st.empty() ? true : false;

最终判断: 遍历结束后,栈空说明所有左括号都成功配对;栈非空说明有左括号没被匹配。


七、复杂度分析

各方法复杂度对比
方法 时间复杂度 空间复杂度 说明
方法一:STL栈 O(n) O(n) 标准解法,稳定高效
方法二:数组模拟栈 O(n) O(n) 省去STL开销,极致性能
方法三:栈+HashMap O(n) O(n) 代码简洁,配对关系清晰
方法四:计数法 O(n) O(1) 错误解法,仅适用于相邻配对
方法五:字符串替换 O(n^3) O(n) 不可用,复杂度太高

方法一详细复杂度分析

时间复杂度:O(n)

  • 只需遍历字符串一次
  • 每个字符最多入栈一次、出栈一次

空间复杂度:O(n)

  • 最坏情况:全部是左括号,如 "((((("
  • 栈中最多存放 n/2 个元素(严谨分析),但渐进表示为 O(n)

补充说明:

情况 栈中元素数量 说明
全部左括号 O(n) "((((("
全部右括号 O(1) 第一步就返回false
交替出现 O(1) "()()()" 每次配对后栈即空

八、面试追问 FAQ

问题 回答要点
Q1: 为什么不使用队列? 队列是FIFO(先进先出),而括号匹配需要LIFO。最近的左括号才应该被匹配,队列无法提供这个能力。
Q2: 可以不用栈吗? 理论上可以用递归模拟栈,但会增加函数调用开销,且有栈溢出风险。不推荐。
Q3: 如果要返回具体错误信息怎么办? 可以在返回false时增加额外逻辑:记录是"多余右括号"、"多余左括号"还是"类型不匹配"。
Q4: 如何处理带优先级的表达式求值? 这是另一个经典问题,通常用两个栈:一个存操作数,一个存操作符。括号匹配只是其中一步。
Q5: 如果字符串很长(10^6级别)怎么办? 当前算法O(n)已经最优,主要瓶颈可能在I/O。可以用内存映射或流式处理,但核心算法不变。
Q6: 三种栈的实现哪个更好? 生产环境优先用方法一(STL栈),代码可读性高;如果在竞赛或极端性能场景,用方法二(数组模拟);方法三适合需要频繁扩展配对规则的场景。

九、相关题目

题号 题目 难度 关联点
32 最长有效括号 困难 栈的延伸应用
921 使括号有效的最少添加 中等 栈的变形
1021 删除最外层的括号 简单 栈的变形
1541 平衡括号的最少插入 中等 栈的变形

推荐刷题顺序:

  1. 本题(20.有效的括号)→ 基础
  2. 921(使括号有效的最少添加)→ 变形
  3. 1021(删除最外层的括号)→ 变形
  4. 32(最长有效括号)→ 综合

十、总结

维度 内容
考察知识点 栈(Stack)的应用
难度 简单
代码框架 遍历 + 栈操作
关键技巧 利用栈的LIFO特性,保证匹配顺序
边界情况 奇数长度、栈空、右括号不匹配
变形题 返回最小插入数、最长有效长度
推荐解法 方法一(STL栈)或方法二(数组模拟)

一句话总结: 括号匹配是栈的入门经典题,核心就是"最近未匹配的左括号 = 当前右括号应该匹配的对象",用栈的LIFO特性天然实现这个逻辑。生产代码推荐使用 方法一(STL栈),代码可读性高且性能足够。


相关推荐
XGeFei1 小时前
时序算法 —— LSTM、ARIMA、随机森林
算法·随机森林·lstm
湖南天硕国产SSD1 小时前
工业存储可靠性进阶:天硕工业固态硬盘动态温控与寿命优化技术实践
网络·数据库·算法·工业存储·天硕存储·工业固态硬盘
legend050709ComeON1 小时前
常见面试题-leetcode
数据结构·算法·leetcode
初中就开始混世的大魔王1 小时前
7 Fast DDS-持久化服务
c++·人工智能·中间件·自动驾驶·信息与通信
Smilecoc1 小时前
决策树(一):决策树基本原理
算法·决策树·机器学习
weixin_307779131 小时前
从工具到协作者:AI在后端研发中的流程重构与组织赋能
人工智能·后端·python·算法·自动化
爱吃生蚝的于勒2 小时前
QT开发第三章——常用控件
linux·服务器·开发语言·前端·javascript·c++·qt
沉下去,苦磨练!2 小时前
深度学习神经网络的搭建
人工智能·算法
Lsk_Smion2 小时前
力扣实训 _ [207].课程表/图论
数据结构·leetcode·图论