DSA sheet · Stack · Concept video

Stack Patterns Overview

This is the opening video of the stack topic. It answers two questions, what is a stack and why do we need one when we already have arrays, and then walks through the six patterns that the teacher says cover about 90–95% of the stack questions you'll meet in interviews and online tests. No single problem is solved fully here. Instead, every pattern gets its idea, a tiny worked example, and the reason a stack beats the plain-array approach. This page adds a ready-to-use Python template for each pattern, so you can come back here before solving any problem in the sheet.

This concept page follows its own order:
① what a stack is → ② why we need it / how to spot a stack problem → ③ each pattern: idea → tiny example → why not an array → Python template → ④ which sheet problems belong to which pattern → ⑤ remember

Part 0 · What a stack is

1A linear structure without indexing

Like an array, a stack is linear: items sit one after another in a line. The big difference: an array lets you jump to any position with an index (arr[2]). A stack has no indexing. You may only work at one end, called the top.

Three operations are allowed, all at the top:

operationwhat it doesPython (list as a stack)cost
pushadd an item on topst.append(x)O(1)
popremove the top itemst.pop()O(1)
peeklook at the top item, don't remove itst[-1]O(1)
empty checkis anything inside?not stO(1)

2Watch it work: push 1, 2, 4, 5

Take the items 1, 2, 4, 5 and push them one by one. Each new item lands on top of the previous one:

push 1
1
push 2
12
push 4
124
push 5
1245

Now suppose you want to remove 2. You can't reach in and grab it. Only the top can be popped: first 5, then 4, and only then is 2 on top and poppable. The item that went in last comes out first. This rule is called LIFO (Last In, First Out).

3Python habits used in every page of this notebook

a Python list is our stack
st = []            # empty stack
st.append(1)       # push -> [1]
st.append(2)       # push -> [1, 2]   (left = bottom, right = top)
top = st[-1]       # peek -> 2
x = st.pop()       # pop  -> x = 2, st = [1]
if st:             # ALWAYS check before pop / peek
    st.pop()
print(st)          # []
Golden rulePopping or peeking an empty list raises IndexError. So write while st and … or if st: before every st.pop() and st[-1].

Part A · Why we need a stack, and how to spot a stack problem

1The problem with arrays: looking back costs a second loop

Say you're walking through an array [1, 2, 3, …] and you've reached 3. Now the question wants something about the items before 3 (like 2 or 1). With a plain array you must start a second loop that walks backwards from 3. Doing that from every position is a loop inside a loop → O(n²).

2What the stack does instead

As we walk forward, we keep the earlier items that still matter (for example, the biggest or smallest ones so far, whatever the question needs) in a stack. When we need to "look back", the right item is waiting on top, with no backward loop. One pass → O(n).

The price is memory: the stack can hold up to n items, so O(n) extra space. A stack is the right tool when that extra space is allowed.

3How to spot a stack problem

Signals from the teacher • You need to go back (backtrack) over items you've already passed.
• From each position you'd have to walk right to left (or left to right again) to find something.
• Your brute force is O(n²) because of that second loop, and you want O(n).
• Extra space O(n) is acceptable.

Part B · Pattern 1: Monotonic stack

1The idea

Monotonic means "always moving in one direction". A monotonic stack is an ordinary stack where we make sure the items inside are always sorted, either increasing or decreasing from bottom to top.

A normal stack would simply hold everything we push. A monotonic stack, before pushing a new item, pops every item on top that would break the order. Those popped items are ones we have proved useless for the rest of the problem.

2Tiny example: push 2, 4, 3, 8, 1

A normal stack ends as [2, 4, 3, 8, 1], with no order at all. Here is what the two monotonic kinds keep:

itemincreasing stack: pop while top > itemstack afterdecreasing stack: pop while top < itemstack after
2nothing[2]nothing[2]
4nothing (2 < 4)[2, 4]pop 2[4]
3pop 4 (4 > 3)[2, 3]nothing (4 > 3)[4, 3]
8nothing[2, 3, 8]pop 3, pop 4[8]
1pop 8, 3, 2[1]nothing (8 > 1)[8, 1]

At every row, each stack is sorted. That's the whole definition.

3Why it's useful (what the stack remembers)

Take "next greater element": for every item, find the first bigger item to its right. Walk from the right and keep a decreasing stack (bottom → top). When a new item x arrives, every smaller-or-equal item on top gets popped. Why is that safe? Any item further left looks right and sees x first, and x is at least as big, so a popped item can never be the first bigger one for anybody again. What survives on top is exactly the answer for x.

