DSA sheet · Stack · Stack-based design pattern

Implement Queue using Stacks

This video opens a new pattern in the stack sheet: design problems. Instead of solving a puzzle on an array, we have to build a data structure using only another data structure. Here we build a queue (first in, first out) using only stacks (last in, first out). The teacher first shows a simple version where every push costs O(n), then improves it so that push is O(1) and pop is amortised O(1). That improvement, and being able to explain why it is better, is what interviewers look for.

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 can only touch the top plate. The last plate you put on is the first one you take off. This rule is called LIFO: Last In, First Out.

The Python list as a stack

a Python list is a ready-made stack
st = []            # empty stack
st.append(5)       # push 5        -> [5]
st.append(7)       # push 7        -> [5, 7]   (right end = top)
top = st[-1]       # peek          -> 7
x = st.pop()       # pop           -> x = 7, st = [5]
if st:             # not empty?    (an empty list is False)
    print(st[-1])

All four operations are O(1). Always check that the stack is not empty before pop() or [-1]. On an empty list both of them crash with an IndexError.

In these notes we draw a stack left = bottom, right = top, like the list itself: [1, 2, 3] means 3 is on top.

What is a queue?

A queue is a line at a ticket counter. The person who came first is served first. This is FIFO: First In, First Out. New people join at the back; people leave from the front.

we push 1, 2, 3, 4, then pop oncewhat comes out
stack (LIFO)4, the newest
queue (FIFO)1, the oldest

The rule of a design problem

We may use only the normal stack operations: push to top, pop from top, peek top, size and is-empty. In Python that means append, pop(), [-1] and len / truthiness. Using pop(0) or st[0] would be cheating, because a real stack cannot reach its bottom.

What does "amortised" cost mean?

Sometimes one operation is expensive, but it happens so rarely that if you add up the cost of a long run of operations and divide by how many there were, each one costs only a constant on average. That average over a whole sequence (not over random inputs) is the amortised cost.

A daily-life picture: you buy a monthly train pass once (expensive day), then ride for free for 30 days. The price per ride is small, even though one day was costly. In Part B, moving all items from one stack to the other is the "monthly pass"; it pays for many cheap pops after it.

Part A · Version 1: costly push (reverse on every push)

LeetCode 232

1The question in simple words

Build a class MyQueue that behaves exactly like a queue, but inside it you may only keep stacks. It must support:

Example call sequence: push(1), push(2), peek() → 1, pop() → 1, empty() → False.

2What the constraints tell us

3Intuition: why one stack is not enough

Push 1 and then 2 onto one stack: [1, 2]. A queue's pop must return 1, but 1 is at the bottom. The only thing a stack lets us touch is the top (2). To reach 1, we must lift 2 off and put it somewhere for a while. That "somewhere" must be a second container, and since we may only use stacks, it is a second stack.

The teacher's idea for Version 1: keep stack 1 always in reversed order, so that the oldest item is always on top. Then pop and peek are just the normal stack pop and peek. Stack 2 is only a temporary holder ("keep my plates for a moment, I'll take them back").

4Building the logic from the example

Push 1 into an empty stack

Nothing is in st1, so just push: st1 = [1]. The oldest item (1) is on top ✓.

Push 2: the new item must go to the BOTTOM

2 is the newest, so in a queue it must come out last. In st1 "comes out last" means "sits at the bottom". But we can only push on top. So:

  1. Move everything from st1 to st2 (pop from st1, push to st2): st1 = [], st2 = [1].
  2. Now st1 is empty, so pushing 2 puts it at the bottom: st1 = [2].
  3. Move everything back from st2 to st1: st1 = [2, 1], st2 = [].

Now 1 is on top again. A pop gives 1, exactly what a queue should give ✓.

Doubt: when we move items to st2 and back, doesn't their order get messed up?
→ No. Moving from one stack to another reverses the order. Doing it twice reverses twice, which gives back the original order. The only change is that the new item has slipped in underneath all of them.

Pop, peek and empty

Doubt: why is checking only st1 enough for empty()?
→ st2 is used only inside push, as a holder, and push always empties it again before finishing. So between any two calls st2 is always empty, and every item of the queue lives in st1. If st1 is empty, the queue is empty.

5Approach steps

  1. Keep two stacks: st1 (holds the queue, oldest on top) and st2 (temporary).
  2. push(x): move all of st1 into st2 → push x into st1 → move all of st2 back into st1.
  3. pop(): return st1.pop().
  4. peek(): return st1[-1].
  5. empty(): return not st1.

6Code (Python)

Version 1: costly push
class MyQueue:
    def __init__(self):
        self.st1 = []     # the queue lives here, OLDEST item on top
        self.st2 = []     # temporary holder, empty between calls

    def push(self, x):
        while self.st1:                     # 1. lift everything off st1
            self.st2.append(self.st1.pop())
        self.st1.append(x)                  # 2. x lands at the bottom
        while self.st2:                     # 3. put everything back on top of x
            self.st1.append(self.st2.pop())

    def pop(self):
        return self.st1.pop()               # oldest is on top

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

    def empty(self):
        return not self.st1

7Code line by line

linewhat it means
self.st1 = [] self.st2 = []Two empty Python lists used as stacks. They are created once per queue object, inside __init__.
while self.st1: self.st2.append(self.st1.pop())Lift every item off st1 and pile it on st2. After this st1 is empty and st2 holds the items in reverse.
self.st1.append(x)st1 is empty, so x becomes its bottom item. The newest item must come out last, so the bottom is exactly where it belongs.
while self.st2: self.st1.append(self.st2.pop())Bring every item back. The second reversal restores their order, now sitting on top of x.
return self.st1.pop()The top of st1 is the oldest item, the one a queue must hand out first.
return self.st1[-1]Same item, but only looked at, not removed.
return not self.st1An empty list is falsy, so not st1 is True exactly when the queue is empty.

8Dry run: operation-sequence table

The teacher pushes 1 and 2, pops, then keeps pushing. We use push 1, push 2, pop, push 3, push 4, peek, pop, empty. Stacks are drawn left = bottom, right = top. "moves" counts single stack pushes/pops so we can compare with Part B later.

#operationwhat happens insidest1 afterst2 afterreturnedmoves
1push 1st1 empty → just push[1][]–1
2push 21 → st2, push 2, 1 → back[2, 1][]–5
3poptop of st1[2][]11
4push 32 → st2, push 3, 2 → back[3, 2][]–5
5push 42, 3 → st2 (st2 = [2, 3]), push 4, 3 then 2 → back[4, 3, 2][]–9
6peeklook at top[4, 3, 2][]20
7poptop of st1[4, 3][]21
8emptyst1 has items[4, 3][]False0
push 4, middle step: st1 cleared
(empty)
st2 holding 2, 3
23
st1 after 4 pushed at bottom
4
st1 after moving back
432

Bottom of each picture is the bottom of the stack; the red box is the top. The oldest item (2) is always on top of st1, so pop is instant.

9Complexity & remember

It passes (100 calls is tiny), but the teacher asks: can we avoid paying O(n) on every push?

Remember Version 1Keep st1 reversed (oldest on top). push = empty st1 into st2, push x, pour st2 back. pop/peek/empty only touch st1.

Part B · Version 2: lazy transfer, amortised O(1)

1The question again, with the new goal

Same four operations. Now we want push in O(1) and pop/peek in amortised O(1) (cheap on average over the whole run).

2What the constraints tell us now

3Intuition: don't reverse until someone needs the front

Look at what Version 1 wastes. Push 1, 2, 3 in a row with no pop in between:

Nobody asked for the front yet, yet we kept reshuffling. The teacher's fix: just pile up new items in st1, and reverse only when a pop or peek actually needs the front. And once reversed into st2, leave them there; st2's top stays the oldest item until st2 runs out.

So the two stacks get fixed jobs:

stackjoborder inside
st1 ("in" stack)receives every pushnewest on top
st2 ("out" stack)serves every pop and peekoldest on top

4Building the logic from the teacher's example

