← Back to DevBytes

Implement Stack using Queues: Multiple Solutions and Complexity Analysis

Implementing a Stack Using Queues: Core Concepts and Problem Statement

The challenge of implementing a stack using queues is a classic problem in data structure design that tests your understanding of fundamental linear data structures. A stack follows the Last-In-First-Out (LIFO) principle — the most recently added element is the first one to be removed. A queue follows the First-In-First-Out (FIFO) principle — elements are removed in the same order they were added. These two behaviors are fundamentally opposite, which makes the implementation both interesting and non-trivial.

When you implement a stack using queues, you are restricted to using only queue operations: enqueue (add to rear), dequeue (remove from front), peek_front (inspect front), is_empty, and size. You cannot directly access elements in the middle or reverse the order without going through the queue interface. This constraint forces you to think creatively about how to simulate LIFO behavior with FIFO primitives.

Why This Problem Matters

This problem is far more than an interview puzzle. Understanding how to simulate one data structure with another builds deep intuition about:

It also appears frequently in technical interviews at companies like Google, Amazon, and Microsoft, making it a high-value topic to master thoroughly.


Approach 1: Two Queues — Push Operation Costly (O(n))

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

In this approach, we use two queues and keep the newly pushed element always at the front of the primary queue. Every push operation rearranges elements to maintain the stack order. Pop becomes trivially O(1) because the top of the stack is sitting right at the front of the primary queue.

How It Works

We maintain two queues: q1 (primary) and q2 (temporary helper). The invariant is that q1 always holds elements in stack order — the top of the stack is at the front of q1. Here is the step-by-step logic:

Code Implementation (Python)

from collections import deque

class StackPushCostly:
    def __init__(self):
        self.q1 = deque()   # primary queue: maintains stack order
        self.q2 = deque()   # temporary helper queue

    def push(self, x: int) -> None:
        # Step 1: Move all elements from q1 to q2
        while self.q1:
            self.q2.append(self.q1.popleft())
        
        # Step 2: Add new element to now-empty q1
        self.q1.append(x)
        
        # Step 3: Move everything back from q2 to q1
        while self.q2:
            self.q1.append(self.q2.popleft())

    def pop(self) -> int:
        if self.empty():
            raise IndexError("pop from empty stack")
        return self.q1.popleft()

    def top(self) -> int:
        if self.empty():
            raise IndexError("peek from empty stack")
        return self.q1[0]

    def empty(self) -> bool:
        return len(self.q1) == 0

    def size(self) -> int:
        return len(self.q1)

Complexity Analysis

This approach makes sense when your application is pop-heavy — you do many more pop/top operations than pushes, so you want those to be lightning fast and are willing to pay the cost on push.


Approach 2: Two Queues — Pop/Top Operation Costly (O(n))

This approach flips the cost model. We keep the queue in standard FIFO order and only rearrange when we need to access the top element. Push is now O(1), while pop and top become O(n).

How It Works

Again we use q1 and q2, but the invariant changes. Elements sit in q1 in FIFO order (oldest element at front). The last inserted element — the stack's top — sits at the rear. To pop or peek, we must drain all elements except the last one into q2, extract that last element, then swap the queue references (or move elements back).

Code Implementation (Python)

from collections import deque

class StackPopCostly:
    def __init__(self):
        self.q1 = deque()   # primary queue in FIFO order
        self.q2 = deque()   # temporary helper

    def push(self, x: int) -> None:
        # O(1): just add to rear
        self.q1.append(x)

    def pop(self) -> int:
        if self.empty():
            raise IndexError("pop from empty stack")
        
        # Move all except the last element from q1 to q2
        while len(self.q1) > 1:
            self.q2.append(self.q1.popleft())
        
        # The last element in q1 is the stack top
        top_value = self.q1.popleft()
        
        # Swap q1 and q2 so q1 holds remaining elements
        self.q1, self.q2 = self.q2, self.q1
        
        return top_value

    def top(self) -> int:
        if self.empty():
            raise IndexError("peek from empty stack")
        
        # Same drain logic but preserve the last element
        while len(self.q1) > 1:
            self.q2.append(self.q1.popleft())
        
        # Peek at the last element
        top_value = self.q1[0]
        
        # Move it to q2 as well, then swap
        self.q2.append(self.q1.popleft())
        self.q1, self.q2 = self.q2, self.q1
        
        return top_value

    def empty(self) -> bool:
        return len(self.q1) == 0

    def size(self) -> int:
        return len(self.q1)

