Matching Brackets with a Stack
Which bracket does this one close?
Put the cursor next to a bracket and your editor lights up its partner. Leave one out and the compiler stops at "expected )". Both answer the same question: which open bracket does this closing bracket close? Always the most recent one that's still open. In {[()]} the ) closes the ( right before it. The ] closes the [, because the ( inside it is already closed, and the } closes the first bracket, last.
The first idea most people have needs no stack at all: delete every adjacent pair, (), [] or {}, and repeat until nothing changes. A valid string disappears completely; ([)] gets stuck, since no pair in it sits side by side:
def is_valid(s):
while True: # delete every adjacent pair, again and again
t = s.replace("()", "").replace("[]", "").replace("{}", "")
if t == s: # nothing left to delete
return s == ""
s = t
It's correct, and it's slow. Each pass rescans the whole string, and a deeply nested string loses only its innermost pair per pass, so n/2 passes of n characters: O(n²). On our judge, with nested strings:
| Length | Deleting pairs | The stack |
|---|---|---|
| 5,000 | 39 ms | 0.5 ms |
| 10,000 | 155 ms | 1.1 ms |
| 20,000 | 616 ms | 2.2 ms |
| 40,000 | 2,462 ms | 4.4 ms |
Each doubling made the deleting version about four times slower. The stack read a million brackets in 105 ms.
The fast way comes straight from the question. A closer closes the most recent bracket that's still open, and the most recent thing still waiting is exactly what Introduction to Stacks said a stack gives back.
![The string {[()]} with an arc joining each pair, the arcs nested inside each other; ( to ) is solid, the outer two dashed, still to come. The ) is being read, and ] and } aren't read yet. Below, a tray holds the brackets still open, {, [ and (, the bottom on the left and ( on top, and an arrow runs from ) down to it. A focal line reads: the most recent bracket still open is always the top.](https://pub-a1b3030acfb94e84ba8a89fb182c53bc.r2.dev/public/dsa-content-122bb259-fc77-445a-bdd6-1dfbedd70b8b/most-recent-open-bracket.webp)
Why counting isn't enough
With one kind of bracket, a counter is all you need: add one for (, take one away for ), fail if it drops below zero, pass if it ends at zero. With three kinds, it's tempting to keep one counter, or one per kind. We ran both against the stack, on strings built to fool them. Bold marks a wrong answer:
| String | One counter | A counter per kind | The stack |
|---|---|---|---|
(] | valid | invalid | invalid |
([)] | valid | valid | invalid |
[(]) | valid | valid | invalid |
{[()]} | valid | valid | valid |
Counts can see how many brackets are open, but not in what order. ([)] has one of each kind, every count ends at zero and never drops below it, yet the ) arrives while [ is the most recent bracket open: the pairs cross instead of nesting.
![Two strings side by side. ([]) has its arcs nested, one inside the other, and is valid. ([)] has its arcs crossing, and is invalid. Under both, the same counts: one of each bracket, all back to zero. A focal line reads: a counter sees how many; a stack sees the order.](https://pub-a1b3030acfb94e84ba8a89fb182c53bc.r2.dev/public/dsa-content-122bb259-fc77-445a-bdd6-1dfbedd70b8b/counts-cant-see-order.webp)
That's also why a mismatch is final. At every moment the stack holds exactly the brackets still open, in the order they opened. A closer can only close the top: closing anything deeper would leave the top's bracket open inside a pair that's already closed. So once ) meets [ on top, nothing later in the string can make it valid, and the check can stop right there.
The template: push openers, pop on closers
Push every opener, and let each closer take the top. The stack always holds the brackets still open, the most recent on top:
Reading {[()]} — The stack holds the brackets still open, each with its position. Green pairs have matched.
Valid Parentheses in full. Read left to right. Push every opener. For a closer, the stack must not be empty and its top must be the matching opener; then pop it. At the end the stack must be empty, or something was never closed. A small map from each closer to its opener keeps the check to one line:
bool isValid(const string& s) {
unordered_map<char, char> opener = {{')', '('}, {']', '['}, {'}', '{'}};
stack<char> open; // the brackets still open, the most recent on top
for (char c : s) {
if (!opener.count(c)) { // an opener waits for its closer
open.push(c);
} else {
if (open.empty() || open.top() != opener[c]) return false; // nothing open, or the wrong kind
open.pop();
}
}
return open.empty(); // anything left was never closed
}
def is_valid(s):
opener = {")": "(", "]": "[", "}": "{"}
stack = [] # the brackets still open, the most recent on top
for c in s:
if c not in opener: # an opener waits for its closer
stack.append(c)
else:
if not stack or stack[-1] != opener[c]:
return False # nothing open, or the wrong kind
stack.pop()
return not stack # anything left was never closed
static boolean isValid(String s) {
Map<Character, Character> opener = Map.of(')', '(', ']', '[', '}', '{');
Deque<Character> open = new ArrayDeque<>(); // the brackets still open, the most recent on top
for (char c : s.toCharArray()) {
if (!opener.containsKey(c)) { // an opener waits for its closer
open.push(c);
} else {
char want = opener.get(c);
if (open.isEmpty() || open.peek() != want) return false; // nothing open, or the wrong kind
open.pop();
}
}
return open.isEmpty(); // anything left was never closed
}
function isValid(s) {
const opener = { ")": "(", "]": "[", "}": "{" };
const open = []; // the brackets still open, the most recent on top
for (const c of s) {
if (!(c in opener)) { // an opener waits for its closer
open.push(c);
} else {
if (open.length === 0 || open[open.length - 1] !== opener[c]) return false; // nothing open, or the wrong kind
open.pop();
}
}
return open.length === 0; // anything left was never closed
}
Each printed true true false false true false false for (), ()[]{}, (], ([)], {[]}, ((, ).
Complexity. Each character is pushed at most once and popped at most once, so the whole pass is O(n). The stack can hold every character when they're all openers, so the space is O(n) too.
Here's the crossing from the table, stopped at the moment it breaks:
Reading ([)] — The closer meets the wrong kind on top, and the check stops there.
Now you be the stack
Below, you read the string one bracket at a time and make every call: push an opener, pop when a closer matches, or call the string invalid. A wrong call is refused with the reason, so you can't skip ahead of the rules. Try the presets that fool counters, then type your own.

Keep positions, and the stack says what to remove
Minimum Remove to Make Valid Parentheses gives you letters and parentheses, and asks for the fewest removals that leave the string valid. The stack still does the matching. Just push each ('s position instead of the character, so you know which ones to remove at the end.
Two kinds of bracket get removed. A ) that finds nothing open is marked on the spot. A ( can only be judged at the end: whatever's still on the stack never closed.
Why that's the fewest. Each time a ) finds the stack empty, the text up to it has one ) more than any valid answer can keep, so some ) up to there must go, and this one will do. Each ( left at the end is one more than the text after it can close. The stack removes exactly one bracket per extra, so nothing can remove fewer. (Other answers may remove different brackets, like either ) in ()), but never fewer.)
Removing from (a))b( — The stack holds positions. Red brackets are the ones removed.
string minRemoveToMakeValid(const string& s) {
stack<int> open; // positions of the '(' still open
vector<bool> drop(s.size(), false); // the brackets to remove
for (int i = 0; i < (int)s.size(); i++) {
if (s[i] == '(') {
open.push(i);
} else if (s[i] == ')') {
if (!open.empty()) open.pop(); // it closes the most recent open '('
else drop[i] = true; // nothing open: one ')' too many
}
}
while (!open.empty()) { // still open at the end: one '(' too many each
drop[open.top()] = true;
open.pop();
}
string out;
for (int i = 0; i < (int)s.size(); i++)
if (!drop[i]) out += s[i];
return out;
}
def min_remove_to_make_valid(s):
stack = [] # positions of the '(' still open
drop = set() # the brackets to remove
for i, c in enumerate(s):
if c == "(":
stack.append(i)
elif c == ")":
if stack:
stack.pop() # it closes the most recent open '('
else:
drop.add(i) # nothing open: one ')' too many
drop.update(stack) # still open at the end: one '(' too many each
return "".join(c for i, c in enumerate(s) if i not in drop)
static String minRemoveToMakeValid(String s) {
Deque<Integer> open = new ArrayDeque<>(); // positions of the '(' still open
boolean[] drop = new boolean[s.length()]; // the brackets to remove
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') {
open.push(i);
} else if (c == ')') {
if (!open.isEmpty()) open.pop(); // it closes the most recent open '('
else drop[i] = true; // nothing open: one ')' too many
}
}
for (int i : open) drop[i] = true; // still open at the end: one '(' too many each
StringBuilder out = new StringBuilder();
for (int i = 0; i < s.length(); i++)
if (!drop[i]) out.append(s.charAt(i));
return out.toString();
}
function minRemoveToMakeValid(s) {
const open = []; // positions of the '(' still open
const drop = new Set(); // the brackets to remove
for (let i = 0; i < s.length; i++) {
if (s[i] === "(") {
open.push(i);
} else if (s[i] === ")") {
if (open.length > 0) open.pop(); // it closes the most recent open '('
else drop.add(i); // nothing open: one ')' too many
}
}
for (const i of open) drop.add(i); // still open at the end: one '(' too many each
let out = "";
for (let i = 0; i < s.length; i++)
if (!drop.has(i)) out += s[i];
return out;
}
Each printed "lee(t(c)o)de" "ab(c)d" "" "(a)b" for lee(t(c)o)de), a)b(c)d, ))((, (a))b(: the first three are LeetCode's examples. ))(( loses everything, since no bracket in it has a partner.

Now pick the removals yourself. Tap the brackets you'd remove, then check: the result has to be valid, with as few removals as the stack's answer.

The same stack, other problems
The stack in bracket matching holds whatever is still unresolved: openers waiting for a closer. Swap what "unresolved" means and the same loop solves other problems.
Simplify Path turns a Unix path like /home//user/../docs/. into its shortest form. The stack holds the directory names so far: a name pushes, .. pops (at the root it does nothing), and . or an empty part between two slashes changes nothing. ... is just a name.
def simplify_path(path):
stack = [] # the directory names so far
for part in path.split("/"):
if part == "..":
if stack:
stack.pop() # up one level; at the root, stay put
elif part and part != ".":
stack.append(part) # a name, even "..."
return "/" + "/".join(stack)
It returned /home, /home/foo, /home/user/Pictures, /, /.../b/d for /home/, /home//foo/, /home/user/Documents/../Pictures, /../, /.../a/../b/c/../d/./.
Asteroid Collision has asteroids moving right (positive) or left (negative) along a line; when two meet, the smaller explodes, and equal ones both explode. The stack holds the survivors so far. A left-mover only meets right-movers on top of the stack, and it keeps going until something stops it, so the check is a loop, not one comparison:
def asteroid_collision(asteroids):
stack = [] # the survivors so far, left to right
for a in asteroids:
alive = True
while alive and a < 0 and stack and stack[-1] > 0:
if stack[-1] < -a:
stack.pop() # the right-mover on top is smaller: it explodes, keep going
elif stack[-1] == -a:
stack.pop() # the same size: both explode
alive = False
else:
alive = False # the right-mover is bigger: a explodes
if alive:
stack.append(a)
return stack
It returned [5, 10], [], [10], [-6, 2, 4], [-2, -1, 1, 2] for [5, 10, -5], [8, -8], [10, 2, -5], [3, 5, -6, 2, -1, 4], [-2, -1, 1, 2]. In the last one the left-movers come first, so nothing ever meets.
The Celebrity Problem asks for the person everyone knows who knows no one, from a matrix where mat[a][b] is 1 when a knows b. The stack holds the candidates. Pop two and look up one cell: if a knows b, a can't be the celebrity; if not, b can't be, since everyone knows the celebrity. Either way one candidate goes, so n − 1 lookups leave one. That one still has to be checked, because ruling everyone else out doesn't prove it's a celebrity:
def find_celebrity(mat):
n = len(mat)
stack = list(range(n)) # everyone starts as a candidate
while len(stack) > 1:
a, b = stack.pop(), stack.pop()
if mat[a][b]:
stack.append(b) # a knows b, so a isn't the celebrity
else:
stack.append(a) # a doesn't know b, so b isn't the celebrity
c = stack[0]
for i in range(n): # the last one standing still has to be checked
if i != c and (mat[c][i] or not mat[i][c]):
return -1
return c
It returned 1, -1 and 0 for our three examples. With 1,000 people and a celebrity, it found person 637 after 2,997 lookups; checking every pair would take 999,000. With no celebrity, the survivor failed the check after 1,000 lookups.
Spotting it, and when it breaks
The clues. Things that nest: brackets, HTML tags, begin and end, folders in a path. The words valid, balanced, well-formed or matching. And anything that waits until something later resolves it: an asteroid waiting for a collision, a candidate waiting to be ruled out.
The near-misses. One kind of bracket needs only a counter: Minimum Add to Make Parentheses Valid counts the extra ( and the extra ), with no stack. The longest valid stretch (Longest Valid Parentheses) keeps positions on the stack with a base, or uses dynamic programming. Making every valid string is backtracking (Recursion & Backtracking).
When it breaks. Removing the fewest brackets works with one kind of bracket only. With several kinds, a stack that drops every closer that doesn't match isn't the fewest: we tried every possible set of removals on ([), and the fewest is 1 (the [), while that stack drops 3. The fewest removals with several kinds is a harder problem, solved with dynamic programming over intervals. And a recursive parser runs out of call stack on deep enough nesting, as Reversing a Linked List measured; an explicit stack doesn't.
Bugs that cost a submission
Popping without checking. A closer at the very start finds nothing to pop:
def is_valid(s):
opener = {")": "(", "]": "[", "}": "{"}
stack = []
for c in s:
if c not in opener:
stack.append(c)
elif stack.pop() != opener[c]: # pops even when nothing is open
return False
return not stack
On ) it crashed with IndexError: pop from empty list. Check that the stack isn't empty before you pop.
Forgetting the end. Every closer matched, so it returns true:
def is_valid(s):
opener = {")": "(", "]": "[", "}": "{"}
stack = []
for c in s:
if c not in opener:
stack.append(c)
else:
if not stack or stack[-1] != opener[c]:
return False
stack.pop()
return True # forgets what's still open
It said True for (( and True for ({. No closer ever arrived to catch them; only the final check that the stack is empty can.
Matching the wrong kind. It checks that something is open, not what:
def is_valid(s):
stack = []
for c in s:
if c in "([{":
stack.append(c)
elif not stack:
return False
else:
stack.pop() # any opener will do: the kind is never checked
return not stack
It said True for (] and True for ([)], the same mistake as one counter. Check that the top is the matching opener.
Removing from the wrong end. A counter finds how many ( are extra, but not which ones:
def min_remove_to_make_valid(s):
kept, open_ = [], 0
for c in s:
if c == ")" and open_ == 0:
continue # a ')' with nothing open: drop it
open_ += 1 if c == "(" else -1 if c == ")" else 0
kept.append(c)
out = []
for c in kept: # drop the extra '(' ... counting from the left
if c == "(" and open_ > 0:
open_ -= 1
continue
out.append(c)
return "".join(out)
On (()( it returned )(, which isn't valid. The extra ( are the ones still open at the end, the last ones; the first ( here had a partner. Keep positions on the stack, or count again from the right.
Check yourself
Quiz
1 / 6Multiple choice
{[(, then a ). What's on the stack now?Multiple choice
Multiple choice
(( runs through the template. Which check rejects it?Multiple choice
())(?Multiple choice
[10, 2, -5]: what's left after every collision?Multiple choice
Practice problems
From easier to harder:
- Valid Parentheses: the template.
- Simplify Path: a stack of directory names.
- Minimum Remove to Make Valid Parentheses: positions on the stack, removed at the end.
- Asteroid Collision: a stack of survivors, compared in a loop.
- The Celebrity Problem: a stack of candidates, and a final check.
Next, Evaluating Expressions with Stacks: operands and operators wait on stacks until it's their turn.