Introduction
LeetCode 1021 Remove Outermost Parentheses sounds intimidating with words like "primitive decomposition" but once you strip away the fancy terminology, it is a beautifully simple problem. It builds directly on the depth-counting technique from LeetCode 1614 and is a perfect example of how one clean insight can collapse a seemingly complex problem into just a few lines of code.
Here is the Link of Question -: LeetCode 1021
In this article we cover plain English explanation, what "primitive" really means, two Java approaches with detailed dry runs, complexity analysis, and everything you need to ace this in an interview.
What Is the Problem Really Asking?
Let us ignore the formal definition for a moment and think practically.
A primitive string is the smallest self-contained valid bracket group. Think of it as a top-level unit — it opens, closes completely, and is not part of a bigger bracket group.
For "(()())(())":
"(()())"is one primitive — it opens and fully closes"(())"is another primitive — it opens and fully closes after
Now from each primitive, remove only the outermost opening and closing bracket, keep everything inside.
"(()())"→ remove outer →"()()""(())"→ remove outer →"()"- Combined →
"()()()"
That is literally the whole problem!
Real Life Analogy — Gift Boxes Inside a Shipping Box
Imagine you ordered gifts online. They all arrive in one big shipping box. Inside that box are individual gift boxes. Inside some gift boxes are smaller boxes.
The "primitive decomposition" is identifying each individual gift box inside the shipping box. Removing the outermost parentheses is like removing just the outer gift box wrapping but keeping everything inside it intact.
You are not unpacking the inner boxes — just peeling off one layer from each top-level group.
Understanding Primitive Decomposition Visually
For s = "(()())(())(()(()))":
Each group that starts at depth 0 and returns to depth 0 is one primitive. Remove the outermost ( and ) from each and concatenate what remains.
The depth analogy from LeetCode 1614 is the key — a primitive starts when depth goes from 0 to 1 and ends when depth returns to 0.
Approach 1: Counter With Substring (Your Solution) ✅
The Idea
Track depth with a counter. Use start and end pointers to mark the boundaries of each primitive. When depth hits 0, you have found a complete primitive — append everything between start+1 and end (skipping the outermost brackets) to the result.
Dry Run — s = "(()())(())"
Tracking c, start, end at each step:
i=0,(→ c=1, end=1i=1,(→ c=2, end=2i=2,)→ c=1, end=3i=3,(→ c=2, end=4i=4,)→ c=1, end=5i=5,)→ c=0 → primitive found! appends.substring(1, 5)="()()"→ start=6, end=6i=6,(→ c=1, end=7i=7,(→ c=2, end=8i=8,)→ c=1, end=9i=9,)→ c=0 → primitive found! appends.substring(7, 9)="()"→ start=10, end=10
Result: "()()" + "()" = "()()()" ✅
Time Complexity: O(n) — single pass plus substring operations Space Complexity: O(n) — StringBuilder stores the result
Approach 2: Counter With Depth Filter (Cleaner & More Intuitive)
The Idea
Instead of tracking start/end pointers, use depth directly to decide whether to include each character. The insight is:
- The outermost
(of each primitive is always encountered whencgoes from 0 to 1 — skip it - The outermost
)of each primitive is always encountered whencgoes from 1 to 0 — skip it - Every other character is inside some primitive — include it
This is arguably the most elegant solution. No pointers, no substring, just one rule — if depth is already greater than 0 when you see (, it is not the outer one. If depth is still greater than 0 after decrementing on ), it is not the outer one.
Dry Run — s = "()()" (Edge Case)
(→ depth=0, skip it, depth becomes 1)→ depth becomes 0, depth=0 so skip it(→ depth=0, skip it, depth becomes 1)→ depth becomes 0, depth=0 so skip it
Result: "" ✅ Both are primitives "()" and "()", removing outer from each gives empty string.
Dry Run — s = "(()())(())(()(()))"
Let us trace just which characters get included vs skipped:
For the first primitive "(()())":
(at depth 0 → skip (outermost opener)(at depth 1 → include)at depth 2→1 → include(at depth 1 → include)at depth 2→1 → include)at depth 1→0 → skip (outermost closer)- Inner content
"()()"included ✅
Same logic applies to each subsequent primitive. Final result: "()()()()(())" ✅
Time Complexity: O(n) — single pass Space Complexity: O(n) — StringBuilder stores result
Approach 3: Stack Based (Verbose but Clear)
The Idea
Use an explicit stack to track open brackets. When the stack becomes empty after a ), you have found a complete primitive. Collect all characters between the outermost brackets.
This is the most verbose but makes the primitive detection very explicit — stack empty means primitive complete. Great for explaining your thought process in an interview before optimizing.
Time Complexity: O(n) Space Complexity: O(n) — stack storage
Approach Comparison
The Stack approach is clearest for explaining primitive detection. The pointer-based counter approach (your solution) is direct and efficient. The depth-filter counter approach is the most elegant — no pointers, no substrings, just a single depth check per character. All three are O(n) time. The depth-filter approach wins on code clarity.
How This Connects to the Full Series
Looking at the bracket problem series you have been building:
20 Valid Parentheses — is the string valid? 1614 Maximum Nesting Depth — how deep is the nesting? 1021 Remove Outermost Parentheses — identify and strip top-level groups
Each problem adds one more layer of understanding about bracket strings. The depth counter technique from 1614 directly powers the optimal solution here. This is pattern building at its best.
Common Mistakes to Avoid
Off-by-one in substring indices In your solution, s.substring(start+1, end) skips the outermost ( by using start+1, and end (not end+1) naturally excludes the outermost ) since end points to it. Getting these indices wrong by even one position gives completely wrong output.
Appending the outermost brackets in the depth-filter approach The condition order matters — for (, check depth BEFORE incrementing. For ), check depth AFTER decrementing. Flipping these gives wrong results.
Forgetting to update start after each primitive In pointer-based approaches, always set start = end + 1 (or start = i + 1) after appending each primitive, otherwise the next primitive's start index is wrong.
FAQs — People Also Ask
Q1. What is a primitive valid parentheses string in LeetCode 1021? A primitive is the smallest self-contained valid bracket group that cannot be split into two smaller valid groups. It starts when depth goes from 0 to 1 and ends when depth returns to 0. For example in "(()())(())", the primitives are "(()())" and "(())".
Q2. What is the most elegant solution for LeetCode 1021? The depth-filter counter approach — increment depth on (, decrement on ), and only append characters when depth is greater than 0 for ( and still greater than 0 after decrement for ). This skips outermost brackets naturally without tracking indices.
Q3. What is the time complexity of LeetCode 1021? All approaches run in O(n) time with a single pass. Space complexity is O(n) for storing the output string in StringBuilder.
Q4. How is LeetCode 1021 different from LeetCode 1614? LeetCode 1614 finds the maximum depth by tracking a counter. LeetCode 1021 uses the same counter technique but to identify primitive boundaries and strip their outermost characters. 1614 returns a number, 1021 returns a modified string.
Q5. Is LeetCode 1021 asked in coding interviews? It appears occasionally as a follow-up to Valid Parentheses or Nesting Depth problems. The real skill it tests is understanding primitive decomposition — identifying self-contained units within a bracket string — which appears in real world parser and compiler design.
Similar LeetCode Problems to Practice Next
- 20. Valid Parentheses — Easy — validate bracket structure
- 1614. Maximum Nesting Depth of Parentheses — Easy — find max depth
- 1249. Minimum Remove to Make Valid Parentheses — Medium — remove minimum brackets
- 394. Decode String — Medium — nested brackets with encoding
- 32. Longest Valid Parentheses — Hard — longest valid substring
Conclusion
LeetCode 1021 Remove Outermost Parentheses is a satisfying problem once you realize that "primitive decomposition" is just a fancy way of saying "find each top-level bracket group." The depth counter is the key — depth 0 to 1 marks the start of a primitive, depth 1 to 0 marks its end.
The depth-filter approach is the cleanest solution — one pass, one counter, one condition. No substring, no pointers, just elegance. Master this alongside LeetCode 20 and 1614 and you will have a complete toolkit for any bracket string problem that comes your way.