Complexity Analysis

This approach is ideal for push-heavy workloads. If your application pushes frequently but rarely pops (e.g., building an audit log where you occasionally need the most recent entry), the O(1) push keeps insertion fast.


Approach 3: Single Queue — Elegant Rotation (Pop Costly, O(n))

Can we simplify further and use just one queue? Yes. The trick is to rotate the queue: repeatedly dequeue from the front and enqueue to the rear, effectively cycling elements until the desired element reaches the front. This is conceptually cleaner and uses less space overhead.

How It Works

We maintain a single queue q. On each push, we add the new element and then immediately rotate the queue so that this new element ends up at the front. This means after every push, the queue maintains stack order (top at front). Alternatively, we can keep FIFO order and rotate only on pop/top — both variants work. The rotation-based push variant is particularly elegant.

Variant A — Push rotates (push O(n), pop O(1)): After enqueuing the new element, rotate all previous elements behind it by cycling them front-to-rear. The queue size minus one rotations bring the new element to the front.

Variant B — Pop rotates (push O(1), pop O(n)): Keep FIFO order. On pop, cycle elements from front to rear until the last inserted element reaches the front, then dequeue it. The number of rotations equals the queue size minus one.

Below is the code for Variant A (push rotates), which is the most commonly taught single-queue solution.

Code Implementation — Single Queue with Push Rotation (Python)

from collections import deque

class StackSingleQueue:
    def __init__(self):
        self.q = deque()

    def push(self, x: int) -> None:
        # Step 1: Add new element at rear
        self.q.append(x)
        
        # Step 2: Rotate all previous elements behind it
        # Rotate size-1 times to bring new element to front
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())

    def pop(self) -> int:
        if self.empty():
            raise IndexError("pop from empty stack")
        # Front is always the stack top after push rotations
        return self.q.popleft()

    def top(self) -> int:
        if self.empty():
            raise IndexError("peek from empty stack")
        return self.q[0]

    def empty(self) -> bool:
        return len(self.q) == 0

    def size(self) -> int:
        return len(self.q)

Code Implementation — Single Queue with Pop Rotation (Python)

from collections import deque

class StackSingleQueuePopRotation:
    def __init__(self):
        self.q = deque()

    def push(self, x: int) -> None:
        # O(1): simply append
        self.q.append(x)

    def pop(self) -> int:
        if self.empty():
            raise IndexError("pop from empty stack")
        
        # Rotate: move front elements to rear until last element is at front
        # We need size-1 rotations
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())
        
        # Now the last element (stack top) is at the front
        return self.q.popleft()

    def top(self) -> int:
        if self.empty():
            raise IndexError("peek from empty stack")
        
        # Same rotation to bring last element to front
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())
        
        top_val = self.q[0]
        # Rotate once more to restore order (move it to rear)
        self.q.append(self.q.popleft())
        return top_val

    def empty(self) -> bool:
        return len(self.q) == 0

    def size(self) -> int:
        return len(self.q)

Complexity Analysis (Variant A — Push Rotation)

Complexity Analysis (Variant B — Pop Rotation)

The single-queue approach is memory-efficient and demonstrates clever use of the queue's own operations to simulate reverse-order access without external storage.


Approach 4: Optimized Single Queue — Tracking Size for O(1) Top (Hybrid)

We can optimize the pop-rotation variant to achieve O(1) top while keeping push O(1) and pop O(n). The idea is to maintain a last_element variable that tracks the most recently pushed value. This gives us instant access to the stack top without any rotation at all for top() calls.

Code Implementation (Python)

from collections import deque

class StackOptimized:
    def __init__(self):
        self.q = deque()
        self.last = None   # tracks the most recently pushed element

    def push(self, x: int) -> None:
        self.q.append(x)
        self.last = x      # update top tracker

    def pop(self) -> int:
        if self.empty():
            raise IndexError("pop from empty stack")
        
        # Rotate to bring the last element to front
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())
        
        # Dequeue the top
        popped = self.q.popleft()
        
        # Update last to the new rear element (the one before pop)
        # After rotation, the rear is the element that was just before top
        if self.q:
            # The new last is the rear of the rotated queue
            # We need to peek at rear; deque supports q[-1] in Python
            self.last = self.q[-1]
        else:
            self.last = None
        
        return popped

    def top(self) -> int:
        if self.empty():
            raise IndexError("peek from empty stack")
        return self.last   # O(1)!

    def empty(self) -> bool:
        return len(self.q) == 0

    def size(self) -> int:
        return len(self.q)

