Design with Stacks and Queues
This family asks you to build a primitive out of the wrong primitive: a queue from stacks, a stack from queues, a stack that also reports its minimum. They look like puzzles, and they are really testing two things:
- Can you carry auxiliary state alongside the data, rather than computing properties on demand?
- Can you reason about amortised cost — recognising that an occasional expensive operation is still per operation on average?
The second point is the real content. An operation that is usually free and occasionally can still be amortised, and being able to prove that is what the two-stack queue exists to elicit.
What you’ll learn
Section titled “What you’ll learn”- The paired-state trick: store
(value, min_so_far)instead of recomputing. - The two-stack queue, and the amortised argument that makes it .
- Why “only refill when empty” is essential, not an optimisation.
- Circular buffers — fixed capacity with modular index arithmetic.
- Three real LeetCode problems solved in the browser: 155, 232, 622.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”Every design in this page keeps a second piece of state alongside the stack — a running minimum, a second stack, a pair of indices. Watch the stack panel here: the input row is consumed left to right while the stack holds only what is still unresolved.
That framing is what the design problems build on: a Min Stack pairs each pushed value with the minimum at the time of pushing, so the extra state is popped in lockstep; the two-stack queue keeps an inbox and an outbox and moves items across only when the outbox runs dry. In both cases the second structure is maintained by the same push/pop rhythm you see here.
Trick 1 — paired state (Min Stack)
Section titled “Trick 1 — paired state (Min Stack)”The naive getMin scans the stack: . Keeping a single min variable
fails, because popping the minimum leaves you with no way to recover the
previous minimum.
The fix is to store, with every element, what the minimum was at the moment it was pushed:
class MinStack:
def __init__(self):
self.stack = [] # entries are (value, min_at_this_depth)
def push(self, val):
current_min = val if not self.stack else min(val, self.stack[-1][1])
self.stack.append((val, current_min))
def pop(self):
self.stack.pop()
def top(self):
return self.stack[-1][0]
def getMin(self):
return self.stack[-1][1] # O(1) -- already computedEvery operation is worst case, at the cost of extra space.
Trick 2 — the two-stack queue
Section titled “Trick 2 — the two-stack queue”A stack reverses order; two stacks reverse it twice, giving you back FIFO.
inboxreceives every push.outboxserves every pop and peek.- When
outboxis empty, pour the wholeinboxinto it — which reverses it, putting the oldest element on top.
class MyQueue:
def __init__(self):
self.inbox = []
self.outbox = []
def push(self, x):
self.inbox.append(x) # always O(1)
def _shift(self):
if not self.outbox: # ONLY when empty -- see below
while self.inbox:
self.outbox.append(self.inbox.pop())
def pop(self):
self._shift()
return self.outbox.pop()
def peek(self):
self._shift()
return self.outbox[-1]
def empty(self):
return not self.inbox and not self.outboxNote also that empty() must check both stacks. An element sitting in
inbox with an empty outbox still means the queue is non-empty.
Trick 3 — circular buffers
Section titled “Trick 3 — circular buffers”Fixed capacity with wraparound: keep a list of size k, a head index, and a
count.
class MyCircularQueue:
def __init__(self, k):
self.data = [0] * k
self.head = 0
self.count = 0
self.cap = k
def enQueue(self, value):
if self.count == self.cap:
return False
tail = (self.head + self.count) % self.cap # modular arithmetic
self.data[tail] = value
self.count += 1
return True
def deQueue(self):
if self.count == 0:
return False
self.head = (self.head + 1) % self.cap
self.count -= 1
return True
def Front(self):
return -1 if self.count == 0 else self.data[self.head]
def Rear(self):
return -1 if self.count == 0 else self.data[(self.head + self.count - 1) % self.cap]Dry run
Section titled “Dry run”The two-stack queue, counting the moves
Section titled “The two-stack queue, counting the moves”Eight operations on MyQueue. The column that matters is the last one — how many elements
crossed from inbox to outbox on this call.
| # | Call | inbox | outbox | Returns | Moves this call |
|---|---|---|---|---|---|
| 1 | push(1) | [1] | [] | — | 0 |
| 2 | push(2) | [1, 2] | [] | — | 0 |
| 3 | pop() | [] | [2] | 1 | 2 |
| 4 | push(3) | [3] | [2] | — | 0 |
| 5 | push(4) | [3, 4] | [2] | — | 0 |
| 6 | pop() | [3, 4] | [] | 2 | 0 |
| 7 | pop() | [] | [4] | 3 | 2 |
| 8 | pop() | [] | [] | 4 | 0 |
Total moves across all eight calls: 4 — exactly the number of pushes, no more. Two calls did work and six did , but no element was ever moved twice.
Step 6 is the one to point at in an interview. outbox is non-empty, so _shift returns
immediately even though inbox has two items waiting. Those items are not stale — they are
younger than 2, and FIFO says they must not be served yet. The “only when empty” guard is
what enforces the ordering and what bounds the total work; it is one condition doing both jobs.
Step 3 also shows the reversal: inbox was [1, 2] with 2 on top, and after pouring,
outbox is [2, 1] with 1 on top — popped immediately as the return value, leaving [2].
Two reversals restore the original order.
The circular queue wrapping around
Section titled “The circular queue wrapping around”Capacity 3, and the interesting moment is the enqueue after a dequeue, when tail wraps to
index 0 while head sits at 1.
| Call | data | head | count | derived tail | Returns | Front / Rear |
|---|---|---|---|---|---|---|
enQueue(1) | [1, 0, 0] | 0 | 1 | 0 | True | 1 / 1 |
enQueue(2) | [1, 2, 0] | 0 | 2 | 1 | True | 1 / 2 |
enQueue(3) | [1, 2, 3] | 0 | 3 | 2 | True | 1 / 3 |
enQueue(4) | [1, 2, 3] | 0 | 3 | 2 | False | 1 / 3 |
deQueue() | [1, 2, 3] | 1 | 2 | 2 | True | 2 / 3 |
enQueue(5) | [5, 2, 3] | 1 | 3 | 0 | True | 2 / 5 |
deQueue() | [5, 2, 3] | 2 | 2 | 0 | True | 3 / 5 |
deQueue() | [5, 2, 3] | 0 | 1 | 0 | True | 5 / 5 |
deQueue() | [5, 2, 3] | 1 | 0 | — | True | -1 / -1 |
deQueue() | [5, 2, 3] | 1 | 0 | — | False | -1 / -1 |
Three things the trace makes concrete:
datais never cleared. After the last dequeue the array still reads[5, 2, 3], and that is correct —count == 0is the only thing that makes the queue empty. Zeroing slots on dequeue is wasted work, and reading a slot outside the live window is the actual bug.- The wrap is invisible in the state. At
enQueue(5),head = 1andcount = 2, sotail = (1 + 2) % 3 = 0. Nothing special-cased it; the modulo did all of it. - Rows 4 and 9 are the ambiguity that
countremoves. AfterenQueue(3)the queue is full withhead = 0; after the third dequeue it is empty withhead = 1— but run one more cycle and the pointers coincide. Ahead/tailpair giveshead == tailfor both full and empty and cannot tell them apart.countdistinguishes 3 from 0 with no extra slot and no flag.
Min Stack with duplicate minima
Section titled “Min Stack with duplicate minima”Push 5, 3, 7, 3, then pop twice. Each entry carries the minimum at its own depth.
| Call | Stack (value, min) | getMin() |
|---|---|---|
push(5) | (5,5) | 5 |
push(3) | (5,5) (3,3) | 3 |
push(7) | (5,5) (3,3) (7,3) | 3 |
push(3) | (5,5) (3,3) (7,3) (3,3) | 3 |
pop() | (5,5) (3,3) (7,3) | 3 |
pop() | (5,5) (3,3) | 3 |
The duplicate 3 is recorded twice, once at depth 2 and once at depth 4. That redundancy is
the point: after the first pop the minimum is still 3, because the deeper copy survives. This is
the case that breaks the space-optimised second-stack variant when it is written with a strict
< — the duplicate never gets pushed to the min stack, so the first pop removes the only copy
and getMin starts reporting 5 while a 3 is still in the main stack. Use <=.
Complexity
Section titled “Complexity”| Design | Per-operation cost | Space |
|---|---|---|
| Min Stack (paired) | worst case | |
| Min Stack (second stack) | worst case | , smaller in practice |
| Two-stack queue | amortised | |
| Stack from one queue | push or pop | |
| Circular queue | worst case | fixed |
The variant map
Section titled “The variant map”| Variant | The auxiliary state | Canonical problem |
|---|---|---|
| Stack + minimum | (value, min_so_far) pairs | 155 |
| Queue from stacks | Two stacks, refill on empty | 232 |
| Stack from queues | One queue, rotate on push | 225 |
| Fixed-capacity queue | Array + head + count | 622 · 641 |
| Max in a sliding window | A monotonic deque | 239 |
| Stack with increment | Lazy pending increments per depth | 1381 |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”LC 155 — Min Stack · Medium
Section titled “LC 155 — Min Stack · Medium”Problem. Design a stack supporting push, pop, top and getMin, each
in time.
Constraints. -2^31 <= val <= 2^31 - 1, up to 3 * 10^4 calls, and pop,
top and getMin are only called on a non-empty stack.
Examples. push(-2), push(0), push(-3), getMin() gives -3,
pop(), top() gives 0, getMin() gives -2
Editorial — approach, complexity, follow-ups
Precompute rather than query. At push time you already know the minimum of everything currently on the stack, so record it alongside the new value.
Time worst case for every operation. Space .
The reason a single min variable fails is worth stating explicitly: the
minimum is not a property of the stack’s top, it is a property of the whole
stack, and popping changes it in a way you cannot undo without history.
Attaching the answer to each depth makes the history implicit in the structure.
The test sequence checks exactly this: after popping -3, getMin must return
-2 — the minimum of what remains — not -3, and not a rescanned value.
Follow-ups you should expect:
- “Reduce the space.” A second stack holding only new minima, pushed when
val <= current_minand popped when the popped value equals its top. Use<=, or duplicate minima break it. - “A max stack too?” Store a triple, or keep a symmetric second stack.
- “
getMinon an empty stack?” LC 155 guarantees it will not happen; a robust version raises or returns a sentinel — say which you would choose. - “Increment the bottom
kelements (LC 1381)?” Keep a parallel array of pending increments and push the pending value down on pop, so the update is rather than .
LC 232 — Implement Queue using Stacks · Easy
Section titled “LC 232 — Implement Queue using Stacks · Easy”Problem. Implement a FIFO queue using only two stacks. Support push,
pop, peek and empty. Each operation must be amortised.
Constraints. 1 <= x <= 9, up to 100 calls, and pop/peek are only
called on a non-empty queue.
Examples. push(1), push(2), peek() gives 1, pop() gives 1,
empty() gives False
Editorial — approach, complexity, follow-ups
A stack reverses; two stacks reverse twice and restore FIFO order. Pushes go to
inbox; pops come from outbox, which is refilled by pouring inbox into it
(reversing it in the process).
Time for push and empty; amortised for pop and
peek, with an worst case on the refill. Space .
The amortised proof, which is the point of the problem: each element is
pushed to inbox once, moved to outbox at most once, and popped from outbox
at most once. So a sequence of n operations performs at most stack
operations in total — each on average.
The refill-only-when-empty condition is what guarantees the “at most once”.
Refilling while outbox still has items would also be wrong, not merely
slow: pouring newer elements on top of older ones destroys the FIFO order.
empty() checking both stacks is the small detail that a quick implementation
misses — after push(1) with nothing shifted yet, outbox is empty but the
queue is not.
Follow-ups you should expect: “Prove the amortised bound” — the counting
argument above. “Implement a stack using queues (LC 225)?” — asymmetric; one
side must be , because a single queue gives you nowhere to reverse into.
“What if you could use only one stack?” — possible with recursion, using the
call stack as the second stack. “Make pop worst case?” — not with this
design; you would need a real deque.
LC 622 — Design Circular Queue · Medium
Section titled “LC 622 — Design Circular Queue · Medium”Problem. Implement a circular queue of fixed size k with enQueue,
deQueue, Front, Rear, isEmpty and isFull. enQueue and deQueue
return a boolean indicating success; Front and Rear return -1 when empty.
Constraints. 1 <= k <= 1000, 0 <= value <= 1000, up to 3000 calls.
Examples. With k = 3: enQueue(1) gives True, enQueue(2) gives
True, enQueue(3) gives True, enQueue(4) gives False (full),
Rear() gives 3, isFull() gives True, deQueue() gives True,
enQueue(4) gives True, Rear() gives 4
Editorial — approach, complexity, follow-ups
A fixed array used cyclically. head marks the front; the tail is derived as
(head + count) % cap, so it never needs storing.
Time worst case for every operation. Space , fixed and allocated once.
The design decision that matters is count over tail. With head and tail
alone, an empty queue and a full queue both satisfy head == tail, so you must
either sacrifice a slot (capacity k needs k+1 storage) or carry an extra
boolean. Storing the count removes the ambiguity and makes isEmpty and
isFull one comparison each.
The test sequence deliberately wraps: after filling all three slots and
dequeuing one, enQueue(4) must write into the freed slot at index 0 while
head points at index 1. Rear() then gives 4 and Front() gives 2,
which only works if the modular arithmetic is right.
Returning False rather than raising on a full enQueue is part of the
specified interface — a small reminder to implement the contract you were given
rather than the one you would have designed.
Follow-ups you should expect: “Why not collections.deque?” — a deque
with maxlen silently discards on overflow instead of refusing, so it does
not meet this spec; and the point is to implement the mechanics. “A circular
deque (LC 641)?” — add insertFront, which decrements head modulo the
capacity. “Make it resizable?” — on full, allocate a bigger array and copy in
logical order; amortised , exactly how dynamic arrays grow. “Thread-safe
ring buffer?” — the classic single-producer/single-consumer lock-free
structure, which is where this design is used in practice.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 225Implement Stack using QueueseasyAsymmetric: rotate on push, so one side is $O(n)$
- 232Implement Queue using StackseasyRefill only when empty -- the amortised argument
- 155Min StackmediumPair each value with the minimum at its depth
- 622Design Circular Queuemedium`head` + `count`, not `head` + `tail`
- 641Design Circular Dequemedium622 plus `insertFront`, decrementing `head` modulo capacity
- 1381Design a Stack With Increment OperationmediumLazy pending increments make `increment` $O(1)$ instead of $O(k)$
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Amortised or worst case?” | Precision | The two-stack queue is amortised, worst case for one pop; Min Stack is worst case |
| “Prove the amortised bound” | Rigour | Each element moves from inbox to outbox exactly once, so n operations cost total |
“Why not a single min variable?” | The paired-state insight | Popping the minimum leaves no record of the previous one |
| “Reduce Min Stack’s space” | Optimisation | A second stack holding only new minima, using <= so duplicates are handled |
| “Why can’t a stack-from-queues be both ways?” | Understanding the asymmetry | One queue gives no second place to reverse into, so one operation must rotate |
“Why head + count?” | Design judgement | head + tail makes full and empty indistinguishable |
| “Make it thread-safe” | Production thinking | Locks per operation, or a lock-free ring buffer for single producer/consumer |
Edge-case checklist
Section titled “Edge-case checklist”- Operations on an empty structure — LC 155 and 232 guarantee they will not
happen; LC 622 requires
-1/False. Implement the stated contract. - Duplicate minima (LC 155) —
push(1), push(1), pop(); the case that breaks a strict-<optimisation. - Negative values — do not initialise a minimum to
0. empty()with items only ininbox(LC 232) — must check both stacks.- Full queue (LC 622) —
enQueuereturnsFalse, not an exception. - Wraparound (LC 622) — dequeue then enqueue so the tail passes index 0.
- Capacity 1 — a single slot that is alternately full and empty.
peeknot consuming — apeekthat pops is a common slip.
Self-check
Section titled “Self-check”-
In the two-stack queue, why does `_shift` refill `outbox` only when it is empty?
Two separate failures in one guard. Correctness: items still in `outbox` are older than everything in `inbox`, so pouring on top would serve younger items first. Cost: the guard is what makes each element move exactly once in its lifetime, giving n operations at most n moves. Refill unconditionally and a single element can be moved on every call -- O(n) per operation, not amortised anything.
pch.quizShowAnswer
B — Refilling while `outbox` is non-empty would both break FIFO order and make every operation O(n) — Two separate failures in one guard. Correctness: items still in `outbox` are older than everything in `inbox`, so pouring on top would serve younger items first. Cost: the guard is what makes each element move exactly once in its lifetime, giving n operations at most n moves. Refill unconditionally and a single element can be moved on every call -- O(n) per operation, not amortised anything.
-
You claim your two-stack queue is O(1). The interviewer asks you to be precise. What is the honest answer?
The refilling `pop` moves the whole inbox and is genuinely O(n). What is bounded is the total: across n operations the refills move at most n elements, so the average is constant. LC 232 asks for amortised O(1) precisely because it wants this design. Quoting flat O(1) is wrong and interviewers do notice.
pch.quizShowAnswer
B — O(1) amortised; a single `pop` is O(n) worst case when it triggers the refill — The refilling `pop` moves the whole inbox and is genuinely O(n). What is bounded is the total: across n operations the refills move at most n elements, so the average is constant. LC 232 asks for amortised O(1) precisely because it wants this design. Quoting flat O(1) is wrong and interviewers do notice.
-
Min Stack: you store only a single `min` variable instead of pairing each element with its minimum. What breaks?
Push 5 then 3, so `min` is 3. Pop. The answer should return to 5, but 5 was overwritten and nothing remembers it -- you would have to rescan the stack, which is the O(n) you were avoiding. Pairing each element with the minimum at its own depth means popping automatically uncovers the correct earlier minimum.
pch.quizShowAnswer
B — `getMin` returns a stale value after popping the current minimum, because the previous minimum was never recorded — Push 5 then 3, so `min` is 3. Pop. The answer should return to 5, but 5 was overwritten and nothing remembers it -- you would have to rescan the stack, which is the O(n) you were avoiding. Pairing each element with the minimum at its own depth means popping automatically uncovers the correct earlier minimum.
-
In the space-optimised Min Stack (a second stack holding only the minima), why must the push condition be `<=` rather than `<`?
Push 3, then 3 again. With strict `<` the second 3 is not recorded. Now pop once: the popped value equals the min-stack top, so the min stack pops too -- and the surviving 3 in the main stack has no representative. `getMin` jumps to whatever is below. With `<=` both copies are recorded and the pops stay in lockstep.
pch.quizShowAnswer
B — With `<`, a duplicated minimum is recorded once but popped twice, so `getMin` reports a value that is too large while the real minimum is still in the stack — Push 3, then 3 again. With strict `<` the second 3 is not recorded. Now pop once: the popped value equals the min-stack top, so the min stack pops too -- and the surviving 3 in the main stack has no representative. `getMin` jumps to whatever is below. With `<=` both copies are recorded and the pops stay in lockstep.
-
A circular queue stores `head` and `tail` instead of `head` and `count`. What goes wrong?
That collision is the whole reason ring buffers are fiddly. The classic workarounds are to waste one slot (full means the next tail would land on head) or to carry a boolean flag. Storing `count` removes the ambiguity outright: empty is `count == 0`, full is `count == cap`, and the tail is derived.
pch.quizShowAnswer
B — Full and empty both satisfy `head == tail`, so the two states are indistinguishable — That collision is the whole reason ring buffers are fiddly. The classic workarounds are to waste one slot (full means the next tail would land on head) or to carry a boolean flag. Storing `count` removes the ambiguity outright: empty is `count == 0`, full is `count == cap`, and the tail is derived.
-
Implementing a stack from a single queue: what is the cost profile?
Push, then rotate the other n-1 elements to the back so the just-pushed element sits at the front: O(n) push, O(1) pop. Or push cheaply and rotate at pop time instead. Either way the cost is paid on one side and it is genuinely O(n) per call -- unlike the two-stack queue, no amortisation saves you, because the rotation repeats on every operation.
pch.quizShowAnswer
B — O(n) on one of the two operations -- you rotate the queue so the newest element is at the front — Push, then rotate the other n-1 elements to the back so the just-pushed element sits at the front: O(n) push, O(1) pop. Or push cheaply and rotate at pop time instead. Either way the cost is paid on one side and it is genuinely O(n) per call -- unlike the two-stack queue, no amortisation saves you, because the rotation repeats on every operation.
Recall card
Section titled “Recall card”- The shape — a design problem is a data structure plus one extra piece of state maintained by the same push/pop rhythm. Find the extra state and the problem is solved.
- Min Stack — store
(value, min_at_this_depth). Popping restores the earlier minimum for free. worst case, space. Second-stack variant: push on<=, never<. - Two-stack queue —
inboxtakes pushes,outboxserves pops, refill only whenoutboxis empty. Each element moves exactly once, so amortised, worst case on a singlepop. Say “amortised” out loud. empty()must check both stacks — an item ininboxwith an emptyoutboxis still an item.- Circular queue — keep
headandcount, derive the tail. Ahead/tailpair cannot distinguish full from empty. - Never clear slots on dequeue.
countdefines what is live; the stale bytes are harmless. - Stack from one queue is on one side, with no amortisation to rescue it — the rotation repeats every call.
- These problems test auxiliary state and amortised reasoning, not algorithms.
- Min Stack: store
(value, min_at_this_depth). The minimum is a property of the whole stack, so attach the answer to each depth and popping restores it for free. - Two-stack queue:
inboxfor pushes,outboxfor pops, and refill only whenoutboxis empty. Each element moves once, so it is amortised — be ready to prove it, and to say “amortised” rather than plain . - Circular buffer: track
headandcount, neverheadandtail, or full and empty become indistinguishable. - A stack from queues is inherently asymmetric — one operation must be .
- Deferring work until it is observed (LC 1381) turns updates into .
Next: Design Iterators and Flatteners — exposing a traversal one element at a time, and doing it lazily.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading