Implement Queue using Stacks: The Lazy Two-Stack Transfer
How to implement a queue using stacks: the two-stack lazy transfer solution in JavaScript, with an amortized O(1) proof, walkthrough, and common mistakes.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
A stack hands you back the newest thing you gave it. A queue must hand back the oldest. Implement Queue using Stacks asks you to fake the second behavior using only the first — and the answer hides one of the most useful ideas in data-structure design: a stack reverses order, so two stacks restore it.
The problem is rated easy, but the follow-up is where interviews are won: make every operation amortized O(1). Most candidates reach for a solution that shuffles every element on every push; the good one moves each element at most once — and explaining why that's O(1) on average is the real test.
The same lazy-transfer instinct — do the expensive reorganization only when forced to, then ride it for free — shows up in buffered I/O, functional queues, and dynamic arrays.
The problem
Design a first-in-first-out (FIFO) queue using only two stacks. Your MyQueue class must support the standard queue API:
push(x)— enqueuexat the backpop()— remove and return the front elementpeek()— return the front element without removing itempty()— returntrueif the queue holds nothing
You may only use standard stack operations: push to top, pop from top, peek at top, and size/empty checks. No indexing into the middle, no shift().
Input: push(1), push(2), peek(), pop(), empty()
Output: 1, 1, false
// peek() sees 1 (oldest), pop() removes 1, queue still holds 2The constraint is the whole problem. An array's shift() would trivially give FIFO — but you're limited to LIFO primitives, so the order has to be manufactured.
The brute force baseline
The direct approach keeps one stack permanently in queue order: on every push, drain the stack into a helper, drop the new element at the bottom, and drain everything back.
class MyQueue {
s1 = []; // always kept in queue order, front on top
s2 = []; // scratch space
push(x) {
while (this.s1.length) this.s2.push(this.s1.pop()); // empty s1
this.s1.push(x); // x goes to the bottom
while (this.s2.length) this.s1.push(this.s2.pop()); // restore the rest on top
}
pop() { return this.s1.pop(); }
peek() { return this.s1[this.s1.length - 1]; }
empty() { return this.s1.length === 0; }
}pop and peek are O(1), but every push moves all n existing elements twice — out and back. A workload of n pushes costs O(n²) total stack operations, and the work is wasted: the queue gets reorganized on every push even if nobody ever asks for the front. Interviewers accept this as a baseline and immediately ask you to beat it.
The key insight: transfer lazily, reverse once
This is the Two Stacks pattern. Split the responsibilities:
inStackreceives everypush. Newest on top, oldest at the bottom — LIFO order.outStackserves everypopandpeek. When it has elements, its top is exactly the queue front.
The bridge between them is one observation: draining a stack into another stack reverses it. Pop inStack repeatedly and push each element onto outStack, and the element at the bottom of inStack — the oldest one — lands on top of outStack. One reversal converts LIFO into FIFO.
The second half of the insight is when to transfer: only when outStack is empty and a front-access is needed. Never earlier. Elements already sitting in outStack are older than anything in inStack, so they must all leave first. Transferring while outStack still has elements would bury older elements under newer ones and corrupt the order.
This laziness is what makes the cost analysis work. Each element makes a fixed journey: pushed onto inStack once, moved to outStack at most once, popped off outStack at most once. Three stack operations per element, ever — so any sequence of n operations costs O(n) total, amortized O(1) each.
The optimal solution
This is the exact algorithm the visualizer steps through:
class MyQueue {
inStack = [];
outStack = [];
push(x) { this.inStack.push(x); }
transfer() {
if (this.outStack.length === 0) {
while (this.inStack.length > 0) {
this.outStack.push(this.inStack.pop());
}
}
}
pop() { this.transfer(); return this.outStack.pop(); }
peek() { this.transfer(); return this.outStack[this.outStack.length - 1]; }
empty() { return !this.inStack.length && !this.outStack.length; }
}Three details carry the correctness:
transfer()guards itself withoutStack.length === 0. Calling it is always safe; it only does work whenoutStackhas run dry.pop()andpeek()both calltransfer()first, so the front element is always on top ofoutStackby the time they read it.empty()checks both stacks — after a transfer, everything lives inoutStack; right after pushes, everything lives ininStack.
Walkthrough: push(1), push(2), peek(), pop(), empty()
Stacks are written bottom → top, so the rightmost value is the top.
| Step | Operation | inStack | outStack | Returns |
|---|---|---|---|---|
| 1 | push(1) | [1] | [] | — |
| 2 | push(2) | [1, 2] | [] | — |
| 3 | peek() → outStack empty → transfer | [] | [2, 1] | — |
| 4 | peek() reads outStack top | [] | [2, 1] | 1 |
| 5 | pop() → outStack non-empty, no transfer | [] | [2] | 1 |
| 6 | empty() → both stacks checked | [] | [2] | false |
Step 3 is the whole trick in one row: inStack held [1, 2] with 2 on top, and the drain popped 2 first, then 1 — so outStack ends as [2, 1] with 1 on top. The oldest element surfaced exactly where a queue front should be. Step 5 shows the payoff of laziness: the pop rides the earlier transfer for free.
Complexity
| Approach | push | pop / peek | Space | Why |
|---|---|---|---|---|
| Costly push (eager reshuffle) | O(n) | O(1) | O(n) | every push moves all n elements out and back |
| Two stacks, lazy transfer | O(1) | amortized O(1), worst case O(n) | O(n) | each element crosses stacks at most once in its lifetime |
The amortized argument, stated the way interviewers want to hear it: charge each element 3 stack operations at push time — entering inStack, its single possible transfer, its final pop. Every later operation spends only pre-paid credit, so n operations never exceed 3n stack ops total. A single pop() after n pushes does O(n) work, but that spike happens at most once per batch of pushes.
Common mistakes
- Transferring while
outStackisn't empty. This is the classic order-corrupting bug. IfoutStack = [2, 1]and you drain a freshinStack = [3]on top,outStackbecomes[2, 1, 3]— andpop()now returns 3 before 1. Only transfer whenoutStackhas fully emptied. - Transferring on every push. That's the brute force wearing a disguise. Correct output, O(n) per push, and you'll be asked why you didn't hit the amortized O(1) follow-up.
empty()checking only one stack. Afterpush(1), pop(), push(2), element 2 sits ininStackwhileoutStackis empty. Checking onlyoutStackreports an empty queue that isn't. Both stacks must be empty.- Using
shift()or index access.array.shift()is a dequeue — using it means you never implemented anything. The exercise only allows push/pop/peek on the two stacks. - Quoting amortized O(1) as worst-case O(1). A single pop can trigger a full O(n) transfer. The average over any sequence is O(1); one individual operation is not guaranteed to be.
Where this pattern shows up next
Once "a stack reverses order" clicks, a whole family of stack problems opens up:
- Remove Outermost Parentheses — depth tracking, the simplest stack idea, without even materializing the stack.
- Evaluate Reverse Polish Notation — a stack as an evaluation machine: operands wait, operators consume.
- Next Greater Element I — the monotonic stack, where elements wait on the stack until their answer arrives.
- Daily Temperatures — the same monotonic wait-and-resolve idea at scale.
You can also step through it interactively and watch the transfer fire: the two towers side by side, elements draining from inStack to outStack, and the oldest value surfacing on top exactly when peek() needs it.
FAQ
Why is implementing a queue with two stacks amortized O(1)?
Because each element makes a bounded journey: pushed onto inStack once, transferred to outStack at most once, popped at most once — three stack operations for its entire lifetime. Any sequence of n queue operations therefore performs at most about 3n stack operations, which averages to O(1) each. A single pop can still cost O(n) when it triggers a transfer, but that expensive transfer only happens after enough cheap pushes have paid for it.
When exactly should elements move from inStack to outStack?
Only when outStack is empty and a pop or peek needs the front element. Everything already in outStack is older than everything in inStack, so outStack must fully drain before the inStack batch matters. Transferring early — while outStack still holds elements — stacks newer elements on top of older ones and breaks FIFO order. The lazy rule is both the correctness condition and the source of the amortized O(1) bound.
Does peek() change the state of the queue?
It can move elements between the two stacks, but it never changes the queue's contents or order. If outStack is empty, peek() triggers the same transfer pop() would, then reads outStack's top without removing it. Two consecutive peeks return the same value — the transfer is an internal reorganization, invisible through the queue API.
Why does empty() need to check both stacks?
Because elements can legitimately live in either stack depending on transfer timing. Right after a series of pushes, everything is in inStack; right after a transfer, everything is in outStack. The queue is empty only when both stacks are empty — checking just one produces false "empty" reports and is one of the most common bugs in this problem.
Is there a mirror problem that implements a stack using queues?
Yes — LeetCode 225, Implement Stack using Queues, flips the exercise. The standard trick there is rotation: after enqueueing a new element, dequeue and re-enqueue the previous size-1 elements so the newest one reaches the front, making push O(n) but pop and top O(1). The asymmetry is instructive: stacks simulate a queue with an amortized-O(1) lazy reversal, while queues simulate a stack only with an eager O(n) rotation, because a single stack drain reverses order but queue rotations never do.
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.