Overview
A stack using queue(s) simulates LIFO behavior using only FIFO primitives (enqueue, dequeue, front/peek, empty, size). This construction is useful for reasoning about data structure equivalence, practicing amortized analysis, and in environments where only queue operations are allowed (e.g., restricted APIs, interview puzzles).
There are two classic approaches:
-
Two-queue strategy
- Costly
pushvariant: keep top at the front ofQ1so thatpopisO(1). - Costly
popvariant: keep items in arrival order; pop drains all but the last to a helper queue.
- Costly
-
One-queue rotation strategy
- After
push(x), rotate the queue soxmoves to the front. Thenpopis a singledequeue.
- After
Note
Throughout, assume queues are standard FIFO with
enqueue(x)at the back anddequeue()from the front, each inO(1)amortized time for a linked-list or circular-buffer implementation.
Structure Definition
State (two-queue version):
Q1: primary queue that always has the stack top at its front (for the costly-pushvariant).Q2: helper queue used duringpush(or duringpopin the costly-popvariant).
State (one-queue rotation version):
Q: a single queue; maintain the invariant that the most recently pushed element sits at the front by rotating after everypush.
Core Operations
A) Two-queue, costly push (fast pop)
Makes pop() and peek() O(1).
class StackUsingQueues:
Q1: Queue
Q2: Queue
method push(x):
Q2.enqueue(x)
while not Q1.empty():
Q2.enqueue(Q1.dequeue())
// swap names so Q1 has the new order
swap(Q1, Q2)
method pop() -> T:
if Q1.empty(): error Underflow
return Q1.dequeue()
method peek() -> T:
if Q1.empty(): error Underflow
return Q1.front()
method empty() -> bool:
return Q1.empty()Cost: push takes Θ(n) (moves all elements); pop/peek are Θ(1).
B) Two-queue, costly pop (fast push)
Keeps push cheap; pop moves n−1 items.
class StackUsingQueues:
Q1: Queue
Q2: Queue
method push(x):
Q1.enqueue(x) // O(1)
method pop() -> T:
if Q1.empty(): error Underflow
while Q1.size() > 1:
Q2.enqueue(Q1.dequeue())
x = Q1.dequeue() // last remaining is the top
swap(Q1, Q2) // Q1 becomes the new primary
return x
method peek() -> T:
if Q1.empty(): error Underflow
while Q1.size() > 1:
Q2.enqueue(Q1.dequeue())
x = Q1.front() // look at last
Q2.enqueue(Q1.dequeue()) // move it too
swap(Q1, Q2)
return x
method empty() -> bool:
return Q1.empty()Cost: push is Θ(1); pop/peek are Θ(n).
C) One-queue rotation (balanced; fast pop)
Rotate after each push so top becomes front.
class StackUsingOneQueue:
Q: Queue
method push(x):
Q.enqueue(x)
// rotate size-1 elements so x moves to the front
k = Q.size() - 1
for i in 1..k:
Q.enqueue(Q.dequeue())
method pop() -> T:
if Q.empty(): error Underflow
return Q.dequeue() // top at front
method peek() -> T:
if Q.empty(): error Underflow
return Q.front()
method empty() -> bool:
return Q.empty()Cost: push is Θ(n) due to the rotation; pop/peek are Θ(1). This mirrors the costly-push two-queue variant but needs only one queue.
Example (Stepwise)
Consider the one-queue rotation with operations: push(1), push(2), push(3), pop(), push(4), peek().
-
push(1):Q=[1](rotate 0). Top=1 at front. -
push(2): enqueue2→[1,2]; rotatesize−1=1→ dequeue1, enqueue1→[2,1]. Top=2 at front. -
push(3): enqueue3→[2,1,3]; rotate 2 →[3,2,1]. Top=3. -
pop()→ dequeue front → returns3;Q=[2,1]. Top=2. -
push(4): enqueue4→[2,1,4]; rotate 2 →[4,2,1]. Top=4. -
peek()→front()returns4.
Note
The invariant “top at front” keeps
popandpeektrivial. The rotation amortizes the reordering cost intopush.
Complexity and Performance
Let n be the number of elements at the moment of the operation.
| Strategy | push | pop | peek | Extra Space |
|---|---|---|---|---|
| Two-queue (costly push) | Θ(n) | Θ(1) | Θ(1) | O(n) across two queues |
| Two-queue (costly pop) | Θ(1) | Θ(n) | Θ(n) | O(n) across two queues |
| One-queue rotation | Θ(n) | Θ(1) | Θ(1) | O(n) in one queue |
-
Amortization: If your workload is pop-heavy (many pops per push), prefer the costly-push variants. If push-heavy, the costly-pop variant can win.
-
Space: All variants store the elements once (
Θ(n)total) plus queue metadata. Two-queue methods temporarily hold elements in both structures during transfers but keep overallΘ(n)capacity.
Tip
On bounded-capacity queues, check before transfers to avoid overflow during the move/rotate phases.
Implementation Details or Trade-offs
-
Queue choice: Use a linked queue or circular buffer with
O(1)operations. Ensuresize()isO(1)if you rely on it for rotation counts. -
Error semantics: Define underflow for
pop/peekon empty. Prefer returning status + out-param in C-like APIs or throwing domain-specific exceptions in higher-level languages. -
Performance profile:
-
One-queue minimizes bookkeeping (no swaps) but performs
size−1moves on eachpush. -
Two-queue versions add a
swap(Q1,Q2)step to keep invariants clean and code straightforward.
-
-
Stability of references: These implementations do not provide stable references to elements (queue nodes may move/rotate). If stability is required, use Stack Using Linked List.
-
Cache behavior: Circular-buffer queues are generally more cache-friendly than linked queues due to contiguous memory.
-
Concurrency: None of these are thread-safe without synchronization. Wrap with a mutex or use lock-free queues carefully (ABA hazards).
Warning
Cost blow-up surprises. It’s easy to flip the invariant and accidentally make both
pushandpopexpensive. Assert post-conditions (e.g., “top at front ofQ1”) after each operation in tests.
Practical Use Cases
-
Educational settings: Demonstrates how to compose data structures and reason about invariants and amortized costs.
-
API constraints: When only FIFO primitives are available, but LIFO behavior is required in a bounded scope.
-
Verification exercises: Good target for unit tests that validate underflow behavior, rotation counts, and equivalence with a reference stack.
Limitations / Pitfalls
Warning
Throughput vs simplicity. A native stack (array or linked) beats these constructions in constant factors and locality. Use the queue-based stack only when constrained or for pedagogy.
Warning
Large elements. Rotations/dequeues copy or move elements repeatedly; if payloads are large, store handles (indices/pointers) instead of full objects.
Warning
Capacity hysteresis. If the underlying queue grows/shrinks dynamically, frequent rotations can trigger resize thrash. Prefer geometric growth with hysteresis.
Summary
A stack can be implemented with queues by enforcing the invariant that the most recently pushed element sits at the front. The two-queue approach offers a choice between costly push (fast pop) and costly pop (fast push), while the one-queue rotation achieves the same effect with a single queue by rotating after each push. All variants are correct; choose based on your operation mix and simplicity needs, keeping an eye on data movement and queue performance characteristics.