Topics › Stacks and Queues › Learning
Balanced Paranthesis
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given a string s made only of the six bracket characters (, ), [, ], {, }. Decide whether it is balanced: every opening bracket is closed by a bracket of the same kind, brackets close in the reverse order they were opened, and no closing bracket appears without a matching opener. An empty string is balanced.
Constraints: 0 ≤ n ≤ 10^4.
Example 1
s = "{[()]}"
answer = true // each closer matches the most recent unclosed opener
Example 2
s = "([)]"
answer = false // ')' arrives while '[' is the most recent unclosed opener
Example 3
s = "(()"
answer = false // one '(' is never closed
Approach 1: Brute force, cancel adjacent pairs
Intuition. In a balanced string there is always at least one pair of brackets that are adjacent and match, such as the () in the middle of {[()]}: the innermost pair has nothing between its two halves. Deleting such a pair leaves a shorter string that is still balanced, and repeating the deletion eventually leaves nothing. Conversely, if the string is unbalanced, at some point no adjacent matching pair exists and something is left over. So: strip (), [] and {} repeatedly and check whether the string vanishes.
Algorithm.
- Repeat: replace every occurrence of
(),[],{}in the string with the empty string. - Stop when a round removes nothing (the length did not change).
- Return
trueif the remaining string is empty.
static boolean bruteForce(String s) {
String cur = s;
while (true) {
String next = cur.replace("()", "").replace("[]", "").replace("{}", "");
if (next.length() == cur.length()) break; // no adjacent pair left to remove
cur = next;
}
return cur.isEmpty();
}
Complexity. Time O(n^2): each round scans the whole string in O(n), and since every productive round removes at least two characters there are at most n/2 rounds. A deeply nested string such as ((((...)))) really does need n/2 rounds. Space O(n) for the intermediate strings.
This is a fine way to explain balance, and it is easy to get right, but it is quadratic and allocates a new string per round. The stack approach does one pass.
Approach 2: Optimal, a stack of open brackets
Intuition. Read left to right. An opening bracket creates an obligation: “something must close me later”. The obligation created most recently is the one that must be met first, which is exactly last-in-first-out behaviour. So push openers on a stack. When a closer arrives, the top of the stack is the only bracket it is allowed to close: if the stack is empty, or the top is a different kind, the string is invalid. At the end, any opener still on the stack was never closed.
Algorithm.
- Create an empty stack of characters.
- For each character
cofs:- If
cis(,[or{, push it. - Otherwise
cis a closer. If the stack is empty, returnfalse. Pop the top; if it is not the opener matchingc, returnfalse.
- If
- Return
trueif the stack is empty,falseotherwise.
static boolean optimal(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) return false; // a closer with nothing open
char open = stack.pop();
if (!matches(open, c)) return false; // the wrong kind of closer
}
}
return stack.isEmpty(); // anything still open was never closed
}
static boolean matches(char open, char close) {
return (open == '(' && close == ')') || (open == '[' && close == ']') || (open == '{' && close == '}');
}
Dry run on example 2, s = "([)]":
| i | c | stack before | action |
|---|---|---|---|
| 0 | ( | [] | push → [(] |
| 1 | [ | [(] | push → [(, [] |
| 2 | ) | [(, [] | pop [, does not match ) → return false |
And on example 1, s = "{[()]}": push {, push [, push (, then ) pops (, ] pops [, } pops {, stack empty → true.
Complexity. Time O(n): each character is pushed and popped at most once. Space O(n) for the stack in the worst case, for example (((( followed by )))).
Approach 3: Push the expected closer
Intuition. The matches table exists because we store what we saw and later compare it with what we see. If instead we push what we expect to see, the comparison becomes a plain equality: on ( push ), on [ push ], on { push }. A closer is then valid exactly when it equals the top of the stack. The logic is the same as approach 2, only the bookkeeping is simpler, and it extends to any number of bracket kinds without a growing matches method.
A cheap early exit comes for free: brackets pair up, so an odd-length string can never be balanced.
Algorithm.
- If
s.length()is odd, returnfalse. - For each character: if it is an opener, push its partner closer. Otherwise, if the stack is empty or its top differs from the character, return
false; else pop. - Return whether the stack is empty.
static boolean optimalExpectedCloser(String s) {
if (s.length() % 2 != 0) return false; // an odd number of brackets can never pair up
Deque<Character> expected = new ArrayDeque<>();
for (char c : s.toCharArray()) {
switch (c) {
case '(' -> expected.push(')');
case '[' -> expected.push(']');
case '{' -> expected.push('}');
default -> {
if (expected.isEmpty() || expected.pop() != c) return false;
}
}
}
return expected.isEmpty();
}
Complexity. Time O(n), space O(n), as in approach 2, with one fewer method call per closer.
Edge cases and pitfalls
- Empty string. Balanced by definition; all three methods return
truewithout special-casing it (the loop simply does not run and the stack is empty). - Closer first.
")("has one of each bracket, so a counting approach would accept it. The stack rejects it immediately because)arrives with nothing to close. Counting only works for a single bracket type. - Unclosed openers.
"(("never fails inside the loop. The finalstack.isEmpty()check is what rejects it; forgetting that check is the most common bug. - Wrong kind of closer.
"(]"and the interleaved"([)]"are rejected because the top of the stack is compared by kind, not just by presence. - Odd length. Can be rejected up front, but it is only an optimisation; the stack logic handles it anyway.
Characterversuschar. WithDeque<Character>,expected.pop() != cunboxes to acharcomparison, which is what we want. Comparing twoCharacterobjects with!=would compare references and can fail for values outside the cached range; here every bracket is a cached ASCII value, but writing!=between an unboxedcharand aCharacterkeeps it safe regardless.- Other characters. The problem guarantees only brackets. If letters could appear, skip them in the loop instead of treating them as closers.
Related problems
- Remove Outermost Parentheses uses the same depth counter idea on a single bracket type.
- Maximum Nesting Depth of the Parentheses tracks the stack height instead of its contents.
- Generate Parentheses constructs all balanced strings of a given length.
- Infix to Postfix Conversion uses a stack of operators with the same “pop until the matching opener” step.
- Implement Stack using Arrays shows how the stack used here works inside.