This hybrid approach gives you the best of both worlds when your workload involves frequent peeks at the top without popping. The top() operation drops from O(n) to O(1), while push remains O(1) and pop remains O(n).


Complexity Summary Table

Here is a comparison of all approaches so you can choose the right one based on your expected operation mix:

Approach Push Pop Top Queues Used
Two Queues — Push Costly O(n) O(1) O(1) 2
Two Queues — Pop Costly O(1) O(n) O(n) 2
Single Queue — Push Rotation O(n) O(1) O(1) 1
Single Queue — Pop Rotation O(1) O(n) O(n) 1
Hybrid — Last Tracker O(1) O(n) O(1) 1

Best Practices and Practical Guidance

Choosing the Right Approach

Your choice should be driven by the expected ratio of push to pop/top operations in your specific use case:

Production Considerations

Common Pitfalls


Putting It All Together: Complete Test Harness

Here is a complete, runnable test that validates any of the above implementations against the expected stack behavior:

def test_stack_implementation(StackClass, name: str):
    """Validates that a stack implementation behaves correctly."""
    stack = StackClass()
    
    # Test empty stack properties
    assert stack.empty() == True, f"{name}: should be empty initially"
    assert stack.size() == 0, f"{name}: size should be 0 initially"
    
    # Test pop on empty raises error
    try:
        stack.pop()
        assert False, f"{name}: pop on empty should raise"
    except IndexError:
        pass  # expected
    
    # Test top on empty raises error
    try:
        stack.top()
        assert False, f"{name}: top on empty should raise"
    except IndexError:
        pass  # expected
    
    # Push elements and verify LIFO order
    stack.push(10)
    assert stack.top() == 10, f"{name}: top should be 10"
    assert stack.size() == 1, f"{name}: size should be 1"
    
    stack.push(20)
    assert stack.top() == 20, f"{name}: top should be 20 after push 20"
    
    stack.push(30)
    assert stack.top() == 30, f"{name}: top should be 30"
    assert stack.size() == 3, f"{name}: size should be 3"
    
    # Pop and verify LIFO
    assert stack.pop() == 30, f"{name}: first pop should be 30"
    assert stack.pop() == 20, f"{name}: second pop should be 20"
    assert stack.pop() == 10, f"{name}: third pop should be 10"
    assert stack.empty() == True, f"{name}: should be empty after popping all"
    
    # Interleaved operations
    stack.push(1)
    stack.push(2)
    assert stack.pop() == 2, f"{name}: interleaved pop should be 2"
    stack.push(3)
    assert stack.top() == 3, f"{name}: top should be 3"
    assert stack.pop() == 3, f"{name}: pop should be 3"
    assert stack.pop() == 1, f"{name}: pop should be 1"
    
    print(f"✓ {name}: All tests passed!")

# Run tests on each implementation
test_stack_implementation(StackPushCostly, "Two Queues - Push Costly")
test_stack_implementation(StackPopCostly, "Two Queues - Pop Costly")
test_stack_implementation(StackSingleQueue, "Single Queue - Push Rotation")
test_stack_implementation(StackSingleQueuePopRotation, "Single Queue - Pop Rotation")
test_stack_implementation(StackOptimized, "Hybrid - Last Tracker")

Expected Output

✓ Two Queues - Push Costly: All tests passed!
✓ Two Queues - Pop Costly: All tests passed!
✓ Single Queue - Push Rotation: All tests passed!
✓ Single Queue - Pop Rotation: All tests passed!
✓ Hybrid - Last Tracker: All tests passed!

Conclusion

Implementing a stack using queues is a beautiful exercise in algorithmic thinking that reveals how constraints breed creativity. There is no single "best" solution — the optimal approach depends entirely on your workload's ratio of pushes to pops. The two-queue methods offer clear separation of concerns at the cost of extra memory during drains. The single-queue rotation methods are memory-frugal and elegant, using the queue itself as a circular buffer to reorder elements. The hybrid approach with a last-element tracker shows how a tiny amount of extra state can dramatically improve performance for specific operation patterns (top-heavy workloads).

Mastering these variations equips you with a nuanced understanding of time-space trade-offs, amortized complexity, and the art of designing data structures under interface constraints. Whether you encounter this in an interview or need to adapt a FIFO message broker to serve LIFO processing in a real system, the patterns explored here form a solid foundation for creative problem-solving with linear data structures.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles