LeetCode 1249中等栈
移除无效的括号 图解题解
这道题到底在问什么
给定字符串 s(小写字母 + 圆括号)。删除最少数量的括号,使结果中每个左括号都能找到一个在它右边的右括号配对、每个右括号也都有左括号配对。返回任意一个合法结果。
- 输入
- s = "lee(t(c)o)de)"
- 输出
- "lee(t(c)o)de"(删掉末尾多出来的右括号)
- 输入
- s = "a)b(c)d"
- 输出
- "ab(c)d"(删掉开头无配对的右括号)
- 输入
- s = "))(("
- 输出
- ""(四个括号全无效,删光)
最优解:一步一步想明白
- 3核心一句话:栈存「等待配对的左括号下标」。右括号空栈即删,扫完栈中残留即删。记住这套,下面每帧都在套它。
- 4开局:指针还没进入字符串,栈是空的。橙色将标记「正在看」的字符,左边竖栏是栈(底→顶)。
- 5先扫一眼 s="a(b)c)d(":里面有 2 个左括号、2 个右括号。它们能否两两配上,得用栈一位位走才知道。从第 0 位开始。
- 6看第 0 位「a」。先判断它是不是括号,结论是字母,那它和括号配对无关。
- 7字母「a」直接保留、完全不碰栈。栈里仍是 0 个等待配对的左括号,继续往后走。
- 8看第 1 位「(」。左括号先不下结论,把它的下标 1 压进栈,排队等一个右括号来配对。
- 9下标 1 入栈,现在栈顶是它。只要后面来个右括号,这一对就能配上。
- 10看第 2 位「b」。先判断它是不是括号,结论是字母,那它和括号配对无关。
- 11字母「b」直接保留、完全不碰栈。栈里仍是 1 个等待配对的左括号,继续往后走。
- 12看第 3 位「)」。栈不空,栈顶是下标 1 的左括号,正好和它配成一对。
- 13配对成功!弹出栈顶下标 1。位置 1 的「(」和位置 3 的「)」都是合法的,保留。
- 14看第 4 位「c」。先判断它是不是括号,结论是字母,那它和括号配对无关。
- 15字母「c」直接保留、完全不碰栈。栈里仍是 0 个等待配对的左括号,继续往后走。
- 16看第 5 位「)」。栈是空的,左边没有任何待配的左括号,这个右括号是多余的,标记删除。
- 17下标 5 进入「待删」名单。这是第一类坏括号:右括号孤立无援。继续往后扫。
- 18看第 6 位「d」。先判断它是不是括号,结论是字母,那它和括号配对无关。
- 19字母「d」直接保留、完全不碰栈。栈里仍是 0 个等待配对的左括号,继续往后走。
- 20看第 7 位「(」。左括号先不下结论,把它的下标 7 压进栈,排队等一个右括号来配对。
- 21下标 7 入栈,现在栈顶是它。只要后面来个右括号,这一对就能配上。
- 22整串扫完。栈里若还有元素,它们都是「等不到右括号」的左括号,属于第二类坏括号,下面逐个清掉。
- 23栈顶下标 7 的「(」始终没等来右括号,标记删除。栈剩 0 个,继续清。
- 24栈清空了,待删名单确定为下标 { 5、7 }。把没被标记的字符按原顺序拼起来,就是答案。
- 25跳过下标 5、7,剩下的字符拼成 "a(b)cd",所有括号都配对合法,且只删了最少的 2 个。
⚠️ 容易写错的地方
✗ 错:只删右括号、忘了扫完清栈中残留左括号
✓ 对:扫完后栈里剩的左括号也全要删
像 "(((" 这种没右括号的,全靠收尾清栈才能删干净
✗ 错:栈里压字符而不是下标
✓ 对:压下标,方便最后定位要删哪个位置
只压字符无法知道删第几个,重建时定不了位
✗ 错:把字母也当括号处理
✓ 对:字母原样保留,只对 ( 和 ) 操作
动了字母会改变结果内容
完整代码(Python / C++ / Java)
Python
class Solution:
def minRemoveToMakeValid(self, s: str) -> str:
remove = set()
stack = []
for i, ch in enumerate(s):
if ch == '(':
stack.append(i)
elif ch == ')':
if stack:
stack.pop()
else:
remove.add(i)
remove.update(stack)
return ''.join(ch for i, ch in enumerate(s) if i not in remove)C++
#include <string>
#include <unordered_set>
#include <vector>
using namespace std;
class Solution {
public:
string minRemoveToMakeValid(string s) {
vector<int> st;
vector<int> remove(s.size());
for (int i = 0; i < (int)s.size(); ++i) {
if (s[i] == '(') st.push_back(i);
else if (s[i] == ')') {
if (!st.empty()) st.pop_back();
else remove[i] = 1;
}
}
for (int i : st) remove[i] = 1;
string ans;
for (int i = 0; i < (int)s.size(); ++i) if (!remove[i]) ans.push_back(s[i]);
return ans;
}
};Java
import java.util.*;
class Solution {
public String minRemoveToMakeValid(String s) {
boolean[] remove = new boolean[s.length()];
Deque<Integer> st = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') st.push(i);
else if (c == ')') {
if (st.isEmpty()) remove[i] = true;
else st.pop();
}
}
while (!st.isEmpty()) remove[st.pop()] = true;
StringBuilder ans = new StringBuilder();
for (int i = 0; i < s.length(); i++) if (!remove[i]) ans.append(s.charAt(i));
return ans.toString();
}
}复杂度
时间
O(n)
扫一遍标记 + 再拼一遍结果
空间
O(n)
栈和删除标记最多记 n 个下标
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 移除无效的括号 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能不用栈,只用一个计数器?+
可以做正向一遍 + 反向一遍的双计数法:正向扫时右括号数超过左括号就删多余右括号,反向扫时左括号数超过右括号就删多余左括号。空间降到 O(1)(不算结果串)。栈法更直观,计数法更省空间,两者同为 O(n) 时间。
题目说「返回任意一个合法结果」,会有多解吗?+
删除的括号数量是唯一确定的(最少删除数固定),但具体删哪几个有时可选。本解法固定删「无配对」的那些,结果稳定且合法,符合「任意一个」的要求。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 移除无效的括号 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。