DSA sheet · Stack · Stack-based design pattern

Min Stack

Build a stack that can also tell you its smallest item in O(1), at any moment, even after pops. Push, pop and top are easy; getMin in constant time is the whole problem. The teacher solves it twice: first with a second "min stack" (2n space), then with a clever encode/decode trick that keeps only one stack and one variable (n space). At the end she shows that Max Stack is the same idea with the comparisons flipped. She calls this problem very interesting because of the space optimisation.

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 (operation-sequence table) → ⑨ complexity & remember

Part 0 · Before starting

What is a stack?

A stack is a pile of plates: you add and remove only at the top. Last in, first out (LIFO). Operations: push(x) puts x on top, pop() removes the top, top() (peek) reads the top without removing it.

a Python list is a ready-made stack
st = []
st.append(5)      # push 5   -> [5]
st.append(8)      # push 8   -> [5, 8]   (right end = top)
st[-1]            # peek     -> 8
st.pop()          # pop      -> 8, st = [5]
if st:            # ALWAYS check "not empty" before pop() or [-1]
    print(st[-1])

We draw stacks left = bottom, right = top, like the list.

A stack never forgets what was below

This is the property that makes Min Stack possible. When you push x, everything already inside stays there, untouched, until x is popped. So "the smallest item from the bottom up to x" stays the same for as long as x is in the stack. We can work it out once at push time and keep it next to x.

Design problems: what does the cost of an operation mean?

In design problems we price each method separately. Worst-case O(1) means every single call is fast. Amortised O(1) means some calls can be slow but the average over a long run is constant (like the queue made of two stacks, Problem 18). In this problem the teacher reaches true worst-case O(1) for every method, which is even stronger than amortised.

Why it matters, counted: with 105 items in the stack and 105 getMin calls, scanning the stack each time costs up to 105 × 105 = 1010 steps (too slow; about 108 is the limit). Reading a stored answer costs 105 steps in total.

Part A · Two stacks: stack + min stack

LeetCode 155

1The question in simple words

Design MinStack with:

Each must run in O(1) time. pop, top and getMin are always called on a non-empty stack.

The teacher's example: push 5, push 8, getMin → 5, push 4, getMin → 4, pop, getMin → 5, push 9, getMin → 5.

2What the constraints tell us

3Intuition: store "the minimum so far" next to every item

Without the O(1) rule, getMin would be a simple loop over the stack. With it, we must keep track of the minimum at every step instead of searching for it later.

Keep a second stack, the min stack, that grows and shrinks together with the main stack. On each push, the min stack gets the smallest value among everything pushed so far that is still inside. Then getMin is just the top of the min stack.

The teacher also mentions you could keep the pair (value, min so far) in one stack. That's the same thing, still 2n space.

4Building the logic from the example

push 5

Main stack [5]. The min stack is empty, so this is the first item: the minimum is 5 itself. Min stack [5].

push 8

Main [5, 8]. What's the minimum of 5 and 8? Compare the new value with the top of the min stack: min(8, 5) = 5. Min stack [5, 5].

Doubt: why push 5 again into the min stack, and not 8?
→ The min stack doesn't store the original numbers; the main stack does that. The min stack stores the answer to getMin for that height of the stack. With 5 and 8 inside, the answer is 5.

getMin → 5

Just read the top of the min stack. We don't pop anything.

push 4 → min(4, 5) = 4

Main [5, 8, 4], min [5, 5, 4]. getMin → 4.

pop → pop BOTH stacks

Main [5, 8]. If we left the min stack alone, its top would still be 4, and getMin would answer 4, a number that isn't even in the stack anymore. So every pop must remove from both stacks. Min [5, 5] → getMin 5 ✓ (correct: 5 and 8 are left).

push 9 → min(9, 5) = 5

5 was pushed first and is still the smallest. However many bigger items come later, getMin keeps saying 5 until 5 itself is popped.

5Approach steps

  1. push(x): push x on the main stack. If the min stack is empty, push x on it; else push min(x, minStack top).
  2. pop(): pop from both stacks (only if non-empty).
  3. top(): top of the main stack.
  4. getMin(): top of the min stack.

6Code (Python)