The teacher says about 25–30% of stack questions belong to this pattern (next greater element, next smaller element, daily temperatures, …). She leaves the "how to maintain it in code" for the problem videos. Problems 2–4 of this notebook cover it in full.

4Why not an array?

Without the stack, for every item you'd scan right until you find a bigger one: O(n²). With the stack, every item is pushed once and popped at most once: O(n).

5Python template

Monotonic template: next greater to the right
def next_greater(arr):
    ans = [-1] * len(arr)
    st = []                                  # bottom -> top: strictly decreasing values
    for i in range(len(arr) - 1, -1, -1):    # answer is on the right -> walk from the right
        while st and st[-1] <= arr[i]:       # not greater -> useless forever
            st.pop()
        if st:
            ans[i] = st[-1]                  # survivor on top = first greater on the right
        st.append(arr[i])
    return ans
knobchoices
directionanswer on the right → loop right to left · answer on the left → loop left to right
pop condition"greater" questions → pop while top <= x · "smaller" questions → pop while top >= x
what to storevalues, or indices when the answer is a distance or a position
RememberMonotonic = sorted inside. Pop what can never be an answer again, then push. Each item is pushed once and popped once → O(n).

Part C · Pattern 2: Expression evaluation

1The idea

When the input contains numbers and operators (+, −, ×, ÷) and you must calculate the result, think stack. The reason is precedence: by the BODMAS rule, × and ÷ must happen before + and −. So you often can't apply an operator the moment you see it. You have to keep earlier numbers waiting somewhere and come back to them. Keeping things waiting is what a stack is for.

2Tiny example: 2 + 3 * 5 - 4

To get 13, we evaluate 3 × 5 and only then go back left to add the 2. "Going back left" with plain arrays means nested loops (O(n²)). The stack avoids that.

3The stack method (three things to track)

The teacher keeps track of three things: the pointer i that walks the string, the last operator seen (op), and the stack of numbers. The rule when a number is complete:

last operator was…do thiswhy
+push +numAddition is lowest priority. Don't add yet: a × might follow and grab this number.
−push −numSame priority as +. Store the number with its minus sign, then everything becomes a sum at the end.
×pop the top, push top × num× binds to the nearest number on its left, and that number is on top of the stack.
÷pop the top, push top ÷ num (cut toward zero)Same reason as ×.

At the end, the answer = sum of everything in the stack.

Hand table for 2+3*5-4

