Implement a queue using the stack data structure. Include the following functions:
enqueue(x: int) -> None: adds x to the end of the queue.dequeue() -> int: removes and returns the element from the front of the queue.peek() -> int: returns the front element of the queue.You may not use any other data structures to implement the queue.
Input: [enqueue(1), enqueue(2), dequeue(), enqueue(3), peek()]
Output: [1, 2]
dequeue and peek operations will only be called on a non-empty queue.New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
A stack hands you the most recently added plate. But a lunch line is fair: whoever arrived first is served first. To fake fairness with two “newest-first” dispensers, pour one dispenser into the other — the pour reverses the order, so the oldest plate rises to the top of the second dispenser and can be served next.
A queue is a first-in-first-out (FIFO) data structure, whereas stacks are a first-in-last-out (FILO) data structure:
The main difference between these data structures is how items are evicted from them. In a queue, the first value to enter is the first to leave, whereas it would be the last to leave in a stack.
Now that we understand how they work, let’s dive into the problem. Let’s start by seeing if it’s possible to replicate the functionality of a queue with just one stack.
Consider the following stack where we push values 1, 2, and 3 to it after receiving enqueue(1), enqueue(2), and enqueue(3):
We now encounter a problem with attempting a dequeue operation since popping off the top of the stack would return 3. The value we actually want popped off is 1, since it was the first value that entered the data structure. However, 1 is all the way at the bottom of the stack.
To get to the bottom, we need to pop off all the values from the top of the stack and temporarily store these values in a separate data structure (temp) so we can add them back to the stack later:
Once we've popped and returned the bottom value (1), push the values stored in temp back onto the stack in reverse order to ensure they're added back correctly:
We know that if we were to use a data structure such as temp, it’d have to be a stack, since the problem specifies only stacks can be used. In this temporary data structure, we remove values in the opposite order in which we added them. In other words, it follows the LIFO principle, which is conveniently how a stack works. This means we can use a stack for our temporary storage.
Now, even though we have a solution that works, having to pop off every single value from the top of the stack whenever we want to access the bottom value is quite time-consuming. To find a way around this, let’s have a closer look at the state of our two stacks right after we’ve moved the stack values to temp:
In our original solution, we would now move the values from temp back to the main stack. However, notice the top of the temp stack now contains the next value we expect to return. This is because it’s the second value to have entered the data structure, and according to the FIFO eviction policy, it should be the next one to be removed.
So, instead of adding these values back to the main stack, we could just leave them in temp and return the stack’s top value at the next dequeue call.
In the above logic, we ended up using two stacks which each serve a unique purpose. In particular, we used:
enqueue_stack).dequeue_stack).An important thing to realize here is that the dequeue stack won’t always be populated with values. So, what should we do when it’s empty? We can just populate it by moving all the values from the enqueue stack to the dequeue stack, just like we did in the example. To understand this more clearly, let’s dive into a full example.
Let’s start with two enqueue calls and push each number onto the enqueue stack.
Now, let's try processing a dequeue call. The first step is to pop off each element from the enqueue stack and push them onto the dequeue stack:
Then, we just return the top value from the dequeue stack:
Let’s enqueue one more value:
If we call dequeue again, we return the value from the top of the dequeue stack:
Now, what happens when we call dequeue and the dequeue stack is empty? We need to repopulate it by popping all the values from the enqueue stack and pushing them into the dequeue stack. Once this is done, we return the top of the dequeue stack as usual:
Regarding the peek function, we follow the same logic as the dequeue function, but instead, we return the top element of the dequeue stack without popping it.
As mentioned before, the dequeue and peek functions have mostly the same behavior, with the only difference being that dequeue pops the top value while peek does not. To avoid duplicate code, the common logic between these functions for transferring values from the enqueue stack to the dequeue stack has been extracted into a separate function, transfer_enqueue_to_dequeue.
class Queue:
def __init__(self):
self.enqueue_stack = []
self.dequeue_stack = []
def enqueue(self, x: int) -> None:
self.enqueue_stack.append(x)
def transfer_enqueue_to_dequeue(self) -> None:
# If the dequeue stack is empty, push all elements from the enqueue stack
# onto the dequeue stack. This ensures the top of the dequeue stack
# contains the most recent value.
if not self.dequeue_stack:
while self.enqueue_stack:
self.dequeue_stack.append(self.enqueue_stack.pop())
def dequeue(self) -> int:
self.transfer_enqueue_to_dequeue()
# Pop and return the value at the top of the dequeue stack.
return self.dequeue_stack.pop() if self.dequeue_stack else None
def peek(self) -> int:
self.transfer_enqueue_to_dequeue()
return self.dequeue_stack[-1] if self.dequeue_stack else Nonestruct MyQueue {
stack<int> inStack, outStack;
void push(int x) {
inStack.push(x);
}
int pop() {
peek(); // make sure outStack has the front
int v = outStack.top();
outStack.pop();
return v;
}
int peek() {
if (outStack.empty()) { // only pour when outStack is empty
while (!inStack.empty()) {
outStack.push(inStack.top());
inStack.pop();
}
}
return outStack.top();
}
bool empty() {
return inStack.empty() && outStack.empty();
}
};class MyQueue {
private Deque<Integer> inStack = new ArrayDeque<>();
private Deque<Integer> outStack = new ArrayDeque<>();
public void push(int x) { inStack.push(x); }
public int pop() { peek(); return outStack.pop(); }
public int peek() {
if (outStack.isEmpty())
while (!inStack.isEmpty()) outStack.push(inStack.pop());
return outStack.peek();
}
public boolean empty() { return inStack.isEmpty() && outStack.isEmpty(); }
}Time complexity: The time complexity of:
enqueue is because we add one element to the enqueue stack in constant time.
dequeue is amortized .
dequeue calls, at most elements are moved between stacks, averaging the cost to time per dequeue operation.peek is amortized for the same reasons as dequeue.
Space complexity: The space complexity is since we maintain two stacks that collectively store all elements of the queue at any given time.
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
in-stack top out-stack top = front
in-stack (bottom→top):
out-stack:
in-stack:
out-stack (bottom→top):
out-stack after both pops:
| operation | in-stack | out-stack | returned |
|---|---|---|---|
| push(1) | 1 | — | — |
| push(2) | 1 2 | — | — |
| push(3) | 1 2 3 | — | — |
| pop() | — | 3 2 | 1 (poured, then popped front) |
| pop() | — | 3 | 2 |
| pop() | — | — | 3 |
in-stack top out-stack top = front
6 5 (top 5). Return 5; out-stack keeps 6.
out-stack after the pop:
in-stack:
out-stack:
| operation | in-stack | out-stack | returned |
|---|---|---|---|
| push(5) | 5 | — | — |
| push(6) | 5 6 | — | — |
| pop() | — | 6 | 5 (poured to 6 5, popped 5) |
| push(7) | 7 | 6 | — (no pour! 6 still waiting) |
| pop() | 7 | — | 6 |
| pop() | — | — | 7 (pour 7, pop it) |
A buggy version that pours on every pop would have re-poured after push(7), pushing 7 on top of 6 and wrongly returning 7 before 6.
Two lists. push appends to in_stack; peek pours into out_stack only when it’s empty; pop calls peek first, then pops.
Two std::stack<int>. Same lazy transfer. Remember top() then pop() because C++ pop() returns nothing.
Two ArrayDeque<Integer>. Same lazy transfer; outStack.pop() auto-unboxes the returned Integer to int.