1021. Remove Outermost Parentheses
Problem Description
This problem asks you to remove the outermost parentheses from each primitive component of a valid parentheses string.
First, let's understand the key concepts:
Valid Parentheses String: A string that follows these rules:
- It can be empty
""
- It can be
"(" + A + ")"
whereA
is a valid parentheses string - It can be
A + B
where bothA
andB
are valid parentheses strings
Examples: ""
, "()"
, "(())()"
, "(()(()))"
Primitive String: A valid parentheses string that cannot be split into two non-empty valid parentheses strings. In other words, it's a "complete" unit that starts with (
and ends with its matching )
, with no way to break it into smaller valid parts.
For example:
"(())"
is primitive - you can't split it into two valid parts"()()"
is NOT primitive - it can be split into"()"
+"()"
The Task: Given a valid parentheses string s
, you need to:
- Decompose it into primitive components (P₁ + P₂ + ... + Pₖ)
- Remove the outermost parentheses from each primitive component
- Concatenate the results
Example walkthrough:
- Input:
"(()())(())"
- This decomposes into two primitive parts:
"(()())"
and"(())"
- Remove outer parentheses from
"(()())"
→"()()"
- Remove outer parentheses from
"(())"
→"()"
- Result:
"()()()"
The solution uses a counter to track when we're inside a primitive component. When the counter is greater than 1 for (
or greater than 0 for )
, we know these are not the outermost parentheses, so we include them in the result.
Intuition
The key insight is recognizing that we need to identify where each primitive component begins and ends, then skip only the first (
and last )
of each component.
Think about how parentheses work: every (
must have a matching )
. When we traverse the string from left to right, we can use a counter that increases for (
and decreases for )
. When this counter returns to 0, we've completed a primitive component.
For example, in "(()())(())"
:
- Start with counter = 0
- First
(
: counter becomes 1 - Second
(
: counter becomes 2 - First
)
: counter becomes 1 - Second
(
: counter becomes 2 - Second
)
: counter becomes 1 - Third
)
: counter becomes 0 → First primitive ends here! - The pattern repeats for the second primitive
Now, which parentheses should we keep? We want to remove only the outermost ones from each primitive. Notice that:
- The first
(
of a primitive is when counter goes from 0 to 1 - The last
)
of a primitive is when counter goes from 1 to 0 - All other parentheses are "inner" and should be kept
This leads to our solution strategy:
- For
(
: add it to result only if counter > 1 (after incrementing) - For
)
: add it to result only if counter > 0 (after decrementing)
This elegantly filters out exactly the outermost parentheses of each primitive component without explicitly finding the boundaries of each primitive.
Learn more about Stack patterns.
Solution Approach
The implementation uses a simple counter-based approach with a single pass through the string:
Algorithm Steps:
-
Initialize variables:
ans = []
: List to store characters of the result stringcnt = 0
: Counter to track nesting level of parentheses
-
Iterate through each character in the string:
For opening parenthesis
'('
:- Increment the counter first:
cnt += 1
- Check if
cnt > 1
:- If true, this is NOT an outermost
(
, so append it toans
- If false (cnt == 1), this is an outermost
(
, so skip it
- If true, this is NOT an outermost
For closing parenthesis
')'
:- Decrement the counter first:
cnt -= 1
- Check if
cnt > 0
:- If true, this is NOT an outermost
)
, so append it toans
- If false (cnt == 0), this is an outermost
)
, so skip it
- If true, this is NOT an outermost
- Increment the counter first:
-
Return the result:
- Join all characters in
ans
to form the final string:''.join(ans)
- Join all characters in
Example Walkthrough with "(()())(())"
:
Character | Action | cnt before | cnt after | Add to result? | Result so far |
---|---|---|---|---|---|
( | increment | 0 | 1 | No (cnt=1) | "" |
( | increment | 1 | 2 | Yes (cnt>1) | "(" |
) | decrement | 2 | 1 | Yes (cnt>0) | "()" |
( | increment | 1 | 2 | Yes (cnt>1) | "()(" |
) | decrement | 2 | 1 | Yes (cnt>0) | "()()" |
) | decrement | 1 | 0 | No (cnt=0) | "()()" |
( | increment | 0 | 1 | No (cnt=1) | "()()" |
( | increment | 1 | 2 | Yes (cnt>1) | "()()(" |
) | decrement | 2 | 1 | Yes (cnt>0) | "()()()" |
) | decrement | 1 | 0 | No (cnt=0) | "()()()" |
Time and Space Complexity:
- Time:
O(n)
where n is the length of the input string (single pass) - Space:
O(n)
for storing the result characters in the list
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with input "(()())(())(()(()))"
Step 1: Identify the primitive components
- A primitive component is complete when our counter returns to 0
"(()())"
- First primitive (counter: 0→1→2→1→2→1→0)"(())"
- Second primitive (counter: 0→1→2→1→0)"(()(()))"
- Third primitive (counter: 0→1→2→1→2→1→2→1→0)
Step 2: Process each character with our counter algorithm
Let's trace through the first primitive "(()())"
:
Char | Counter Before | Action | Counter After | Keep? | Reason |
---|---|---|---|---|---|
( | 0 | cnt++ | 1 | No | Outermost opening |
( | 1 | cnt++ | 2 | Yes | Inner parenthesis (cnt > 1) |
) | 2 | cnt-- | 1 | Yes | Inner parenthesis (cnt > 0) |
( | 1 | cnt++ | 2 | Yes | Inner parenthesis (cnt > 1) |
) | 2 | cnt-- | 1 | Yes | Inner parenthesis (cnt > 0) |
) | 1 | cnt-- | 0 | No | Outermost closing |
Result from first primitive: "()()"
Step 3: Apply same logic to remaining primitives
- Second primitive
"(())"
→ Keep middle()
→ Result:"()"
- Third primitive
"(()(()))"
→ Keep middle"()(())"
→ Result:"()(())"
Step 4: Concatenate all results
Final answer: "()()"
+ "()"
+ "()(())"
= "()()()()(())"
The beauty of this approach is that we don't need to explicitly find primitive boundaries - the counter naturally filters out exactly the outermost parentheses of each primitive component in a single pass.
Solution Implementation
1class Solution:
2 def removeOuterParentheses(self, s: str) -> str:
3 """
4 Remove the outermost parentheses of every primitive valid parentheses string.
5 A primitive string is a valid parentheses string that cannot be split into
6 two non-empty valid parentheses strings.
7
8 Args:
9 s: A valid parentheses string
10
11 Returns:
12 The string with outermost parentheses removed from each primitive part
13 """
14 result = []
15 depth = 0 # Track the depth/level of nested parentheses
16
17 for char in s:
18 if char == '(':
19 # Increment depth when encountering opening parenthesis
20 depth += 1
21 # Only add to result if it's not an outermost opening parenthesis
22 # (depth > 1 means we're inside at least one pair already)
23 if depth > 1:
24 result.append(char)
25 else: # char == ')'
26 # Decrement depth when encountering closing parenthesis
27 depth -= 1
28 # Only add to result if it's not an outermost closing parenthesis
29 # (depth > 0 means we're still inside at least one pair)
30 if depth > 0:
31 result.append(char)
32
33 return ''.join(result)
34
1class Solution {
2 public String removeOuterParentheses(String s) {
3 // StringBuilder to store the result after removing outer parentheses
4 StringBuilder result = new StringBuilder();
5
6 // Counter to track the depth of nested parentheses
7 int depth = 0;
8
9 // Iterate through each character in the input string
10 for (int i = 0; i < s.length(); i++) {
11 char currentChar = s.charAt(i);
12
13 if (currentChar == '(') {
14 // Opening parenthesis case
15 // Increment depth first, then check if it's not an outer parenthesis
16 depth++;
17 if (depth > 1) {
18 // Not an outer opening parenthesis, so add it to result
19 result.append(currentChar);
20 }
21 } else {
22 // Closing parenthesis case
23 // Decrement depth first, then check if it's not an outer parenthesis
24 depth--;
25 if (depth > 0) {
26 // Not an outer closing parenthesis, so add it to result
27 result.append(currentChar);
28 }
29 }
30 }
31
32 // Convert StringBuilder to String and return
33 return result.toString();
34 }
35}
36
1class Solution {
2public:
3 string removeOuterParentheses(string s) {
4 string result;
5 int openCount = 0; // Counter to track the depth of parentheses
6
7 for (char& ch : s) {
8 if (ch == '(') {
9 // Increment counter first, then check if it's not an outer opening parenthesis
10 openCount++;
11 if (openCount > 1) {
12 // This is not an outer opening parenthesis, add it to result
13 result.push_back(ch);
14 }
15 } else { // ch == ')'
16 // Decrement counter first, then check if it's not an outer closing parenthesis
17 openCount--;
18 if (openCount > 0) {
19 // This is not an outer closing parenthesis, add it to result
20 result.push_back(ch);
21 }
22 }
23 }
24
25 return result;
26 }
27};
28
1/**
2 * Removes the outermost parentheses from each primitive valid parentheses string
3 * @param s - Input string containing valid parentheses
4 * @returns String with outer parentheses removed from each primitive part
5 */
6function removeOuterParentheses(s: string): string {
7 let result: string = '';
8 let depth: number = 0;
9
10 // Iterate through each character in the input string
11 for (const character of s) {
12 // Increment depth when encountering an opening parenthesis
13 if (character === '(') {
14 depth++;
15 }
16
17 // Add character to result only if it's not part of the outermost layer
18 // (depth === 1 means we're at the outermost opening or closing parenthesis)
19 if (depth !== 1) {
20 result += character;
21 }
22
23 // Decrement depth when encountering a closing parenthesis
24 if (character === ')') {
25 depth--;
26 }
27 }
28
29 return result;
30}
31
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the input string s
. The algorithm iterates through each character in the string exactly once. Each operation inside the loop (comparison, increment/decrement, and append) takes O(1)
time, resulting in a total time complexity of O(n)
.
Space Complexity: O(n)
, where n
is the length of the input string s
. In the worst case, the ans
list could store almost all characters from the input string (except for the outermost parentheses of each primitive string). For example, if the input is "(())"
, the output would be "()"
, storing 2 out of 4 characters. The final join
operation creates a new string of similar size. Therefore, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Order of Counter Update
The Problem: A common mistake is updating the counter AFTER checking the condition, which leads to incorrect identification of outermost parentheses.
Incorrect Implementation:
def removeOuterParentheses(self, s: str) -> str:
result = []
depth = 0
for char in s:
if char == '(':
if depth > 0: # Check BEFORE incrementing
result.append(char)
depth += 1 # Increment AFTER checking
else:
if depth > 1: # Check BEFORE decrementing
result.append(char)
depth -= 1 # Decrement AFTER checking
return ''.join(result)
Why it fails:
- For
(
, checkingdepth > 0
before incrementing means the first(
of each primitive (when depth=0) won't be skipped - For
)
, checkingdepth > 1
before decrementing means inner)
characters might be incorrectly skipped
Correct Approach:
- For
(
: Increment FIRST, then check ifdepth > 1
- For
)
: Decrement FIRST, then check ifdepth > 0
Pitfall 2: Using Wrong Threshold Values
The Problem: Using the same threshold for both opening and closing parentheses, or mixing up the conditions.
Incorrect Implementation:
def removeOuterParentheses(self, s: str) -> str:
result = []
depth = 0
for char in s:
if char == '(':
depth += 1
if depth > 0: # Wrong: should be > 1
result.append(char)
else:
depth -= 1
if depth >= 0: # Wrong: should be > 0
result.append(char)
return ''.join(result)
Why it fails:
- Using
depth > 0
for(
would include the outermost opening parenthesis - Using
depth >= 0
for)
would include the outermost closing parenthesis
Correct Thresholds:
- After incrementing for
(
: checkdepth > 1
(we're nested inside) - After decrementing for
)
: checkdepth > 0
(we're still inside)
Pitfall 3: Attempting to Track Primitive Boundaries Explicitly
The Problem: Trying to explicitly identify where each primitive component starts and ends, which adds unnecessary complexity.
Overcomplicated Approach:
def removeOuterParentheses(self, s: str) -> str:
result = []
depth = 0
start = 0 # Unnecessary tracking
for i, char in enumerate(s):
if char == '(':
if depth == 0:
start = i # Mark primitive start
depth += 1
else:
depth -= 1
if depth == 0:
# Extract primitive and process
primitive = s[start:i+1]
result.append(primitive[1:-1])
return ''.join(result)
Why it's problematic:
- Adds unnecessary variables and logic
- More prone to off-by-one errors
- Harder to understand and maintain
Better Solution: The counter-based approach naturally handles primitive boundaries without explicit tracking. When the counter returns to 0, we've completed a primitive component.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!