icharwhat happensstack AFTERop after
02build number: num = 2 (can't do anything with it yet)[]+ (start)
1+number done. Last op is + → push 2[2]+
23num = 3[2]+
3*last op + → push 3 (don't add 2 + 3, a × may follow!)[2, 3]*
45num = 5[2, 3]*
5-last op × → pop 3, push 3 × 5 = 15[2, 15]−
64num = 4, end of string → last op − → push −4[2, 15, −4]-

Sum = 2 + 15 − 4 = 13 ✓

before the ×
23
3 popped, 15 pushed
215
end → sum
215−4

4Python template

Expression template (+ - * /, no brackets)
def calculate(s):
    st = []
    num = 0
    op = '+'                                 # pretend there is a '+' before the first number
    for i, ch in enumerate(s):
        if ch.isdigit():
            num = num * 10 + int(ch)         # numbers can have many digits
        if (not ch.isdigit() and ch != ' ') or i == len(s) - 1:
            if op == '+':
                st.append(num)
            elif op == '-':
                st.append(-num)
            elif op == '*':
                st.append(st.pop() * num)
            else:                            # '/': cut toward zero
                a = st.pop()
                q = abs(a) // num
                st.append(q if a >= 0 else -q)
            op = ch                          # remember this operator for the NEXT number
            num = 0
    return sum(st)
linewhat it means
op = '+'The first number has no operator before it. Treating it as "+" makes it go straight into the stack.
num = num * 10 + int(ch)Builds multi-digit numbers: "12" → 1, then 12.
if (not ch.isdigit() and ch != ' ') or i == len(s) - 1:A number has just ended: either we hit an operator, or we're at the last character.
st.append(st.pop() * num)× uses the nearest number on its left, which is the top of the stack.
q = abs(a) // num ...Python's // rounds down (−7 // 2 = −4). Calculator problems want "toward zero" (−3). So divide the absolute values and put the sign back.
return sum(st)Only + and − remain, and they're baked into the signs. Just add up.
Doubt: why don't we pop for +?
→ Because we don't know yet whether a higher-priority × or ÷ comes next and needs that number. Pushing keeps it available. All the additions happen safely at the very end.
RememberRemember the last operator. + / −: push ±num. × / ÷: pop, combine, push. At the end: sum the stack. Problems: Basic Calculator I / II, Evaluate Reverse Polish Notation.

Part D · Pattern 3: Undo (cancel the previous item)

1The idea

Some questions say: "when you meet a certain character, delete (undo) the one just before it". The "one just before" is exactly the top of a stack. So deleting = popping. The teacher finds this the easiest pattern.

2Tiny example: "babbbacc", where two equal neighbours cancel

Rule: if the current character equals the one right before it, both disappear.

Brute force first

For each i, start j = i − 1 and compare. Only the adjacent character matters (a b farther back can't cancel this one). When they match, delete both from the string. Deleting from the middle of a string shifts all later characters left, which is O(n) each time. Over the whole string → O(n²).

With a stack

The stack holds the characters that survive so far. For each new character: if it matches the top, pop (that deletes the earlier one) and don't push the current one (it's deleted too). Otherwise push it. Popping from the top never shifts anything, so each step is O(1).

icharwhat we pop (and why)what we pushstack AFTER
0bnothing (empty)b[b]
1anothing (top b ≠ a)a[b, a]
2bnothing (top a ≠ b)b[b, a, b]
3bpop b: equals current → both cancelnothing[b, a]
4bnothing (top a ≠ b)b[b, a, b]
5anothinga[b, a, b, a]
6cnothingc[b, a, b, a, c]
7cpop c: equals currentnothing[b, a, b, a]

Read the stack bottom to top: "baba". Check by eye: babbbacc. The circled pairs cancel, and b a b a is left.

Doubt: after a pop, the teacher says to keep checking "until the condition fails". Do I need a while?
→ For "pairs cancel" rules, one check is enough. The stack never has two equal neighbours (we'd have cancelled them), so after one pop the new top can't match the current character again. Any later chain reaction happens naturally: the next character is compared with the new top. For rules where one character can cancel many earlier ones, you would need a while.

3Python template

Undo template (adjacent equal pairs cancel)
def remove_pairs(s):
    st = []
    for ch in s:
        if st and st[-1] == ch:              # current cancels the previous survivor
            st.pop()                         # delete the earlier one ...
        else:                                # ... and do NOT push the current one
            st.append(ch)
    return ''.join(st)                       # survivors, bottom to top
RememberDelete the previous item = pop. Delete the current item = don't push. The stack at the end is the answer. O(n) time, O(n) space. Problems: Remove All Adjacent Duplicates, Backspace String Compare, Make The String Great, Min Length After Removing Substrings.

Part E · Pattern 4: Parentheses

1The idea

Whenever a question asks you to match brackets, check whether brackets are valid, or count what's needed to make them valid, think stack. An opening bracket alone tells you nothing. The checking only starts when a closing bracket appears, and then you must look back for its partner. Looking back = stack.

The partner of a closing bracket must be the most recent unmatched opening bracket. That's precisely the top of the stack.

2Tiny example: [(()){}]

icharwhat we pop (and why)what we pushstack AFTER
0[opener: nothing to check[[ '[' ]
1(opener([ '[', '(' ]
2(opener([ '[', '(', '(' ]
3)pop (: top matchesnothing[ '[', '(' ]
4)pop (: matchesnothing[ '[' ]
5{opener{[ '[', '{' ]
6}pop {: matchesnothing[ '[' ]
7]pop [: matchesnothing[ ]

Stack empty at the end → valid. Without a stack, each closer would start a j loop back towards index 0 to find its partner → O(n²).

deepest point (i=2)
[((
after i=5
[{
end
(empty) → valid

3Python template

Parentheses template (valid or not)
def is_valid(s):
    partner = {')': '(', ']': '[', '}': '{'}
    st = []
    for ch in s:
        if ch in '([{':
            st.append(ch)                    # opener: just remember it
        else:
            if not st or st[-1] != partner[ch]:
                return False                 # closer with no / wrong opener
            st.pop()                         # matched pair disappears
    return not st                            # leftover openers = invalid
Doubt: why check not st at the very end?
→ A string like "((" never hits a bad closer, but its openers are never closed. A non-empty stack at the end means unmatched openers → invalid.

4Space saver: a counter for one bracket type

When there is only one kind of bracket, ( and ), the stack would only ever hold (. So we don't need the items, only how many there are. A plain integer counter does the job in O(1) space. (Not shown in the video. It's the usual follow-up in the parentheses problems of the sheet.)

Counter version (only round brackets)
def min_add_to_make_valid(s):
    open_count = 0                           # '(' still waiting for a ')'
    need_open = 0                            # ')' that had no '(' before them
    for ch in s:
        if ch == '(':
            open_count += 1                  # like a push
        elif open_count > 0:
            open_count -= 1                  # like a pop: matched
        else:
            need_open += 1                   # a ')' with nothing to match
    return need_open + open_count            # brackets we must add
RememberOpener → push. Closer → it must match the top, then pop. End → the stack must be empty. One bracket type → a counter is enough. Problems: Valid Parentheses, Minimum Add, Score of Parentheses, Longest Valid Parentheses, Minimum Remove to Make Valid.

Part F · Pattern 5: Stack-based design

1The idea

Here you're asked to build a data structure using stacks, for example a queue, a min stack or a max stack. The teacher says beginners usually find this pattern tricky, but it isn't really hard, and 3–4 practice problems are enough.

The contrast that matters: a stack is LIFO. Push 1, 2, 3, 4 and pop gives 4 first. A queue is FIFO (First In, First Out). Push 1, 2, 3, 4 and remove gives 1 first, like a line at a ticket counter.

2Tiny example: a queue from two stacks

Use one stack for input (where pushes go) and one for output (where pops come from). When output is empty and someone wants the front, pour all of input into output. Pouring reverses the order, so the oldest item ends up on top of output.

operationin-stack afterout-stack aftervalue returned
push(1)[1][]-
push(2)[1, 2][]-
push(3)[1, 2, 3][]-
pop()[][3, 2]1 (out was empty → pour 3, 2, 1 → pop the top 1)
push(4)[4][3, 2]-
pop()[4][3]2 (out not empty, no pouring)
peek()[4][3]3
pop()[4][]3
pop()[][]4 (pour 4, then pop)
empty()[][]True

Out came 1, 2, 3, 4, the same order they went in. A queue ✓.

3Why it's still fast: amortised O(1)

A single pop may pour many items, which looks like O(n). But each item is poured at most once in its life: pushed into "in" once, moved to "out" once, popped from "out" once. Over any sequence of k operations the total work is O(k), so the average per operation is O(1). This kind of average-over-a-sequence cost is called amortised.

4Python templates

Design template: queue from two stacks
class MyQueue:
    def __init__(self):
        self.inp = []                        # new items land here
        self.out = []                        # oldest item is on top here

    def push(self, x):
        self.inp.append(x)

    def _move(self):
        if not self.out:                     # pour only when out is empty
            while self.inp:
                self.out.append(self.inp.pop())

    def pop(self):
        self._move()
        return self.out.pop()

    def peek(self):
        self._move()
        return self.out[-1]

    def empty(self):
        return not self.inp and not self.out
Design template: min stack
class MinStack:
    def __init__(self):
        self.st = []                         # each entry: (value, min of everything up to here)

    def push(self, val):
        cur_min = val if not self.st else min(val, self.st[-1][1])
        self.st.append((val, cur_min))

    def pop(self):
        self.st.pop()

    def top(self):
        return self.st[-1][0]

    def getMin(self):
        return self.st[-1][1]                # O(1): stored alongside the top
Doubt: why must we pour only when out is empty?
→ Items already in out are older than everything in inp. If we poured on top of them, newer items would land above older ones and come out first, which breaks FIFO.
RememberQueue from stacks = an in stack + an out stack, and pour only when out is empty. Min/max stack = store the running min/max with each item. Problems: Implement Queue using Stacks, Implement Stack using Queues, Min Stack, Design Stack With Increment.

Part G · Pattern 6: Recursive stack

1The idea

You get a stack and must change it, for example reverse it, delete the bottom (or the middle) item, or insert an item at the bottom. The catch: no extra data structure is allowed. You may only use the stack itself and recursion.

2Tiny example: delete the bottom of [1, 2, 3, 4]

The obvious way: pop 4, 3, 2 into another list, pop 1 and throw it away, then push 2, 3, 4 back. That uses an extra list, which is not allowed here.

The recursive way: pop the top and hold it in a local variable of the current function call, then call the function again on the smaller stack. When only one item is left, that's the bottom, so remove it. While the calls return, each one pushes its held item back. The "extra storage" is Python's own call stack, not a data structure we built.

calls waiting (deepest point)
call 1 holds 4call 2 holds 3call 3 holds 2call 4: st=[1] → pop 1
the real stack then
(empty)
after the calls return
234

3Python templates

Recursive stack templates
def insert_at_bottom(st, x):
    if not st:                               # empty -> x is now the bottom
        st.append(x)
        return
    top = st.pop()                           # hold the top in this call
    insert_at_bottom(st, x)
    st.append(top)                           # put it back on the way out

def reverse_stack(st):
    if not st:
        return
    top = st.pop()
    reverse_stack(st)                        # reverse the rest
    insert_at_bottom(st, top)                # old top goes to the bottom

def delete_bottom(st):
    if not st:
        return
    if len(st) == 1:                         # only the bottom is left
        st.pop()
        return
    top = st.pop()
    delete_bottom(st)
    st.append(top)
functionexampletime
insert_at_bottom([1, 2, 3], 9)→ [9, 1, 2, 3]O(n)
reverse_stack([1, 2, 3, 4])→ [4, 3, 2, 1]O(n²): n inserts, each O(n)
delete_bottom([1, 2, 3, 4])→ [2, 3, 4]O(n)
Doubt: isn't the call stack also extra space?
→ Yes, O(n) of it. The rule in these problems is about not creating an extra data structure yourself. Recursion is the allowed tool.
RememberPop, hold the item in the call, recurse on the smaller stack, push it back on the way out. Problems: reverse a stack, sort a stack, delete the middle, insert at the bottom (see the Recursion notebook).

Part H · Which sheet problem uses which pattern

Grouped by the technique each problem needs (the sheet's own order may group a few slightly differently):

patternproblems in this notebookwhat the stack holds
Monotonic2 Next Greater Element · 3 Next Greater Element II · 4 Daily Temperatures · 5 Asteroid Collision · 6 Largest Rectangle in Histogram · 9 Maximal Rectanglesorted candidates (values or indices)
Monotonic + greedy23 Remove K Digits · 24 Remove Duplicate Lettersthe best answer built so far. Pop a bigger previous digit/letter to make the result smaller.
Expression evaluation7 Basic Calculator II · 8 Evaluate Reverse Polish Notation · 22 Decode String (nested, uses a stack per bracket level)numbers (with signs) waiting to be combined
Undo10 Backspace String Compare · 11 Remove All Adjacent Duplicates · 12 Make The String Great · 13 Min Length After Removing Substringscharacters that survive so far
Parentheses14 Valid Parentheses · 15 Minimum Add · 16 Score of Parentheses · 17 Longest Valid Parentheses · 25 Minimum Remove to Make Validunmatched openers (or their indices)
Design18 Queue using Stacks · 19 Stack using Queues · 20 Min Stack · 21 Stack With Incrementthe data, plus extra info (min, pending increments…)
Recursive stackcovered in the Recursion notebook (reverse a stack, sort a stack)the call stack holds popped items

The teacher's advice: solve at least 3–4 problems from each pattern and tick them off in the sheet as you go.


Part I · Revision page

patternsignal in the questioncore movebrute force → stack
Monotonicnext / previous greater or smallerpop what can never be an answer, then pushO(n²) → O(n)
Expressionnumbers + operators, "evaluate"last operator decides: push ±num, or pop-combine-pushO(n²) → O(n)
Undoa character deletes the previous onematch → pop (and don't push)O(n²) → O(n)
Parenthesesmatch / validate / count bracketsopener push, closer must match topO(n²) → O(n)
Design"implement X using stacks", min/max stacktwo stacks, or store extra info per itemamortised O(1) per op
Recursivechange a stack with no extra structurepop, recurse, push backuses the call stack
If you remember only 5 lines 1. Stack = LIFO, only the top: push, pop, peek, all O(1). No indexing.
2. Need to look back at earlier items? A stack turns the O(n²) back-scan into O(n), for O(n) space.
3. Monotonic: keep it sorted by popping useless items. Each item is pushed once and popped once.
4. Expression / Undo / Parentheses: the top is "the most recent thing still waiting".
5. Design uses two stacks (amortised O(1)). Recursive changes the stack using only the call stack.
Mistakes to avoid ✗ popping or peeking an empty stack
✗ applying + immediately in an expression (breaks BODMAS: 21 instead of 13)
✗ using // for "truncate toward zero" with negative numbers
✗ pushing the current character after it cancelled the top (undo pattern)
✗ forgetting the "stack must be empty at the end" check for brackets
✗ pouring into the out-stack while it still has items (queue design)
test it yourself (paste under the templates above)
print(next_greater([2, 4, 3, 8, 1]))     # [4, 8, 8, -1, -1]
print(calculate("2+3*5-4"))              # 13
print(remove_pairs("babbbacc"))          # baba
print(is_valid("[(()){}]"))              # True
print(min_add_to_make_valid("())("))     # 2
q = MyQueue(); q.push(1); q.push(2); q.push(3)
print(q.pop(), q.peek())                 # 1 2
st = [1, 2, 3, 4]; reverse_stack(st)
print(st)                                # [4, 3, 2, 1]

Based on this video: Stack Data Structure | 6 Stack Patterns