Min Stack with two stacks
class MinStack:
    def __init__(self):
        self.st = []        # the real values
        self.mn = []        # mn[i] = smallest of st[0..i]

    def push(self, val):
        self.st.append(val)
        if not self.mn:                         # first item
            self.mn.append(val)
        else:
            self.mn.append(min(val, self.mn[-1]))

    def pop(self):
        if self.st:                             # both stacks always have the same size
            self.st.pop()
            self.mn.pop()

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

    def getMin(self):
        return self.mn[-1] if self.mn else -1

The teacher returns −1 from top/getMin when the stack is empty, as a safety net. LeetCode never calls them on an empty stack.

7Code line by line

linewhat it means
self.st.append(val)The real value always goes on the main stack.
if not self.mn: self.mn.append(val)Nothing before it, so it is the minimum.
self.mn.append(min(val, self.mn[-1]))The new minimum is either the new value or the old minimum, whichever is smaller.
self.st.pop() self.mn.pop()Remove the item and the "minimum at that height" together, so the two stacks stay the same size.
return self.mn[-1]Read, don't pop. getMin must not change the stack.
Doubt: should empty-style checks look at st or mn?
→ Either works, because the two always have equal size. Logically the main stack is the one that "is" the stack, so the teacher checks st.

8Dry run: operation-sequence table

#operationwhyst aftermn afterreturned
1push 5mn empty → 5[5][5]–
2push 8min(8, 5) = 5[5, 8][5, 5]–
3getMintop of mn[5, 8][5, 5]5
4push 4min(4, 5) = 4[5, 8, 4][5, 5, 4]–
5getMintop of mn[5, 8, 4][5, 5, 4]4
6poppop both[5, 8][5, 5]–
7getMintop of mn[5, 8][5, 5]5
8push 9min(9, 5) = 5[5, 8, 9][5, 5, 5]–
9toptop of st[5, 8, 9][5, 5, 5]9
10getMintop of mn[5, 8, 9][5, 5, 5]5
after step 4: st
584
after step 4: mn
554
after step 6: st
58
after step 6: mn
55

Each row of mn is the answer to getMin when the stack has exactly that many items.

9Complexity & remember

Remember Part AA second stack holds "min so far" for every height. push: min(x, mn top). pop: pop both. getMin: mn[-1].

Part B · One stack + one variable: encode the previous minimum

1The question again, with the new goal

Same four methods, all O(1), but now use only one stack of n values plus a few variables: O(n) space instead of O(2n).

2What the constraints tell us now

3Intuition: why one variable fails, and how to hide the old minimum inside the stack

First try: keep one variable mini. push 5 → mini 5. push 8 → still 5. push 4 → mini 4. getMin → 4 ✓. Now pop 4. The minimum should go back to 5, but the variable only remembers 4, and 5 is gone. One variable can't remember the previous minimum. Two, three variables? There can be 105 changes, so no.

The teacher notices when the old minimum gets lost: only at a push that makes a new minimum (like 4). Pushing 8 or 9 doesn't change the minimum, so popping them later doesn't need any fixing.

So the idea: at the moment a new minimum arrives, don't store it as it is. Store a special "encoded" number that both (a) marks this spot as "the minimum changed here" and (b) hides the previous minimum inside it. When we later pop that spot, we notice the mark and decode the old minimum back.

4Building the encode/decode rule step by step

Step 1: find a mark we can recognise

Look at every normal (not encoded) value in the stack: each one is ≥ the current minimum (5 ≥ 5, 8 ≥ 5). That's always true, since the minimum is the smallest of them. So if we ever see a stack value that is less than the current minimum, it can't be a normal value. That breaks the rule on purpose and works as our mark.

The markstack top < mini → "this is an encoded spot; the minimum changed here".

Step 2: derive a formula that is smaller than the new minimum

A new minimum x arrives, with the previous minimum prev. We know x < prev, so

x − prev < 0 → add x to both sides → 2x − prev < x

So store e = 2x − prev. After the push, the minimum becomes x, and e < x = mini. The mark works ✓. Example: prev = 5, x = 4 → e = 2·4 − 5 = 3, and 3 < 4 ✓. (Any number below 4 would mark the spot; this formula also hides 5 inside.)

Step 3: decode when popping

We pop e = 3 and see 3 < mini (4). From e = 2x − prev and x = mini, solve for prev:

