Balanced Parentheses Problem Explained Using Stacks — Step by Step
Every compiler, code editor, and browser in the world checks whether your brackets match up before it does anything else. When VS Code underlines a missing closing brace in red, or when Java throws a compilation error about unexpected tokens, it's running a version of the balanced parentheses check under the hood. This is not a toy problem — it's the gatekeeper that runs before your code ever executes.
The problem itself is simple to state: given a string of brackets like '({[]})' or '([)]', decide whether every opening bracket has a matching closing bracket in the right order. The tricky part is 'in the right order' — because '([)]' has the same number of openers and closers, yet it's wrong. Order matters, and that's exactly what a stack was designed to track.
By the end of this article you'll understand what a stack is and why it's the perfect tool here, you'll be able to write a complete Java solution from memory, you'll know the two mistakes that trip up nearly every beginner, and you'll be ready to answer the three versions of this question that interviewers love to ask.
What Is a Stack and Why Does It Fit This Problem Perfectly?
A stack is a collection with one rule: the last thing you put in is the first thing you get out. Programmers call this LIFO — Last In, First Out. Think of a stack of plates at a buffet. You add a clean plate to the top, and when someone takes a plate, they always grab from the top. Nobody digs from the bottom.
A stack supports three core operations. Push adds an item to the top. Pop removes and returns the item from the top. Peek looks at the top item without removing it. That's it. Simple, but powerful.
Now think about why this fits bracket matching perfectly. When you see an opening bracket like '(' or '{' or '[', you don't know yet what will close it — so you push it onto the stack and move on. When you later see a closing bracket like ')' or '}' or ']', the most recently opened bracket — sitting right on top of the stack — is the one that should match it. If it does match, pop it off and keep going. If it doesn't match, or the stack is empty when you expected something there, the string is unbalanced.
The stack's LIFO nature mirrors the natural nesting of brackets. The most recently opened bracket must be the first one closed. That's not a coincidence — stacks exist precisely to handle this kind of nested, ordered structure.
import java.util.Stack; public class StackDemo { public static void main(String[] args) { // Java's built-in Stack class — we'll use this throughout the article Stack<Character> bracketStack = new Stack<>(); // PUSH: add items to the top of the stack bracketStack.push('('); // stack is now: ['('] bracketStack.push('['); // stack is now: ['(', '['] bracketStack.push('{'); // stack is now: ['(', '[', '{'] System.out.println("Stack after pushing three brackets: " + bracketStack); System.out.println("Top of stack (peek): " + bracketStack.peek()); // looks without removing // POP: remove and return the top item char topBracket = bracketStack.pop(); // removes '{' — the last thing we pushed System.out.println("Popped: " + topBracket); // Output: { System.out.println("Stack after one pop: " + bracketStack); // ['(', '['] // isEmpty: crucial check before popping to avoid EmptyStackException System.out.println("Is stack empty? " + bracketStack.isEmpty()); // false } }
Top of stack (peek): {
Popped: {
Stack after one pop: [(, []
Is stack empty? false
The Algorithm: Walking Through the Logic Step by Step
Here's the core insight stated plainly: scan the string character by character. If the character is an opening bracket ('(', '[', or '{'), push it onto the stack. If it's a closing bracket (')', ']', or '}'), check whether the top of the stack holds the matching opener. If yes, pop and continue. If no — or if the stack is empty — the string is unbalanced, stop and return false.
When you finish scanning every character, check whether the stack is empty. An empty stack means every opener found its closer. A non-empty stack means there are openers left hanging with no closers — also unbalanced.
Let's trace through '({[]})' manually before writing any code. - Read '(' → push. Stack: ['('] - Read '{' → push. Stack: ['(', '{'] - Read '[' → push. Stack: ['(', '{', '['] - Read ']' → top is '[', which matches ']'. Pop. Stack: ['(', '{'] - Read '}' → top is '{', which matches '}'. Pop. Stack: ['('] - Read ')' → top is '(', which matches ')'. Pop. Stack: [] - End of string, stack is empty → BALANCED.
Now trace '([)]': - Read '(' → push. Stack: ['('] - Read '[' → push. Stack: ['(', '['] - Read ')' → top is '[', which does NOT match ')'. → UNBALANCED. Stop immediately.
The mismatch detection happens in the middle of the string. That's what makes this algorithm elegant — it fails fast.
import java.util.Stack; public class BalancedParenthesesChecker { /** * Returns true if every opening bracket in the input string * has a correctly ordered, matching closing bracket. */ public static boolean isBalanced(String inputString) { // Our stack holds opening brackets as we encounter them Stack<Character> openingBrackets = new Stack<>(); // Walk through every character in the string one at a time for (int i = 0; i < inputString.length(); i++) { char currentChar = inputString.charAt(i); // --- CASE 1: It's an opening bracket → push and move on --- if (currentChar == '(' || currentChar == '[' || currentChar == '{') { openingBrackets.push(currentChar); // --- CASE 2: It's a closing bracket → check if it matches the top --- } else if (currentChar == ')' || currentChar == ']' || currentChar == '}') { // If the stack is empty, there's no opener to match this closer if (openingBrackets.isEmpty()) { return false; // e.g. input starts with ')' — immediately wrong } // Look at the most recently pushed opener char mostRecentOpener = openingBrackets.pop(); // Check whether the opener and closer are a valid pair boolean isMatchingPair = (mostRecentOpener == '(' && currentChar == ')') || (mostRecentOpener == '[' && currentChar == ']') || (mostRecentOpener == '{' && currentChar == '}'); if (!isMatchingPair) { return false; // e.g. '([)]' — opener and closer don't match } // If they matched, the pop already removed the opener — carry on } // Any other character (letters, digits, spaces) is ignored } // After scanning everything, the stack must be empty. // If it's not, some openers never found their closers. return openingBrackets.isEmpty(); } public static void main(String[] args) { String[] testCases = { "({[]})", // all three bracket types, properly nested "(())", // nested parentheses "([)]", // wrong order — should fail "(((", // openers with no closers — should fail ")))", // closers with no openers — should fail "", // empty string — technically balanced "{[()()]}", // complex valid nesting "({)}", // looks close but order is wrong }; for (String testCase : testCases) { String result = isBalanced(testCase) ? "BALANCED" : "UNBALANCED"; // Padding the test case for clean console output System.out.printf("%-15s → %s%n", testCase.isEmpty() ? "(empty)" : testCase, result); } } }
(()) → BALANCED
([)] → UNBALANCED
((( → UNBALANCED
))) → UNBALANCED
(empty) → BALANCED
{[()()]} → BALANCED
({)} → UNBALANCED
Common Gotchas That Trip Up Nearly Every Beginner
Even after understanding the algorithm, there are two specific bugs that show up in beginner solutions so frequently they're almost a rite of passage. Let's look at them directly, see the broken code, and then see the fix.
The first gotcha is forgetting the final isEmpty() check. Many beginners return true as soon as the loop finishes without errors, not realising that unmatched openers sitting in the stack also make the string invalid.
The second gotcha is popping without checking whether the stack is empty first. If your input string starts with a closing bracket, the stack is empty when you try to pop — and Java throws an EmptyStackException that crashes your program instead of gracefully returning false.
There's also a subtler third mistake: using == to compare String objects instead of char values. This only applies if you accidentally store brackets as Strings rather than chars, but it produces silent wrong answers — one of the nastiest kinds of bugs.
The runnable examples below show each broken scenario and the one-line fix for each.
import java.util.Stack; public class CommonMistakesDemo { // ❌ BUGGY VERSION 1: Missing the final isEmpty() check public static boolean buggyNoEmptyCheck(String input) { Stack<Character> stack = new Stack<>(); for (char ch : input.toCharArray()) { if (ch == '(' || ch == '[' || ch == '{') { stack.push(ch); } else if (ch == ')' || ch == ']' || ch == '}') { if (stack.isEmpty()) return false; stack.pop(); // simplified — ignoring mismatch for this demo } } return true; // ❌ BUG: returns true even when '(((' is the input! // Fix: return stack.isEmpty(); } // ❌ BUGGY VERSION 2: Popping without checking isEmpty first public static boolean buggyNoEmptyGuard(String input) { Stack<Character> stack = new Stack<>(); for (char ch : input.toCharArray()) { if (ch == '(' || ch == '[' || ch == '{') { stack.push(ch); } else if (ch == ')' || ch == ']' || ch == '}') { char top = stack.pop(); // ❌ BUG: crashes with EmptyStackException if input starts with ')' // Fix: if (stack.isEmpty()) return false; ← add this line BEFORE calling pop() } } return stack.isEmpty(); } public static void main(String[] args) { // Demonstrate Bug 1: '(((' has 3 openers and no closers System.out.println("Bug 1 — '(((' should be UNBALANCED:"); System.out.println("Buggy result: " + buggyNoEmptyCheck("(((")); // prints true — WRONG // Demonstrate Bug 2: input starts with ')' System.out.println("\nBug 2 — ')))' should be UNBALANCED:"); try { System.out.println("Buggy result: " + buggyNoEmptyGuard(")))")); } catch (java.util.EmptyStackException e) { System.out.println("CRASHED with EmptyStackException! Fix: check isEmpty() before pop()"); } // Show the correct version handles both gracefully System.out.println("\nCorrect isBalanced('((('): " + BalancedParenthesesChecker.isBalanced("(((")); System.out.println("Correct isBalanced('))): " + BalancedParenthesesChecker.isBalanced(")))")); } }
Buggy result: true
Bug 2 — ')))' should be UNBALANCED:
CRASHED with EmptyStackException! Fix: check isEmpty() before pop()
Correct isBalanced('((('): false
Correct isBalanced(')))'): false
Interview Prep: Variations Interviewers Add to the Core Problem
Once you've nailed the base algorithm, interviewers don't stop there. They mutate the problem to see if you truly understand it or just memorised the solution. Here are the three most common extensions, with the key insight for each.
Variation 1: 'What if the string contains non-bracket characters like letters and numbers?' The answer is: ignore them. Your loop only acts when a character is a bracket. Everything else passes through. The code already handles this — add any character to a test string and verify it yourself.
Variation 2: 'Can you solve this without a stack?' Yes, but only if the input uses a single type of bracket — say, only parentheses. In that case you can use a counter: increment for '(', decrement for ')'. If counter goes negative, return false. If counter is zero at the end, return true. This breaks immediately with multiple bracket types because you lose ordering information.
Variation 3: 'What's the minimum number of bracket additions or removals to make a string balanced?' This is a harder follow-up (LeetCode 921). The trick is to track two counters — open (unmatched openers) and close (unmatched closers) — and return their sum at the end. Knowing this follow-up exists and describing the approach impresses interviewers even if you don't code it live.
The code below implements the counter shortcut for single-bracket strings, so you can see where it works — and understand exactly why it breaks with mixed brackets.
public class SingleBracketCounterApproach { /** * Counter-only approach — ONLY works correctly for a single bracket type. * Fails silently for mixed brackets like '([)]'. * Use this ONLY when you're certain the input has one bracket type. */ public static boolean isBalancedSingleType(String input) { int openCount = 0; // tracks how many unmatched '(' we've seen so far for (char ch : input.toCharArray()) { if (ch == '(') { openCount++; // found an opener — remember it } else if (ch == ')') { openCount--; // found a closer — cancel one opener if (openCount < 0) { return false; // more closers than openers so far — can't recover } } } // openCount == 0 means all openers were matched return openCount == 0; } public static void main(String[] args) { // Works correctly for parentheses-only strings System.out.println("'(())' → " + isBalancedSingleType("(())")); // true System.out.println("'())(' → " + isBalancedSingleType("())(" )); // false System.out.println("'(((' → " + isBalancedSingleType("(((")); // false // ❌ Where the counter approach SILENTLY gives wrong answers // '([)]' has 2 openers and 2 closers, but they're in wrong order // Counter sees: +1, +1(ignored [), -1, -1(ignored ]) = 0 → reports BALANCED // But it's actually UNBALANCED — use the stack solution for mixed brackets! System.out.println("\n'([)]' counter result (WRONG): " + isBalancedSingleType("([)]")); System.out.println("'([)]' stack result (CORRECT): " + BalancedParenthesesChecker.isBalanced("([)]")); } }
'())(' → false
'(((' → false
'([)]' counter result (WRONG): true
'([)]' stack result (CORRECT): false
| Aspect | Stack Approach (General) | Counter Approach (Single Bracket Only) |
|---|---|---|
| Supports multiple bracket types | Yes — handles '(', '[', '{' together | No — gives wrong answers silently |
| Detects wrong ordering like '([)]' | Yes — catches it immediately | No — counts match so it returns true incorrectly |
| Time Complexity | O(n) — one pass through the string | O(n) — one pass through the string |
| Space Complexity | O(n) — worst case all chars on stack | O(1) — just one integer counter |
| Handles empty string | Yes — returns true correctly | Yes — returns true correctly |
| Fails fast on first mismatch | Yes — returns false immediately | Yes — returns false when count goes negative |
| Interview recommended? | Always — it is the correct general solution | Only mention as a follow-up optimization for limited inputs |
🎯 Key Takeaways
- A stack's LIFO property is not accidental here — it directly mirrors bracket nesting, where the most recently opened bracket must always be the first one closed.
- There are exactly two valid reasons to return false mid-loop: the stack is empty when you see a closing bracket, or the top of the stack doesn't match the closing bracket you just read.
- The final isEmpty() check is not optional — a string full of unclosed openers like '(((' will pass the entire loop without error and needs that last check to be caught.
- The counter shortcut (O(1) space) only works for single-bracket inputs — it silently gives wrong answers for mixed brackets because it loses all ordering information.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Forgetting the final isEmpty() check — Your loop finishes without errors but you return true unconditionally, so '(((' gets reported as BALANCED — Fix: always return stack.isEmpty() instead of return true at the end of the method.
- ✕Mistake 2: Calling pop() without first checking isEmpty() — Input like ')))' has a closing bracket before any opener, so the stack is empty when you try to pop, causing Java to throw an EmptyStackException and crash the program — Fix: always guard with if (stack.isEmpty()) return false; immediately before every pop() call.
- ✕Mistake 3: Using the counter trick for multi-bracket strings — The string '([)]' has equal openers and closers so the counter reaches zero and reports BALANCED, but it's actually wrong because the order is invalid — Fix: use the stack approach whenever your input can contain more than one type of bracket, and reserve the counter only for parentheses-only problems.
Interview Questions on This Topic
- QGiven the string '{[()]}', walk me through your algorithm step by step — what's on the stack after each character?
- QWhat are the time and space complexities of your solution, and is there any way to reduce the space complexity?
- QHow would you modify your solution to also return the index of the first unmatched bracket, rather than just true or false?
Frequently Asked Questions
What is the balanced parentheses problem in data structures?
The balanced parentheses problem asks you to determine whether every opening bracket in a string has a corresponding closing bracket in the correct order. A string like '({[]})' is balanced, while '([)]' is not — even though it has equal numbers of openers and closers, the order is wrong. A stack is the standard data structure used to solve it.
Why do we use a stack to solve the balanced parentheses problem?
A stack follows Last In, First Out order, which perfectly matches how brackets nest. When you open a bracket, you don't know what closes it yet, so you push it and wait. The moment you see a closing bracket, the most recently opened bracket — sitting on top of the stack — is the one that must match. No other data structure captures this nested ordering as naturally.
Is an empty string considered balanced?
Yes. An empty string contains zero opening brackets and zero closing brackets, so every opener (there are none) has a matching closer. The algorithm handles this correctly by default: the loop body never executes, and the final isEmpty() check on an empty stack returns true.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.