push 1, push 2, push 3 → just push

st1 = [1, 2, 3], st2 = []. Three moves in total.

pop → st2 is empty, so pour st1 into st2

Pop 3, 2, 1 from st1 and push them into st2 in that order: st2 = [3, 2, 1]. Now 1, the oldest, is on top. Pop it → 1 ✓.

push 4 → just push into st1

st1 = [4], st2 = [3, 2].

pop → st2 is NOT empty, so do NOT pour

The front of the queue is 2, and 2 is already on top of st2. Pop → 2 ✓.

Doubt: why not pour st1 into st2 every time a pop comes?
→ Try it: st2 = [3, 2], pour 4 on top → st2 = [3, 2, 4]. Now pop gives 4, but the queue's front is 2. Everything in st2 is older than everything in st1 (it was pushed earlier), so st2 must be fully used up before any st1 item comes over. That's the rule: pour only when st2 is empty.

pop → 3, then pop → 4

st2 = [3] → pop gives 3, st2 is empty. Next pop: st2 is empty, so pour st1 (just [4]) into st2 → pop gives 4 ✓.

peek uses the same rule as pop

peek also needs the front, so it does the same "if st2 is empty, pour" step, then returns st2[-1] instead of popping it.

empty must now check BOTH stacks

Items can sit in either stack. After 100 pushes and no pop, all of them are in st1 and st2 is empty. After 100 pushes and one pop, 99 of them are in st2 and st1 is empty. In neither case is the queue empty. So the queue is empty only when both st1 and st2 are empty.

Doubt: in Version 1 we checked only st1. Why is that wrong now?
→ In Version 1, st2 was always empty between calls. In Version 2, st2 keeps items between calls by design. Checking only st1 would say "empty" right after the first pop in the example above, while 2 and 3 are still waiting in st2.
Doubt: why make push the cheap one, and not pop?
→ The teacher's reasoning: you can never pop more items than you have pushed, so the number of pushes is at least the number of pops. Speeding up the operation that happens more often helps more. (There is also a mirror-image version that keeps pop O(1) and makes push O(n), which is just Version 1. The lazy version is better than both because it never moves an item back.)

5Approach steps

  1. push(x): st1.append(x). Nothing else.
  2. pop(): if st2 is empty, move every item from st1 into st2. Then return st2.pop().
  3. peek(): same "if st2 is empty, pour" step, then return st2[-1].
  4. empty(): return True only if both st1 and st2 are empty.

6Code (Python)

Version 2: amortised O(1)
class MyQueue:
    def __init__(self):
        self.st1 = []     # "in" stack: every push lands here
        self.st2 = []     # "out" stack: oldest item on top

    def push(self, x):
        self.st1.append(x)                  # O(1), no reshuffling

    def _move(self):
        if not self.st2:                    # pour ONLY when st2 ran out
            while self.st1:
                self.st2.append(self.st1.pop())

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

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

    def empty(self):
        return not self.st1 and not self.st2

The teacher writes the "if st2 is empty, pour" loop twice, once inside pop and once inside peek. Putting it in a small helper _move is the same logic with less repetition.

7Code line by line

linewhat it means
self.st1.append(x)A new item is the newest in the queue. Just drop it on st1. One move.
if not self.st2:Only when the out-stack has run dry do we refill it. If it still has items, its top is already the front of the queue.
while self.st1: self.st2.append(self.st1.pop())Pour st1 into st2. This reverses the order, so the oldest of the st1 items lands on top of st2.
return self.st2.pop()Hand out the front item. Safe because the problem promises the queue isn't empty here, so after the pour st2 has something.
return self.st2[-1]peek: same item, not removed.
return not self.st1 and not self.st2The queue is empty only if neither stack holds anything.

8Dry run: operation-sequence table

The teacher's sequence: push 1, push 2, push 3, pop, push 4, pop, pop, pop, then we add peek and empty checks.

#operationwhat happens insidest1 afterst2 afterreturnedmoves
1push 1append to st1[1][]–1
2push 2append to st1[1, 2][]–1
3push 3append to st1[1, 2, 3][]–1
4peekst2 empty → pour 3, 2, 1 → st2 = [3, 2, 1]; look at top[][3, 2, 1]16
5popst2 not empty → no pour; pop top[][3, 2]11
6push 4append to st1[4][3, 2]–1
7popst2 not empty → don't touch 4; pop top[4][3]21
8emptyst1 has 4, st2 has 3[4][3]False0
9popst2 not empty; pop top[4][]31
10popst2 empty → pour 4 → st2 = [4]; pop[][]43
11emptyboth empty[][]True0

The values came out 1, 2, 3, 4: the exact order they went in. That's FIFO ✓.

after step 3: st1
123
after step 4 pour: st2
321
after step 6: st1
4
after step 6: st2
32

At step 6 the queue (front → back) is 2, 3, 4: read st2 from top to bottom, then st1 from bottom to top.

9Complexity & remember (with a counted example)

Why the average is O(1): follow one item

Over its whole life, each item is touched at most 4 times: pushed onto st1, popped from st1, pushed onto st2, popped from st2. It never goes back from st2 to st1. So for any sequence of n operations, the total work is at most about 4n moves, which is O(1) per operation on average.

Counting it: push 1 … 100, then pop 100 times

Version 1 (costly push)Version 2 (lazy)
the 100 pushesthe k-th push moves k−1 items out and back: 4(k−1) + 1 moves. Sum for k = 1…100 = 4 × 4950 + 100 = 19,9001 move each = 100
the first pop1pour 100 items (200 moves) + 1 pop = 201
the other 99 pops1 each = 99st2 not empty, 1 each = 99
total20,000 moves400 moves = 4 per item

This is the teacher's point: in Version 1 we reshuffle on every push, so 100 pushes cost about 100 × 100. In Version 2 we reverse once when the first pop arrives, and that single pour serves the next 100 pops.

Remember Version 2push → st1 only. pop/peek → if st2 is empty, pour all of st1 into st2, then use st2's top. empty → both empty. Each item moves at most once from st1 to st2 → amortised O(1).

Part C · Revision page

Version 1: costly pushVersion 2: lazy transfer
where items live between callsonly st1 (reversed, oldest on top)st1 (newest on top) and st2 (oldest on top)
pushst1 → st2, push x, st2 → st1: O(n)append to st1: O(1)
pop / peekst1 top: O(1)if st2 empty, pour; st2 top: amortised O(1)
emptynot st1not st1 and not st2
100 pushes + 100 pops20,000 moves400 moves
role of st2a temporary holderthe "out" stack, keeps items between calls
If you remember only 5 lines 1. One stack can't give FIFO: the oldest is at the bottom, so you need a second stack to hold the items above it.
2. Moving all items from one stack to another reverses their order.
3. Version 1: reverse on every push so st1 always has the oldest on top. Push O(n).
4. Version 2: push to st1; pour into st2 only when st2 is empty; pop/peek from st2.
5. Every item moves st1 → st2 at most once, so the total is O(n) for n operations: amortised O(1).
Mistakes to avoid ✗ pouring st1 into st2 when st2 still has items (newer items jump ahead of older ones)
✗ checking only one stack in empty() for Version 2
✗ forgetting the pour step in peek() (st2 may be empty, so st2[-1] crashes)
✗ using pop(0) or st[0]: a stack can't touch its bottom
✗ saying "pop is O(n)" for Version 2 in an interview without adding "but amortised O(1)"

Interview tip from the teacher: companies like asking design questions (build a hash map, a linked list, a queue from stacks and the reverse). Show the simple version first, then explain why you moved the expensive work from push to a rare pour, and name it "amortised". That journey is what adds value.

test it yourself (paste under either version)
q = MyQueue()
q.push(1); q.push(2); q.push(3)
print(q.peek())    # 1
print(q.pop())     # 1
q.push(4)
print(q.pop())     # 2
print(q.empty())   # False
print(q.pop())     # 3
print(q.pop())     # 4
print(q.empty())   # True

Based on this video: Implement Queue using Stacks