prev = 2·mini − e = 2·4 − 3 = 5 ✓

So pop sets mini = 2·mini − top, bringing back the old minimum.

Doubt: what is the real value at an encoded spot? (needed for top())
→ The real value there was x, the item that became the minimum, and it's still the minimum while it's on top. So if the top is encoded, the real top value is mini itself. Otherwise the top is a normal value; return it as it is.
Doubt: what if x equals the current minimum (push 5 when mini is 5)?
→ The minimum doesn't change, so store 5 normally. When it's popped, 5 < 5 is false, so mini stays 5, which is correct because another 5 is still inside. That's why the encode test is strictly x < mini.
Doubt: does it work with negative numbers?
→ Yes; the algebra never assumed positive values. mini = −3, push −10: e = 2·(−10) − (−3) = −17 < −10 ✓. Pop: prev = 2·(−10) − (−17) = −3 ✓.

5Approach steps

  1. push(x): if the stack is empty → push x, mini = x. Else if x < mini → push 2x − mini, then mini = x. Else → push x.
  2. pop(): take the top. If top < mini → it was encoded → mini = 2·mini − top.
  3. top(): if top < mini → return mini, else return top.
  4. getMin(): return mini.

6Code (Python)

Min Stack with O(n) space (encode/decode)
class MinStack:
    def __init__(self):
        self.st = []
        self.mini = None

    def push(self, val):
        if not self.st:                          # first item
            self.st.append(val)
            self.mini = val
        elif val < self.mini:                   # new minimum: hide the old one
            self.st.append(2 * val - self.mini)  # encoded, always < val
            self.mini = val
        else:
            self.st.append(val)                  # minimum unchanged

    def pop(self):
        if not self.st:
            return
        top = self.st.pop()
        if top < self.mini:                     # encoded spot: minimum changed here
            self.mini = 2 * self.mini - top      # decode the previous minimum

    def top(self):
        if not self.st:
            return -1
        top = self.st[-1]
        return self.mini if top < self.mini else top

    def getMin(self):
        return self.mini if self.st else -1

7Code line by line

linewhat it means
if not self.st: ... self.mini = valThe first item is the minimum. Nothing older to hide.
elif val < self.mini:A new minimum. This is the only moment the old minimum would be lost.
self.st.append(2 * val - self.mini)Store the encoded number. It is smaller than val (the new mini), so we can recognise it later, and it contains the old mini.
self.mini = valNow update the minimum. Order matters: encode with the OLD mini first.
else: self.st.append(val)Not a new minimum: a plain value, ≥ mini.
if top < self.mini: self.mini = 2 * self.mini - topWe're removing the item that was the minimum. Decode the minimum from before it.
return self.mini if top < self.mini else topAt an encoded spot, the real value is the current minimum.

8Dry run: operation-sequence table

The teacher's sequence first, then two more new minimums in a row to see that the chain of old minimums decodes back correctly. Bold = encoded value.

#operationwhyst aftermini afterreturned
1push 5empty → plain, mini = 5[5]5–
2push 88 ≥ 5 → plain[5, 8]5–
3getMinread mini[5, 8]55
4push 44 < 5 → store 2·4 − 5 = 3; mini = 4[5, 8, 3]4–
5getMinread mini[5, 8, 3]44
6top3 < 4 → encoded → real value = mini[5, 8, 3]44
7pop3 < 4 → mini = 2·4 − 3 = 5[5, 8]5–
8getMinread mini[5, 8]55
9push 99 ≥ 5 → plain[5, 8, 9]5–
10push 22 < 5 → store 2·2 − 5 = −1; mini = 2[5, 8, 9, −1]2–
11push 11 < 2 → store 2·1 − 2 = 0; mini = 1[5, 8, 9, −1, 0]1–
12pop0 < 1 → mini = 2·1 − 0 = 2[5, 8, 9, −1]2–
13pop−1 < 2 → mini = 2·2 − (−1) = 5[5, 8, 9]5–
14top, getMin9 ≥ 5 → plain[5, 8, 9]59, 5
after step 4 (mini = 4)
583 (enc)
after step 11 (mini = 1)
589−1 (enc)0 (enc)
after step 13 (mini = 5)
589

Each encoded value holds the minimum from just before it: 0 hides 2, −1 hides 5. Popping peels them off in order.

