LeetCode 1021简单栈 · 单调栈
删除最外层的括号 图解题解
这道题到底在问什么
有效括号串 s 可以唯一拆成若干个「原语」(不能再被拆成两段非空有效串的最小单元)。把每个原语最外层的一对括号去掉,再拼接,返回结果。
- 输入
- s = "(()())(())"
- 输出
- "()()()"
最优解:一步一步想明白
- 3记住这句口诀:左括号深度大于 0 才输出再加一,右括号先减一还大于 0 才输出。后面每一帧都在套它。
- 4开局:深度计数器 depth = 0,结果串为空。上面一排是要扫的括号串,下面这根栈每压入一个左括号就长高一层,栈高就是当前深度。指针从第 0 个字符开始往右走。
- 5指针移到第 0 个字符,是一个左括号。先看现在的深度是 0,也就是栈里还没闭合的左括号有 0 个,再决定这个字符输不输出。
- 6这个左括号进栈前深度是 0,它正是一个原语最外层那一层,按规矩跳过不输出,只让深度加一变成 1。
- 7指针移到第 1 个字符,是一个左括号。先看现在的深度是 1,也就是栈里还没闭合的左括号有 1 个,再决定这个字符输不输出。
- 8这个左括号进栈前深度是 1,已经大于 0,说明外面还套着括号,它是内层的,要输出。先把 ( 写进结果,再让深度加一变成 2。
- 9指针移到第 2 个字符,是一个右括号。先看现在的深度是 2,也就是栈里还没闭合的左括号有 2 个,再决定这个字符输不输出。
- 10右括号先让深度减一,现在是 1,还大于 0,说明它封的不是最外层,要输出。把 ) 写进结果。
- 11指针移到第 3 个字符,是一个左括号。先看现在的深度是 1,也就是栈里还没闭合的左括号有 1 个,再决定这个字符输不输出。
- 12这个左括号进栈前深度是 1,已经大于 0,说明外面还套着括号,它是内层的,要输出。先把 ( 写进结果,再让深度加一变成 2。
- 13指针移到第 4 个字符,是一个右括号。先看现在的深度是 2,也就是栈里还没闭合的左括号有 2 个,再决定这个字符输不输出。
- 14右括号先让深度减一,现在是 1,还大于 0,说明它封的不是最外层,要输出。把 ) 写进结果。
- 15指针移到第 5 个字符,是一个右括号。先看现在的深度是 1,也就是栈里还没闭合的左括号有 1 个,再决定这个字符输不输出。
- 16右括号让深度减一变成 0,正好闭合了一整个原语,它是最外层的右括号,跳过不输出,结果里又攒齐了一段。
- 17指针移到第 6 个字符,是一个左括号。先看现在的深度是 0,也就是栈里还没闭合的左括号有 0 个,再决定这个字符输不输出。
- 18这个左括号进栈前深度是 0,它正是一个原语最外层那一层,按规矩跳过不输出,只让深度加一变成 1。
- 19指针移到第 7 个字符,是一个左括号。先看现在的深度是 1,也就是栈里还没闭合的左括号有 1 个,再决定这个字符输不输出。
- 20这个左括号进栈前深度是 1,已经大于 0,说明外面还套着括号,它是内层的,要输出。先把 ( 写进结果,再让深度加一变成 2。
- 21指针移到第 8 个字符,是一个右括号。先看现在的深度是 2,也就是栈里还没闭合的左括号有 2 个,再决定这个字符输不输出。
- 22右括号先让深度减一,现在是 1,还大于 0,说明它封的不是最外层,要输出。把 ) 写进结果。
- 23指针移到第 9 个字符,是一个右括号。先看现在的深度是 1,也就是栈里还没闭合的左括号有 1 个,再决定这个字符输不输出。
- 24右括号让深度减一变成 0,正好闭合了一整个原语,它是最外层的右括号,跳过不输出,结果里又攒齐了一段。
- 25整串扫完,栈又空了、深度从 0 出发也回到 0,说明括号配对完整。最外层的每一对括号都被跳过,留下的就是去掉外层后的结果 ()()()。
⚠️ 容易写错的地方
✗ 错:左右括号的判断顺序写反
✓ 对:左括号先加再判断是否大于 1,右括号先减再判断是否大于 0
顺序错了会把最外层那对括号也输出,或把内层的漏掉
✗ 错:以为只删整串最外面一对括号
✓ 对:要按原语分段,每段各删一对
(()())(()) 是两个原语,各自的外层都要删,不是只删整串两头
✗ 错:把输出结果的长度算进空间复杂度
✓ 对:额外空间只有一个计数器 O(1)
结果串是题目要求的返回值,一般不计入额外空间
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def removeOuterParentheses(self, s: str) -> str:
ans = []
cnt = 0
for c in s:
if c == '(':
cnt += 1
if cnt > 1:
ans.append(c)
else:
cnt -= 1
if cnt > 0:
ans.append(c)
return ''.join(ans)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
string removeOuterParentheses(string s) {
string ans;
int cnt = 0;
for (char& c : s) {
if (c == '(') {
if (++cnt > 1) {
ans.push_back(c);
}
} else {
if (--cnt) {
ans.push_back(c);
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public String removeOuterParentheses(String s) {
StringBuilder ans = new StringBuilder();
int cnt = 0;
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (c == '(') {
if (++cnt > 1) {
ans.append(c);
}
} else {
if (--cnt > 0) {
ans.append(c);
}
}
}
return ans.toString();
}
}复杂度
时间
O(n)
每个字符只看一次,一遍扫完
空间
O(1)
只用一个深度计数器;返回的结果串不计入额外空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 删除最外层的括号 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和判断括号是否有效(lc20)有什么联系?+
内核是同一套深度计数。lc20 有多种括号,要用栈记住类型才能配对;这题只有一种括号,深度数值就够。两题都靠深度归零来识别一个完整段的结束,lc20 用它判匹配,这题用它找原语的外层。
如果让你顺便返回原语的个数,怎么改?+
每次深度从 1 减到 0,就意味着一个原语刚刚闭合,计数加一即可。扫完一遍就同时得到删外层的结果和原语个数,不增加复杂度。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 删除最外层的括号 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。