Implementing a Stack Using Two Queues
In the study of data structures, Stacks and Queues represent two fundamental ways of organizing linear data, yet they operate on diametrically opposed principles:
- Stack: Follows the LIFO (Last-In, First-Out) principle. The last element added is the first one to be removed.
- Queue: Follows the FIFO (First-In, First-Out) principle. The first element added is the first one to be removed.
A classic algorithmic challenge is to implement a Stack using only the operations available in a Queue. The difficulty arises from this inherent contradiction: how can we use a structure that prioritizes the "oldest" elements to simulate a structure that prioritizes the "newest" elements?
To resolve this, we must use a second queue as a buffer or "transfer station." By moving elements between the two queues, we can effectively reorder them, ensuring that the most recently added element eventually resides at the "front" of our primary queue, mimicking the "top" of a stack.
Implementation Strategies
There are two primary ways to approach this problem. While both achieve the same functional result, they distribute the computational "cost" differently between the push and pop operations.
Strategy 1: Push-Heavy (O(n) Push, O(1) Pop)
In this approach, we perform the heavy lifting during the push operation. Every time a new element is added, we rearrange the entire queue so that the newest element is always at the front. This makes the pop, top, and empty operations extremely efficient.
Strategy 2: Pop-Heavy (O(1) Push, O(n) Pop)
Here, we prioritize the push operation by simply adding the new element to the end of the queue. However, when a pop is requested, we must move all elements except the very last one into the auxiliary queue. The remaining element is our "stack top." This makes push very fast but makes pop and top computationally expensive.
Detailed Implementation (Strategy 1)
Below is a Python implementation of Strategy 1, which ensures that the pop operation remains $O(1)$. We use collections.deque to represent our queues.
from collections import deque
class MyStack:
def __init__(self):
# q1 acts as our main storage where the front is the stack top
self.q1 = deque()
# q2 serves as an auxiliary buffer for reordering
self.q2 = deque()
def push(self, x: int) -> None:
# 1. Add the new element to the empty auxiliary queue
self.q2.append(x)
# 2. Move all existing elements from q1 to q2
# This places the new element at the front of the sequence
while self.q1:
self.q2.append(self.q1.popleft())
# 3. Swap the names of q1 and q2 so q1 always holds the data
self.q1, self.q2 = self.q2, self.q1
def pop(self) -> int:
# Since q1 is maintained such that the front is the stack top
return self.q1.popleft()
def top(self) -> int:
# Return the element at the front of the queue
return self.q1[0]
def empty(self) -> bool:
# Return True if the main queue is empty
return not self.q1
Execution Walkthrough
Let's trace the state of the queues through a sequence of operations: push(1), push(2), and push(3).
push(1):q2receives1.q1is empty, so no elements are moved.- After swapping,
q1 = [1],q2 = [].
push(2):q2receives2.- We move
1fromq1toq2. Nowq2 = [2, 1]. - After swapping,
q1 = [2, 1],q2 = [].
push(3):q2receives3.- We move
2and then1fromq1toq2. Nowq2 = [3, 2, 1]. - After swapping,
q1 = [3, 2, 1],q2 = [].
At this point, the front of q1 is 3, which was the last element pushed. This perfectly simulates the LIFO behavior. If we call pop(), we get 3; if we call top(), we see 3.
Complexity Analysis
| Operation | Strategy 1 (Push-Heavy) | Strategy 2 (Pop-Heavy) |
|---|---|---|
| Push | $O(n)$ | $O(1)$ |
| Pop | $O(1)$ | $O(n)$ |
| Top | $O(1)$ | $O(n)$ |
| Empty | $O(1)$ | $O(1)$ |
| Space | $O(n)$ | $O(n)$ |
Which one should you choose?
- If your application involves frequent reads/pops but infrequent writes, Strategy 1 is superior.
- If your application is write-intensive and you only occasionally need to access the top element, Strategy 2 provides better amortized performance for the
pushoperation.
Beyond the Basics: Further Optimizations
While the two-queue approach is the standard solution, it is worth noting a few advanced insights:
- The Single Queue Optimization: It is actually possible to implement a stack using only one queue. After performing a standard
enqueueoperation, you can determine the current size of the queue ($n$) and perform $n-1$ "rotate" operations (dequeuing an element and immediately enqueuing it back). This effectively moves the newest element to the front of the queue, achieving the same result as Strategy 1 without the need for a second physical queue. - The Symmetric Problem: A common follow-up question is: "How do you implement a Queue using two Stacks?" The logic is mirrored—one stack handles incoming data, and the second stack handles outgoing data, transferring elements only when the second stack is empty.
Mastering these transformations is not just about memorizing code; it is about understanding how different data structures can be composed to manipulate the flow of information. This conceptual flexibility is the foundation for working with more complex structures like Deques (Double-Ended Queues) and Priority Queues.