LeetCode 1190 - Reverse Substrings Between Each Pair of Parentheses
The problem gives us a string s containing lowercase English letters and balanced parentheses. Our task is to repeatedly reverse the substrings enclosed inside every matching pair of parentheses, starting from the innermost pair first.
Difficulty: š” Medium
Topics: String, Stack
Solution
Problem Understanding
The problem gives us a string s containing lowercase English letters and balanced parentheses. Our task is to repeatedly reverse the substrings enclosed inside every matching pair of parentheses, starting from the innermost pair first. After all reversals are complete, we must return the final string without any parentheses.
The important detail is that nested parentheses matter. When one pair of parentheses exists inside another pair, the inner substring must be reversed before the outer substring is processed. This means the transformations happen from the deepest nesting level outward.
For example, consider:
(u(love)i)
The innermost substring is:
(love)
Reversing it gives:
(u(evol)i)
Now the outer parentheses contain:
uevoli
Reversing that gives:
iloveu
The final answer removes all parentheses.
The input size is at most 2000 characters. This is small enough that even some quadratic solutions can pass, but repeated substring reconstruction can still become inefficient and messy. Since the parentheses are guaranteed to be balanced, we never need to worry about invalid expressions or unmatched brackets.
Several edge cases are important:
- Strings with no parentheses at all, such as
"abc", should simply return"abc". - Deeply nested parentheses can expose inefficient reversal strategies.
- Empty parentheses such as
"()"produce an empty substring and should not break the logic. - Multiple adjacent parenthesized sections must each be processed independently.
- Nested reversals can restore original ordering in surprising ways, so careful processing order is necessary.
Approaches
Brute Force Approach
A straightforward approach is to repeatedly locate the innermost pair of parentheses, reverse the substring inside it, replace the entire parenthesized portion with the reversed substring, and continue until no parentheses remain.
One way to implement this is:
- Scan the string from left to right.
- Whenever a closing parenthesis
)is found, search backward for its matching opening parenthesis(. - Extract the substring inside.
- Reverse it.
- Rebuild the string without the parentheses.
- Restart scanning.
This works because reversing the innermost substring first matches the problem requirement. However, every replacement creates a new string, and repeated scans across the string become expensive.
In the worst case, each reversal operation touches nearly the entire string, and there may be many nested pairs. This leads to quadratic time complexity.
Optimal Approach
The key insight is that parentheses define directional changes in traversal.
Instead of repeatedly reconstructing substrings, we can preprocess the matching parentheses positions using a stack. Once we know which parentheses match each other, we can simulate traversal through the string.
The clever trick is:
- Normally, we move left to right.
- When we encounter a parenthesis, we jump to its matching parenthesis and reverse traversal direction.
- Because direction changes naturally simulate substring reversal, we never explicitly reverse substrings.
This produces an elegant linear time solution.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Repeated substring rebuilding and rescanning |
| Optimal | O(n) | O(n) | Uses matching parenthesis indices and directional traversal |
Algorithm Walkthrough
Step 1: Match Parentheses Using a Stack
We first preprocess the string to determine which parentheses belong together.
We use a stack to store indices of opening parentheses.
As we scan the string:
- If we see
'(', we push its index onto the stack. - If we see
')', we pop the most recent opening index. - These two indices form a matching pair.
We store the mapping in a dictionary or array:
partner[left_index] = right_index
partner[right_index] = left_index
This allows constant time jumps between matching parentheses.
Step 2: Traverse the String
We now simulate movement through the string.
We maintain:
index, the current positiondirection, either1for left-to-right or-1for right-to-left
Initially:
index = 0
direction = 1
Step 3: Process Each Character
While the index stays within bounds:
-
If the current character is a letter:
-
Append it to the result.
-
Move to the next position using the current direction.
-
If the current character is a parenthesis:
-
Jump to its matching parenthesis using the mapping.
-
Reverse the traversal direction.
-
Move one step further in the new direction.
The direction reversal is the crucial idea. It naturally simulates substring reversal without explicitly reversing any substring.
Step 4: Build the Final Result
Because parentheses are never appended to the result, the final string automatically excludes them.
We join all collected characters and return the final string.
Why it works
Whenever traversal enters a parenthesized section, direction flips. This means characters inside that section are visited in reverse order. If another nested parenthesis is encountered, direction flips again, restoring the original direction inside the nested section. These alternating direction changes exactly reproduce the required innermost-first reversal behavior.
Since every character is visited at most once during traversal, the algorithm produces the correct reversed output efficiently.
Python Solution
class Solution:
def reverseParentheses(self, s: str) -> str:
n = len(s)
pair = {}
stack = []
# Match parentheses
for index, char in enumerate(s):
if char == '(':
stack.append(index)
elif char == ')':
opening_index = stack.pop()
pair[opening_index] = index
pair[index] = opening_index
result = []
index = 0
direction = 1
while 0 <= index < n:
if s[index] == '(' or s[index] == ')':
index = pair[index]
direction *= -1
else:
result.append(s[index])
index += direction
return ''.join(result)
The implementation begins by preprocessing the parentheses relationships. The stack stores indices of opening parentheses. Whenever a closing parenthesis is encountered, we pop the matching opening index and record the relationship in both directions.
The second phase performs directional traversal.
The index variable tracks our current position, while direction determines whether we move left-to-right or right-to-left.
When we encounter a normal character, we append it directly to the result.
When we encounter a parenthesis, we jump to its matching partner and reverse the traversal direction. This effectively simulates reversing the enclosed substring without actually creating reversed substrings.
Finally, we join the collected characters into the final answer.
Go Solution
func reverseParentheses(s string) string {
n := len(s)
pair := make(map[int]int)
stack := []int{}
// Match parentheses
for i, ch := range s {
if ch == '(' {
stack = append(stack, i)
} else if ch == ')' {
openingIndex := stack[len(stack)-1]
stack = stack[:len(stack)-1]
pair[openingIndex] = i
pair[i] = openingIndex
}
}
result := []byte{}
index := 0
direction := 1
for index >= 0 && index < n {
if s[index] == '(' || s[index] == ')' {
index = pair[index]
direction *= -1
} else {
result = append(result, s[index])
}
index += direction
}
return string(result)
}
The Go implementation follows the same logic as the Python solution.
A map[int]int stores matching parenthesis indices, while a slice is used as the stack.
The result is accumulated using a []byte, which is efficient for string construction in Go. Since the input only contains ASCII lowercase letters and parentheses, byte-based processing is completely safe.
Unlike Python lists, Go slices require manual removal of the top stack element using slicing syntax:
stack = stack[:len(stack)-1]
No special handling for integer overflow is required because indices remain within the string length constraints.
Worked Examples
Example 1
Input:
(abcd)
Parenthesis Mapping
| Index | Character | Matching Index |
|---|---|---|
| 0 | ( | 5 |
| 5 | ) | 0 |
Traversal
| Step | Index | Character | Direction | Result |
|---|---|---|---|---|
| 1 | 0 | ( | 1 ā -1 | "" |
| 2 | 4 | d | -1 | "d" |
| 3 | 3 | c | -1 | "dc" |
| 4 | 2 | b | -1 | "dcb" |
| 5 | 1 | a | -1 | "dcba" |
| 6 | 0 | ( | -1 ā 1 | "dcba" |
Final answer:
dcba
Example 2
Input:
(u(love)i)
Parenthesis Mapping
| Index | Character | Matching Index |
|---|---|---|
| 0 | ( | 9 |
| 2 | ( | 7 |
| 7 | ) | 2 |
| 9 | ) | 0 |
Traversal
| Step | Index | Character | Direction | Result |
|---|---|---|---|---|
| 1 | 0 | ( | 1 ā -1 | "" |
| 2 | 8 | i | -1 | "i" |
| 3 | 7 | ) | -1 ā 1 | "i" |
| 4 | 3 | l | 1 | "il" |
| 5 | 4 | o | 1 | "ilo" |
| 6 | 5 | v | 1 | "ilov" |
| 7 | 6 | e | 1 | "ilove" |
| 8 | 7 | ) | 1 ā -1 | "ilove" |
| 9 | 1 | u | -1 | "iloveu" |
Final answer:
iloveu
Example 3
Input:
(ed(et(oc))el)
Parenthesis Mapping
| Index | Character | Matching Index |
|---|---|---|
| 0 | ( | 13 |
| 3 | ( | 10 |
| 6 | ( | 9 |
| 9 | ) | 6 |
| 10 | ) | 3 |
| 13 | ) | 0 |
Traversal Result
The traversal sequence produces:
leetcode
The nested direction reversals naturally simulate:
"oc"ā"co""etco"ā"octe"- Entire substring reversal ā
"leetcode"
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each character is processed at most once |
| Space | O(n) | Stack, parenthesis mapping, and output storage |
The preprocessing step scans the string once to match parentheses. The traversal phase also visits each position at most once because jumps occur in constant time using the mapping table.
Although direction changes occur frequently, each character contributes only constant work overall, giving linear time complexity.
The extra space comes from:
- The stack used for matching parentheses
- The mapping structure
- The output buffer
All of these scale linearly with the input size.
Test Cases
solution = Solution()
assert solution.reverseParentheses("(abcd)") == "dcba" # simple reversal
assert solution.reverseParentheses("(u(love)i)") == "iloveu" # nested reversal
assert solution.reverseParentheses("(ed(et(oc))el)") == "leetcode" # multiple nesting levels
assert solution.reverseParentheses("abc") == "abc" # no parentheses
assert solution.reverseParentheses("a(bc)d") == "acbd" # single middle section
assert solution.reverseParentheses("a(bcdefghijkl(mno)p)q") == "apmnolkjihgfedcbq" # deep nesting
assert solution.reverseParentheses("()") == "" # empty parentheses
assert solution.reverseParentheses("(a)") == "a" # single character inside parentheses
assert solution.reverseParentheses("((abc))") == "abc" # double reversal restores order
assert solution.reverseParentheses("(ab(cd)ef)") == "fecdba" # nested inner section
assert solution.reverseParentheses("x(yz)") == "xzy" # suffix reversal
assert solution.reverseParentheses("(x(y(z)))") == "yzx" # multiple nested layers
Test Summary
| Test | Why |
|---|---|
(abcd) |
Basic reversal |
(u(love)i) |
Nested parentheses |
(ed(et(oc))el) |
Multiple nesting levels |
abc |
No parentheses |
a(bc)d |
Parentheses in the middle |
a(bcdefghijkl(mno)p)q |
Deep nesting stress test |
() |
Empty substring |
(a) |
Single character reversal |
((abc)) |
Double reversal behavior |
(ab(cd)ef) |
Nested mixed sections |
x(yz) |
Reversal near end of string |
(x(y(z))) |
Multiple direction flips |
Edge Cases
One important edge case is a string with no parentheses at all, such as "abc". A buggy implementation might assume every string contains at least one parenthesized section and fail unnecessarily. In this implementation, traversal simply appends every character directly to the result because no direction changes ever occur.
Another critical case is empty parentheses:
()
Some substring-based approaches accidentally produce index errors or attempt invalid reversals on empty ranges. In this solution, the traversal simply jumps across the matching parentheses and reverses direction without appending any characters, correctly producing an empty string.
Deeply nested parentheses are another common source of bugs. For example:
((abc))
The inner reversal produces "cba", while the outer reversal restores the original order "abc". Implementations that reverse substrings in the wrong order often fail here. The directional traversal method handles nesting naturally because each pair of parentheses flips traversal direction exactly once.
A final tricky case involves adjacent nested sections with characters outside parentheses, such as:
a(bc)d
The algorithm must preserve characters outside parentheses while reversing only the enclosed substring. Since traversal only changes direction inside matched pairs, outer characters remain in their original positions automatically.