Facebook Pixel

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 + ")" where A is a valid parentheses string
  • It can be A + B where both A and B 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:

  1. Decompose it into primitive components (P₁ + P₂ + ... + Pₖ)
  2. Remove the outermost parentheses from each primitive component
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Initialize variables:

    • ans = []: List to store characters of the result string
    • cnt = 0: Counter to track nesting level of parentheses
  2. 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 to ans
      • If false (cnt == 1), this is an outermost (, so skip it

    For closing parenthesis ')':

    • Decrement the counter first: cnt -= 1
    • Check if cnt > 0:
      • If true, this is NOT an outermost ), so append it to ans
      • If false (cnt == 0), this is an outermost ), so skip it
  3. Return the result:

    • Join all characters in ans to form the final string: ''.join(ans)

Example Walkthrough with "(()())(())":

CharacterActioncnt beforecnt afterAdd to result?Result so far
(increment01No (cnt=1)""
(increment12Yes (cnt>1)"("
)decrement21Yes (cnt>0)"()"
(increment12Yes (cnt>1)"()("
)decrement21Yes (cnt>0)"()()"
)decrement10No (cnt=0)"()()"
(increment01No (cnt=1)"()()"
(increment12Yes (cnt>1)"()()("
)decrement21Yes (cnt>0)"()()()"
)decrement10No (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 Evaluator

Example 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 "(()())"

CharCounter BeforeActionCounter AfterKeep?Reason
(0cnt++1NoOutermost opening
(1cnt++2YesInner parenthesis (cnt > 1)
)2cnt--1YesInner parenthesis (cnt > 0)
(1cnt++2YesInner parenthesis (cnt > 1)
)2cnt--1YesInner parenthesis (cnt > 0)
)1cnt--0NoOutermost 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 (, checking depth > 0 before incrementing means the first ( of each primitive (when depth=0) won't be skipped
  • For ), checking depth > 1 before decrementing means inner ) characters might be incorrectly skipped

Correct Approach:

  • For (: Increment FIRST, then check if depth > 1
  • For ): Decrement FIRST, then check if depth > 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 (: check depth > 1 (we're nested inside)
  • After decrementing for ): check depth > 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More