【LeetCode Hot100 刷题日记 (69/100)】20. 有效的括号 —— 栈匹配算法 🧠

5 阅读4分钟

📌 题目链接:20. 有效的括号 - 力扣(LeetCode)

🔍 难度:简单 | 🏷️ 标签:栈、字符串、哈希表

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(n)


🔍 题目分析

本题要求判断一个仅由 '(', ')', '{', '}', '[', ']' 组成的字符串是否为有效括号字符串。有效性需满足两个核心条件:

  1. 类型匹配:每个左括号必须有相同类型的右括号闭合;
  2. 顺序正确:括号必须按“最近打开、最先关闭”的原则闭合(即 LIFO 原则)。

例如:

  • "()"
  • "()[]{}"
  • "([)]" ❌(虽然数量匹配,但顺序错误)
  • "(((" ❌(未闭合)

💡 关键洞察:这种“后进先出”的匹配特性,天然适合用 栈(Stack) 来处理。

此外,题目隐含一个快速剪枝条件:有效括号字符串长度必为偶数。若输入长度为奇数,可直接返回 false,无需后续处理。


🧠 核心算法及代码讲解

✅ 算法思想:栈 + 哈希映射

我们使用 来暂存遇到的左括号,并借助 哈希表 快速判断右括号是否与栈顶左括号匹配。

步骤详解(C++ 视角):

  1. 预判奇数长度:若 s.length() 为奇数,直接返回 false

  2. 构建映射关系:建立右括号 → 左括号的映射(如 ')' → '('),便于快速查找。

  3. 遍历字符串

    • 若当前字符是 左括号(不在哈希表中),压入栈;

    • 若是 右括号(在哈希表中):

      • 检查栈是否为空(无左括号可匹配)→ 无效;
      • 检查栈顶是否等于该右括号对应的左括号 → 不等则无效;
      • 匹配成功,弹出栈顶。
  4. 最终检查:遍历结束后,栈必须为空,否则存在未闭合的左括号。

📜 C++ 核心代码(带行注释):

bool isValid(string s) {
    int n = s.size();
    // 🚩 剪枝:奇数长度不可能有效
    if (n % 2 == 1) return false;

    // 🔑 哈希表:右括号 -> 对应左括号
    unordered_map<char, char> pairs = {
        {')', '('},
        {']', '['},
        {'}', '{'}
    };
    stack<char> stk; // 📦 用于存储左括号

    for (char ch : s) {
        // 🔍 如果是右括号(存在于哈希表中)
        if (pairs.count(ch)) {
            // ❌ 栈空 或 栈顶不匹配 → 无效
            if (stk.empty() || stk.top() != pairs[ch]) {
                return false;
            }
            stk.pop(); // ✅ 匹配成功,弹出
        } else {
            // 📥 左括号,直接入栈
            stk.push(ch);
        }
    }
    // 🎯 最终栈必须为空
    return stk.empty();
}

💡 为什么用“右→左”映射?
因为我们遇到右括号时,需要知道它“期待”的左括号是什么。这样设计逻辑更自然,避免对每个字符做多重 if 判断。


🧩 解题思路(分步拆解)

  1. Step 1:边界处理

    • 字符串为空?✅ 有效(题目允许,且符合定义)。
    • 长度为奇数?❌ 直接返回 false
  2. Step 2:初始化数据结构

    • 创建 unordered_map 存储括号对;
    • 创建 stack 存储待匹配的左括号。
  3. Step 3:逐字符扫描

    • 对每个字符 ch

      • ch 是右括号(通过 pairs.count(ch) 判断):

        • 检查栈是否非空且栈顶 == pairs[ch]
        • 否则立即返回 false
      • 否则(左括号):压栈。

  4. Step 4:最终验证

    • 所有字符处理完后,栈必须为空,否则有未闭合括号。

📊 算法分析

项目分析
时间复杂度O(n):每个字符最多入栈、出栈一次,哈希查找 O(1)。
空间复杂度O(n +
面试考点• 栈的典型应用场景 • 哈希表优化查找 • 边界条件处理(奇数长度、空串) • 代码鲁棒性

💬 面试加分点

  • 主动提出“奇数长度剪枝”;
  • 解释为何用“右→左”而非“左→右”映射;
  • 讨论其他解法(如计数器?不行!因为顺序重要,如 "([)]" 会误判)。

💻 代码

✅ C++ 完整代码(严格遵循模板)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

bool isValid(string s) {
    int n = s.size();
    if (n % 2 == 1) return false;

    unordered_map<char, char> pairs = {
        {')', '('},
        {']', '['},
        {'}', '{'}
    };
    stack<char> stk;

    for (char ch : s) {
        if (pairs.count(ch)) {
            if (stk.empty() || stk.top() != pairs[ch]) {
                return false;
            }
            stk.pop();
        } else {
            stk.push(ch);
        }
    }
    return stk.empty();
}

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 🧪 测试用例
    cout << boolalpha;
    cout << isValid("()") << "\n";       // true
    cout << isValid("()[]{}") << "\n";   // true
    cout << isValid("(]") << "\n";       // false
    cout << isValid("([])") << "\n";     // true
    cout << isValid("([)]") << "\n";     // false
    cout << isValid("{}") << "\n";   // true
    cout << isValid("") << "\n";         // true
    cout << isValid("(((") << "\n";      // false

    return 0;
}

✅ JavaScript 完整代码

/**
 * @param {string} s
 * @return {boolean}
 */
const leftToRight = {
    '(': ')',
    '[': ']',
    '{': '}'
};

const isValid = function(s) {
    if (!s) return true;
    const stack = []; // 栈
    const len = s.length; // 缓存长度
    for (let i = 0; i < len; i++) {
        const ch = s[i];
        if (ch === "(" || ch === "[" || ch === "{") {
            stack.push(leftToRight[ch]);
        } else {
            if (!stack.length || stack.pop() !== ch) {
                return false;
            }
        }
    }
    return !stack.length;
    // 或者 return stack.length === 0;
};

🔁 JS 与 C++ 思路对比

  • JS 版使用 左→右 映射,将期望的右括号压栈,匹配时直接比较 pop() === ch
  • C++ 版使用 右→左 映射,匹配时比较 top() === pairs[ch]
    两种方式等价,各有风格偏好。

🌟 结语 & 下期预告

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!