← Back to DevBytes

Solving Implement Queue using Stacks in Python: Step-by-Step Guide

Introduction to Implementing Queue using Stacks

The "Implement Queue using Stacks" problem is a classic data structure challenge that frequently appears in coding interviews and computer science curricula. At its core, the task asks you to build a queue — a First-In-First-Out (FIFO) structure — using only stack operations, which are inherently Last-In-First-Out (LIFO). This constraint forces developers to think creatively about how to reverse the natural ordering of a stack to achieve queue behavior.

In this tutorial, you will learn what the problem entails, why it matters, two common approaches to solve it in Python, and best practices to keep your implementation clean and efficient.

What Is a Queue and What Is a Stack?

Before diving into the implementation, it is important to understand the two data structures involved.

Stack

A stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle. The last element added is the first one removed. Stacks support two primary operations:

Additional helper operations often include peek() (view the top element) and empty() (check if the stack is empty).

Queue

A queue is a linear data structure that follows the First-In-First-Out (FIFO) principle. The first element added is the first one removed. Queues support two primary operations:

The challenge is to simulate the FIFO behavior of a queue using only the LIFO operations of one or more stacks.

Why This Problem Matters

This problem is more than an academic exercise. It teaches several foundational concepts:

Approach 1: Making Push Costly (Two Stacks)

In this approach, we use two stacks: stack1 and stack2. The idea is to keep stack1 always holding the queue order with the front of the queue at the top. Whenever we push a new element, we move all existing elements to stack2, push the new element to stack1, and then move everything back.

Complexity Analysis

Implementation

class MyQueue:
    def __init__(self):
        self.stack1 = []
        self.stack2 = []

    def push(self, x: int) -> None:
        # Move all elements from stack1 to stack2
        while self.stack1:
            self.stack2.append(self.stack1.pop())
        # Push the new element onto stack1
        self.stack1.append(x)
        # Move everything back from stack2 to stack1
        while self.stack2:
            self.stack1.append(self.stack2.pop())

    def pop(self) -> int:
        if not self.stack1:
            raise IndexError("pop from empty queue")
        return self.stack1.pop()

    def peek(self) -> int:
        if not self.stack1:
            raise IndexError("peek from empty queue")
        return self.stack1[-1]

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

Example Usage

q = MyQueue()
q.push(1)
q.push(2)
q.push(3)

print(q.peek())   # Output: 1
print(q.pop())    # Output: 1
print(q.pop())    # Output: 2
print(q.empty())  # Output: False
print(q.pop())    # Output: 3
print(q.empty())  # Output: True

This approach is intuitive because the queue order is always maintained in stack1. However, the expensive push operation makes it inefficient when you have many insertions relative to removals.

Approach 2: Making Pop Costly (Amortized O(1))

The second approach also uses two stacks, but it reverses the cost. Here, stack_in receives all new elements, and stack_out provides elements for pop and peek. When stack_out is empty and we need to pop or peek, we transfer all elements from stack_in to stack_out. This reversal naturally reorders the elements into FIFO order.

Complexity Analysis

The amortized O(1) comes from the fact that each element is moved from stack_in to stack_out at most once across its lifetime in the queue.

Implementation

class MyQueue:
    def __init__(self):
        self.stack_in = []
        self.stack_out = []

    def push(self, x: int) -> None:
        self.stack_in.append(x)

    def pop(self) -> int:
        self._transfer_if_needed()
        if not self.stack_out:
            raise IndexError("pop from empty queue")
        return self.stack_out.pop()

    def peek(self) -> int:
        self._transfer_if_needed()
        if not self.stack_out:
            raise IndexError("peek from empty queue")
        return self.stack_out[-1]

    def empty(self) -> bool:
        return not self.stack_in and not self.stack_out

    def _transfer_if_needed(self) -> None:
        # Only transfer when stack_out is empty
        if not self.stack_out:
            while self.stack_in:
                self.stack_out.append(self.stack_in.pop())

Example Usage

q = MyQueue()
q.push(10)
q.push(20)
q.push(30)

print(q.peek())   # Output: 10
print(q.pop())    # Output: 10
q.push(40)
print(q.pop())    # Output: 20
print(q.pop())    # Output: 30
print(q.pop())    # Output: 40
print(q.empty())  # Output: True

Notice how pushing 40 after several pops does not disrupt the queue order. The stack_in simply accumulates new elements until stack_out is exhausted, at which point another transfer occurs.

Comparing the Two Approaches

Choosing between the two approaches depends on your use case:

Best Practices

Use Python Lists as Stacks

Python's built-in list type is ideal for stack operations. The append() and pop() methods are both O(1) and operate on the end of the list, which is exactly how a stack behaves. Avoid using insert(0, x) or pop(0) on lists, as these are O(n) operations.

Guard Against Empty Queues

Always check whether your stacks are empty before popping or peeking. Raising a clear exception, such as IndexError, helps catch bugs early. Alternatively, you can return None or a sentinel value, but exceptions are more Pythonic for truly invalid operations.

Encapsulate the Transfer Logic

In Approach 2, the transfer logic is shared by both pop and peek. Extracting it into a private helper method like _transfer_if_needed keeps your code DRY and easier to maintain.

Prefer Amortized Efficiency

Unless you have a specific reason to optimize for worst-case pop performance, prefer the amortized O(1) approach. It scales better for most real-world workloads where pushes and pops are interleaved.

Write Tests

Always test edge cases such as popping from an empty queue, pushing after popping, and interleaving operations. Here is a small test suite you can adapt:

def test_queue():
    q = MyQueue()
    assert q.empty() is True

    q.push(1)
    q.push(2)
    assert q.peek() == 1
    assert q.pop() == 1
    assert q.peek() == 2

    q.push(3)
    assert q.pop() == 2
    assert q.pop() == 3
    assert q.empty() is True

    try:
        q.pop()
        assert False, "Expected IndexError"
    except IndexError:
        pass

    print("All tests passed.")

test_queue()

Common Pitfalls

Conclusion

Implementing a queue using stacks is a deceptively simple problem that reveals deep truths about data structures, ordering, and algorithmic trade-offs. By using two stacks and choosing where to place the expensive operation, you can simulate FIFO behavior with either costly pushes or amortized costly pops. The two-stack approach with amortized O(1) pop is generally the best solution for most scenarios and is the answer interviewers expect. Master this pattern, and you will not only be ready for coding interviews but also gain a stronger intuition for how fundamental data structures can be composed to solve real engineering problems.

— Ad —

Google AdSense will appear here after approval

← Back to all articles