DSA sheet · Stack · Pattern 4: parentheses & scoring

Valid Parentheses

This video opens the fourth stack pattern of the sheet: parentheses and scoring. The teacher calls it her favourite pattern, because once the one idea clicks ("every closing bracket must pair with the nearest unmatched opening bracket"), the whole family of problems becomes easy. She first solves it with a brute force that deletes matched pairs from the string, works out why it is slow, and then replaces the deleting with a stack. Every later parentheses problem (min add, score, longest valid, min remove) builds on this page.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a stack?

A stack is a pile of plates. You can only put a new plate on top, and you can only take the plate that is on top. The plate you put last is the first one you take out. This rule is called LIFO: Last In, First Out.

operationmeaningPython (a list used as a stack)cost
pushput an item on topstack.append(x)O(1)
popremove the top item and give it backstack.pop()O(1)
peek / toplook at the top item without removing itstack[-1]O(1)
is empty?is there nothing in the pile?not stack (True when empty)O(1)
a Python list is all we need
stack = []            # empty pile
stack.append('(')     # push  -> ['(']
stack.append('[')     # push  -> ['(', '[']   (right end = top)
print(stack[-1])      # peek  -> '['
x = stack.pop()       # pop   -> x = '[', stack = ['(']
if stack:             # ALWAYS check before pop / peek
    print(stack[-1])  # '('
Golden safety ruleCalling stack.pop() or stack[-1] on an empty list crashes with IndexError. So before every pop or peek, ask "is the stack empty?" In this problem that check is not only about safety: an empty stack at the wrong moment is itself the answer (False).

In all drawings on this page the stack is written left = bottom, right = top. In the vertical pictures, the top item is red.

The parentheses pattern

A bracket string is "balanced" when every opening bracket gets its own closing bracket, of the same kind ("family"), and in the right place. "Right place" means: when a closer arrives, it must close the most recent opener that is still waiting. The most recent unfinished thing is exactly what a stack keeps on top. So:

Counter trick (space optimisation), for later pages: when there is only one kind of bracket, e.g. only ( and ), every waiting opener looks the same. Then we don't need to remember which openers are waiting, only how many. A single integer open_count replaces the whole stack and space drops from O(n) to O(1). That trick does not work in this problem, because here there are three families and we must know the kind of the top opener. You will see the counter version in Problems 15, 16 and 17.

Part A · Brute force: delete matching neighbours

LeetCode 20

1The question in simple words

You get a string s made only of the six characters ( ) [ ] { }. Return True if it is valid, otherwise False. Valid means:

The teacher's opening example shows why position matters, not just counts:

first([])valid: the ] closes the nearest opener [, then ) closes (
second([)]invalid: same brackets, but ) arrives while [ is the nearest opener

Both strings have one round pair and one square pair. Only the order differs. So we cannot just count; whenever a closer appears it must match the nearest still-open bracket (the "top" one). That is already a hint towards a stack.

2What the constraints tell us

3Intuition: cancel the innermost pairs first

Look for an opener that is immediately followed by its own closer, like () or []. Such a pair is certainly correct, and nothing else can sit between them. So rub it out. After rubbing it out, two brackets that were apart may now be neighbours, and maybe they form a pair too. Keep rubbing out pairs. If the whole string disappears, it was valid. If something stays behind, some bracket had no partner.

In Python a string can't be changed in place (it is immutable), so we copy it into a list of characters that we can delete from. (In Java the teacher uses a StringBuilder for the same reason.)

4Building the logic from examples

We read two neighbours at a time: sb[i] and sb[i+1].

Check 1: an opener followed by a closer must be from the same family

bad pair(]opener then closer, but different families → this can never be fixed, so return False right away
good pair()same family → a real pair → delete both
Doubt: is returning False straight away safe, or should we keep scanning?
→ Safe. The closer ] sits right after (, so whatever happens later, the nearest opener for that ] is the (, which is the wrong family. The teacher says "return false then and there". Her final code just moves on (i += 1) instead, and the leftover characters make the answer False at the end anyway. Both give the same answer; the early return only saves time.

Example 1: ()[] → where should i go after a deletion?

i = 0()[]pair → delete
i = 0[]the list shrank and shifted left, so i = 0 already points at the next unchecked bracket → pair → delete
endemptynothing left → True

At first the teacher thinks "after deleting two characters, jump i forward by two". Then she corrects herself: after the deletion, everything on the right slides left by two, so i is already standing on the next unchecked character. If i is 0, we simply stay.

Example 2: (() → step back after a deletion when i > 0

i = 0(()two openers, no pair → i += 1 (keep moving while we see openers)
i = 1(()pair → delete. i was 1 (> 0), so there is something on the left that is still waiting → step back: i = 0
i = 0(no i+1 any more → loop ends. One bracket left → False
Doubt: why step back to i − 1 after deleting?
→ The deletion can create a new pair from the bracket on the left and the bracket that just slid in. Example: ([]). At i = 1 we delete [], and now ( (at 0) and ) are neighbours. If we don't step back to i = 0, we never compare them. We only step back when i > 0, because at i = 0 there is nothing on the left.

Example 3: ()) → a leftover closer

i = 0())pair → delete, stay at 0
i = 0)only one character → loop ends → leftover → False

The final rule

We only ever delete when sb[i] is an opener and sb[i+1] is its own closer. Otherwise we move right. At the end, empty list ⇒ valid, anything left ⇒ not valid. That one check covers all three examples: True for 1, False for 2 and 3.

5Approach steps

  1. Copy the string into a list sb. Set i = 0.
  2. While i < len(sb) − 1 (we need both sb[i] and sb[i+1]):
  3. If sb[i] is an opener and sb[i+1] is its matching closer → delete both. Then if i > 0, do i −= 1; otherwise stay.
  4. Otherwise → i += 1.
  5. After the loop, return len(sb) == 0.

6Code (Python)

Valid Parentheses, brute force (delete pairs)
class Solution:
    def isValid(self, s: str) -> bool:
        sb = list(s)                                  # strings are immutable, lists are not
        close_of = {'(': ')', '[': ']', '{': '}'}     # opener -> its closer
        i = 0
        while i < len(sb) - 1:                        # we read sb[i] and sb[i+1]
            if sb[i] in close_of and close_of[sb[i]] == sb[i + 1]:
                del sb[i:i + 2]                       # delete the pair (i and i+1)
                if i > 0:
                    i -= 1                            # a new pair may have formed on the left
            else:
                i += 1
        return len(sb) == 0

7Code line by line

linewhat it means
sb = list(s)A changeable copy. Python strings can't be edited in place.
close_of = {...}For each opener, which closer belongs to its family.
while i < len(sb) - 1:Stop at the second-last index, because we always read i+1 too. len(sb) is re-read every time, because the list keeps shrinking.
if sb[i] in close_of and close_of[sb[i]] == sb[i + 1]:"sb[i] is an opener, and the next one is exactly its closer."
del sb[i:i + 2]Remove indexes i and i+1 (the end index i+2 is not included). Everything to the right shifts left by 2. This shifting is the hidden O(n) cost.
if i > 0: i -= 1Go back one step so the bracket on the left can be compared with the bracket that slid in. If i is 0, stay (there's nothing on the left).
else: i += 1No pair here, move right.
return len(sb) == 0Everything paired up ⇒ True; any leftover ⇒ False.
Doubt (a fix): near the end of the video the teacher says "if i is 0, do i++" after a deletion. Is that right?
→ No, it contradicts her own walkthrough of Example 1, where she keeps i in place. With i += 1, the string ()() would become () with i = 1, the loop would end, and we would wrongly return False. The correct rule after a deletion is: step back if i > 0, otherwise stay. The i += 1 belongs only to the "no pair" branch. The code above does this, and the tests check ()().

8Dry run: ([)] and {[()]}

Run 1: ([)], the "wrong position" string from the start.

isb[i], sb[i+1]pair?actionsb after
0(, [no (next isn't a closer)i = 1( [ ) ]
1[, )no (different family)i = 2( [ ) ]
2), ]no (sb[i] isn't an opener)i = 3( [ ) ]
33 < 4 − 1 is false → loop ends. 4 characters left → False ✓

Run 2: {[()]}, deeply nested, shows the step-back working three times.

isb[i], sb[i+1]pair?actionsb after
0{, [noi = 1{ [ ( ) ] }
1[, (noi = 2{ [ ( ) ] }
2(, )yesdelete, i = 1{ [ ] }
1[, ]yesdelete, i = 0{ }
0{, }yesdelete, stay at 0(empty)
00 < −1 is false → loop ends. Empty → True ✓

9Complexity & remember

Remember the brute forceDelete any "opener immediately followed by its own closer". After deleting, step back one (if i > 0) because a new pair may have formed. Valid ⇔ nothing is left. O(n²) because deleting shifts the array.

Part B · Optimal: a stack of open brackets

1The question (same as Part A)

Same input, same output. We want to get rid of the costly deletions.

2What the constraints tell us

3Intuition: what was the brute force really doing?

Each time the brute force deleted a pair, it then went back one step to look at the bracket before. Going back to the latest unfinished thing is the "undo" feeling we met in the previous pattern (backspace, adjacent duplicates). Whenever we keep needing "the most recent unfinished item", the teacher's rule is: use a stack.

Instead of keeping openers inside the string and deleting them later, we save each opener on a stack the moment we see it. When a closer comes, its partner must be the nearest unmatched opener, which is exactly the stack's top. Match → pop. No array shifting at all.

The teacher also notes: the brute force looked at "opener at i, closer at i+1", while the stack looks at "closer now, opener before it". It is the same check, seen from the other side.

4Building the logic from examples

A long valid example: [((){})()]

Openers [ ( ( are pushed. The first ) sees ( on top → pop. { is pushed, } sees { → pop. The next ) sees the second ( → pop. Then ( push, ) pop. Finally ] sees [ → pop. The stack is empty at the end → valid. Closers are never pushed; they only remove things.

False case 1: top is the wrong family

stack([current char ) → top is [ → not its family → return False immediately

False case 2: something is left at the end, e.g. (()

end(we ran out of characters but one opener is still waiting → False

This is the stack version of "the string must end up empty" from Part A. So the final answer is stack is empty, not just True.

False case 3: a closer comes when the stack is empty, e.g. )(

i = 0empty) needs an opener before it, but there is none → return False right away
Doubt: why return immediately instead of finishing the loop?
→ Nothing later can fix a closer that came too early: openers that come later are on its right, and a closer can only close something on its left. So the answer is already decided. Returning early saves the rest of the work. (It also avoids popping an empty list, which would crash.)

How do we know a character is a closer, and which opener it wants?

You could write a few if/else branches. The teacher prefers a small map with the closer as the key and the opener as the value:

closer → opener
match = {')': '(', ']': '[', '}': '{'}
Doubt: why put the closer as the key, not the opener?
→ Because the moment we need the map is when we're holding a closer. With closers as keys, one lookup answers both questions: "is this a closer?" (ch in match) and "which opener should be on top?" (match[ch]). The map has only 3 entries, so it costs constant space.

5Approach steps

  1. Make an empty stack and the map closer → opener.
  2. For each character ch:
  3. If it is not a key of the map, it's an opener → push it.
  4. Otherwise it's a closer: if the stack is empty → return False.
  5. Pop the top. If it isn't match[ch] → return False.
  6. After the loop, return True only if the stack is empty.

6Code (Python)

Valid Parentheses, stack (optimal)
class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        match = {')': '(', ']': '[', '}': '{'}   # closer -> opener
        for ch in s:
            if ch not in match:                  # an opener
                stack.append(ch)
            else:                                # a closer
                if not stack:                    # nothing to close
                    return False
                if stack.pop() != match[ch]:     # wrong family on top
                    return False
        return not stack                         # leftover openers -> False

7Code line by line

linewhat it means
stack = []Will hold only openers that are still waiting for their partner.
match = {')': '(', ...}Closer as key, opener as value: lets us test "is it a closer?" and find "its opener" in one place.
if ch not in match: stack.append(ch)Not a closer ⇒ an opener ⇒ push. It waits for its closer.
if not stack: return FalseA closer with nobody waiting (like )(). Check this before popping, or pop() crashes.
if stack.pop() != match[ch]: return FalseRemove the nearest waiting opener. If it's from another family (like [ for )), the order is broken. Popping before comparing is fine, because if it doesn't match we return anyway.
return not stackTrue only if every opener found a partner. Leftovers (like (() ⇒ False.

8Dry run: [((){})()]

index0123456789
s[((){})()]
ichwhat we pop (and why)what we pushstack after (bottom → top)answer so far
0[—[ (opener)[still OK
1(—([ (still OK
2(—([ ( (still OK
3)(: top matches )—[ (still OK
4{—{[ ( {still OK
5}{: matches }—[ (still OK
6)(: matches—[still OK
7(—([ (still OK
8)(: matches—[still OK
9][: matches—(empty)loop ends
endstack is emptyTrue
after i = 2 (tallest)
[((
after i = 4
[({
after i = 9
(empty)

Quick runs of the three False cases:

swhat happensanswer
([)]push (, push [; ) pops [ ≠ (False (wrong family)
(()push, push, pop → stack ( left at the endFalse (leftover)
)() with an empty stackFalse (nothing to close)

9Complexity & remember

Remember Valid ParenthesesOpener → push. Closer → stack empty? False. Pop ≠ partner? False. End → valid only if the stack is empty. Map is closer → opener.

Teacher's advice: write it yourself from scratch (no copy-paste), and dry run it on your own strings with mixed brackets. That's how the pattern sticks.


Part C · Revision page

Brute force (delete pairs)Stack
where unmatched openers liveinside the string (a list)on the stack
what we do on a matchdelete sb[i], sb[i+1], step backpop the top
what "valid" means at the endlist is emptystack is empty
early Falseopener followed by a wrong-family closerempty stack on a closer, or wrong family on top
timeO(n²) (deletion shifts)O(n)
spaceO(n)O(n)
reason for Falseexamplecaught by
wrong family / wrong order([)], (]stack.pop() != match[ch]
closer before any opener)(, ]if not stack
opener never closed((), [return not stack
If you remember only 5 lines 1. A closer must close the nearest waiting opener → that's the stack top.
2. Push openers only; closers never go on the stack.
3. Closer + empty stack → False. Closer + wrong top → False.
4. At the end, valid ⇔ the stack is empty.
5. Brute force deletes pairs and is O(n²) because deleting shifts the array; the stack is O(n).
Mistakes to avoid ✗ returning True at the end without checking the stack is empty ((( would pass)
✗ popping or peeking without checking for empty first (crash on ))
✗ only counting brackets (counts can't see ([)] is wrong)
✗ in the brute force, moving i forward after a deletion (misses new pairs)
✗ map in the wrong direction (opener → closer makes the closer lookup awkward)
test it yourself (paste under either solution)
s = Solution()
print(s.isValid("()[]{}"))       # True
print(s.isValid("[((){})()]"))   # True
print(s.isValid("{[()]}"))       # True
print(s.isValid("([)]"))         # False  wrong order
print(s.isValid("(()"))          # False  leftover opener
print(s.isValid(")("))           # False  closer first
print(s.isValid("(]"))           # False  wrong family

Based on this video: Valid Parentheses | Stack pattern: parentheses & scoring