9Complexity & remember

The teacher's honest advice: encode/decode tricks are rare in DSA, and it's normal not to invent this on your first try. But after seeing it once, you should be able to reproduce it, including why the formula is 2x − prev.

Remember Part BNew min x → push 2x − mini, mini = x. Pop: if top < mini → mini = 2·mini − top. Top: if top < mini → answer is mini. Encode only when x < mini (strict).

Part C · Max Stack: the same trick, flipped

The sheet's next item is a stack with getMax() instead of getMin(). The teacher says everything is the same and asks you to try it on your own first. (Note: LeetCode 716 "Max Stack" also has a popMax method, which needs a different design. Here we mean the simple version: push, pop, top, getMax.)

1What changes

2Code (Python)

Max Stack, two stacks
class MaxStackTwo:
    def __init__(self):
        self.st, self.mx = [], []

    def push(self, val):
        self.st.append(val)
        self.mx.append(val if not self.mx else max(val, self.mx[-1]))

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

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

    def getMax(self):
        return self.mx[-1] if self.mx else -1
Max Stack, O(n) space (encode/decode)
class MaxStack:
    def __init__(self):
        self.st = []
        self.maxi = None

    def push(self, val):
        if not self.st:
            self.st.append(val)
            self.maxi = val
        elif val > self.maxi:                   # new maximum
            self.st.append(2 * val - self.maxi)  # encoded, always > val
            self.maxi = val
        else:
            self.st.append(val)

    def pop(self):
        if not self.st:
            return
        top = self.st.pop()
        if top > self.maxi:                     # encoded spot
            self.maxi = 2 * self.maxi - top

    def top(self):
        if not self.st:
            return -1
        top = self.st[-1]
        return self.maxi if top > self.maxi else top

    def getMax(self):
        return self.maxi if self.st else -1

3Dry run

#operationwhyst aftermaxi afterreturned
1push 3first → plain[3]3–
2push 77 > 3 → store 2·7 − 3 = 11[3, 11]7–
3push 66 ≤ 7 → plain[3, 11, 6]7–
4getMaxread maxisame77
5pop6 ≤ 7 → plain[3, 11]7–
6top11 > 7 → encoded → real value = maxi[3, 11]77
7pop11 > 7 → maxi = 2·7 − 11 = 3[3]3–
8getMaxread maxi[3]33

4Complexity & remember

O(1) per method; O(2n) or O(n) space, exactly as for Min Stack.

Remember Max StackFlip every comparison: encode when x > maxi, the mark is top > maxi. The formulas 2x − prev and 2·maxi − top stay the same.

Part D · Revision page

Part A: two stacksPart B: encode/decode
storedst (values) + mn (min so far)st (values, some encoded) + one variable mini
pushpush x; push min(x, mn top)x < mini → push 2x − mini, mini = x; else push x
poppop bothif top < mini → mini = 2·mini − top
topst[-1]top < mini ? mini : top
getMinmn[-1]mini
time / spaceO(1) each / O(2n)O(1) each / O(n)
Java/C++ noteint is finestore long: 2x − mini can overflow int
If you remember only 5 lines 1. O(1) getMin means tracking the minimum at every push, not searching later.
2. Two stacks: mn top = min so far; pop both stacks together.
3. One variable alone fails because a pop can remove the current minimum.
4. Hide the old minimum in an encoded value 2x − prev, which is smaller than x, so it's recognisable.
5. Decode on pop with prev = 2·mini − top; for Max Stack flip the comparisons.
Mistakes to avoid ✗ popping only the main stack in Part A (getMin returns a value that's gone)
✗ updating mini before computing 2x − mini (encodes the wrong value)
✗ using <= instead of < to decide encoding
✗ returning the raw encoded number from top()
✗ in Java/C++, storing 2x − mini in an int (overflow near ±231)
test it yourself (paste under either MinStack)
s = MinStack()
s.push(5); s.push(8)
print(s.getMin())   # 5
s.push(4)
print(s.getMin())   # 4
print(s.top())      # 4
s.pop()
print(s.getMin())   # 5
s.push(9)
print(s.top(), s.getMin())   # 9 5
s.push(-2**31); print(s.getMin())   # -2147483648

Based on this video: Min Stack