232. Implement Queue using Stacks
Problem Description
You need to implement a queue data structure using only two stacks. A queue follows First-In-First-Out (FIFO) principle, meaning elements are removed in the same order they were added. However, stacks follow Last-In-First-Out (LIFO) principle.
Your MyQueue
class must support these four operations:
push(x)
: Adds elementx
to the back of the queuepop()
: Removes and returns the element from the front of the queuepeek()
: Returns the element at the front of the queue without removing itempty()
: Returnstrue
if the queue has no elements,false
otherwise
The key constraint is that you can only use standard stack operations:
- Push to top
- Pop from top
- Peek at top
- Check size
- Check if empty
The solution uses two stacks to achieve queue behavior. stk1
serves as the input stack where new elements are pushed. stk2
serves as the output stack for pop and peek operations.
The clever trick is the move()
helper function: when we need to access the front of the queue (for pop()
or peek()
), and stk2
is empty, we transfer all elements from stk1
to stk2
. This reversal of elements transforms the LIFO order back to FIFO order, allowing us to access elements in the correct queue order from stk2
.
For example, if we push 1, 2, 3 into the queue:
- They go into
stk1
as [1, 2, 3] (3 on top) - When we need to pop, they're moved to
stk2
as [3, 2, 1] (1 on top) - Now popping from
stk2
gives us 1, which was the first element pushed
Intuition
The fundamental challenge here is that stacks and queues have opposite access patterns. A stack gives us access to the most recently added element (LIFO), while a queue needs access to the oldest element (FIFO). How can we bridge this gap using only stack operations?
The key insight comes from observing what happens when we transfer elements between two stacks. If we have elements [1, 2, 3] in a stack (with 3 on top) and pop them one by one into another stack, the second stack will have [3, 2, 1] (with 1 on top). This reversal is crucial - the element that was at the bottom of the first stack is now at the top of the second stack!
This gives us our strategy: we can use one stack for incoming elements and another for outgoing elements. Think of it like having two containers where:
- The first container (
stk1
) collects new arrivals in the order they come - The second container (
stk2
) is where we serve from
When stk2
is empty and we need to serve (pop or peek), we transfer everything from stk1
to stk2
. This transfer reverses the order, making the oldest element in stk1
become the topmost element in stk2
- exactly what we need for queue behavior!
The beauty of this approach is that each element moves at most twice: once into stk1
during push, and once from stk1
to stk2
during the transfer. After the transfer, elements in stk2
are already in the correct order for serving, so subsequent pops and peeks are direct operations until stk2
is empty again.
This "lazy" transfer strategy (only moving elements when necessary) ensures we maintain amortized O(1)
complexity for all operations while using only basic stack operations.
Solution Approach
The implementation uses two stacks to simulate queue behavior. Let's walk through each component:
Data Structure Setup
We initialize two empty stacks:
stk1
: The input stack for enqueue operationsstk2
: The output stack for dequeue operations
Push Operation
The push(x)
operation is straightforward - we simply append the element to stk1
:
def push(self, x: int) -> None:
self.stk1.append(x)
This maintains O(1)
time complexity since we're just adding to the top of the stack.
The Move Helper Function
The core logic lies in the move()
helper function:
def move(self):
if not self.stk2:
while self.stk1:
self.stk2.append(self.stk1.pop())
This function transfers all elements from stk1
to stk2
only when stk2
is empty. The transfer reverses the order of elements, converting LIFO to FIFO ordering.
Pop Operation
For pop()
, we first call move()
to ensure stk2
has elements in the correct order, then pop from stk2
:
def pop(self) -> int:
self.move()
return self.stk2.pop()
If stk2
already has elements, move()
does nothing. Otherwise, it transfers all elements from stk1
.
Peek Operation
The peek()
operation follows the same pattern but returns the top element without removing it:
def peek(self) -> int:
self.move()
return self.stk2[-1]
Empty Check
The queue is empty only when both stacks are empty:
def empty(self) -> bool:
return not self.stk1 and not self.stk2
Time Complexity Analysis
- Push:
O(1)
- Direct append tostk1
- Pop/Peek: Amortized
O(1)
- Each element is moved at most once fromstk1
tostk2
. Over n operations, the total cost isO(n)
, giving amortizedO(1)
per operation - Empty:
O(1)
- Simple boolean check
The key pattern here is the "lazy evaluation" - we only transfer elements when necessary, maintaining efficiency while using only standard stack operations.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a sequence of operations to see how the two-stack approach works:
Initial state: stk1 = []
, stk2 = []
Step 1: push(1)
- Add 1 to
stk1
- Result:
stk1 = [1]
,stk2 = []
Step 2: push(2)
- Add 2 to
stk1
- Result:
stk1 = [1, 2]
,stk2 = []
Step 3: push(3)
- Add 3 to
stk1
- Result:
stk1 = [1, 2, 3]
,stk2 = []
(3 is on top)
Step 4: pop()
- Call
move()
: Sincestk2
is empty, transfer all elements fromstk1
tostk2
- Pop 3 from
stk1
, push tostk2
:stk2 = [3]
- Pop 2 from
stk1
, push tostk2
:stk2 = [3, 2]
- Pop 1 from
stk1
, push tostk2
:stk2 = [3, 2, 1]
(1 is on top)
- Pop 3 from
- After move:
stk1 = []
,stk2 = [3, 2, 1]
- Pop from
stk2
: returns 1 - Result:
stk1 = []
,stk2 = [3, 2]
Step 5: push(4)
- Add 4 to
stk1
- Result:
stk1 = [4]
,stk2 = [3, 2]
Step 6: peek()
- Call
move()
: Sincestk2
is not empty, no transfer occurs - Return top of
stk2
: returns 2 - Result:
stk1 = [4]
,stk2 = [3, 2]
(no change)
Step 7: pop()
- Call
move()
:stk2
still not empty, no transfer - Pop from
stk2
: returns 2 - Result:
stk1 = [4]
,stk2 = [3]
Step 8: pop()
- Call
move()
:stk2
still not empty, no transfer - Pop from
stk2
: returns 3 - Result:
stk1 = [4]
,stk2 = []
Step 9: pop()
- Call
move()
: Sincestk2
is empty, transfer all fromstk1
- Pop 4 from
stk1
, push tostk2
:stk2 = [4]
- Pop 4 from
- After move:
stk1 = []
,stk2 = [4]
- Pop from
stk2
: returns 4 - Result:
stk1 = []
,stk2 = []
Notice how the elements were returned in FIFO order (1, 2, 3, 4) despite using only stack operations. The key moment is when move()
transfers elements - this reversal converts the LIFO ordering of stk1
into the FIFO ordering needed for queue operations.
Solution Implementation
1class MyQueue:
2 """
3 Implement a queue using two stacks.
4 The queue follows FIFO (First In First Out) principle.
5 """
6
7 def __init__(self):
8 """Initialize two stacks for queue implementation."""
9 self.input_stack = [] # Stack for enqueue operations
10 self.output_stack = [] # Stack for dequeue operations
11
12 def push(self, x: int) -> None:
13 """
14 Add an element to the rear of the queue.
15 Time Complexity: O(1)
16
17 Args:
18 x: The element to be added to the queue
19 """
20 self.input_stack.append(x)
21
22 def pop(self) -> int:
23 """
24 Remove and return the element from the front of the queue.
25 Time Complexity: Amortized O(1), Worst case O(n)
26
27 Returns:
28 The front element of the queue
29 """
30 self._transfer_if_needed()
31 return self.output_stack.pop()
32
33 def peek(self) -> int:
34 """
35 Return the front element without removing it.
36 Time Complexity: Amortized O(1), Worst case O(n)
37
38 Returns:
39 The front element of the queue
40 """
41 self._transfer_if_needed()
42 return self.output_stack[-1]
43
44 def empty(self) -> bool:
45 """
46 Check if the queue is empty.
47 Time Complexity: O(1)
48
49 Returns:
50 True if queue is empty, False otherwise
51 """
52 return len(self.input_stack) == 0 and len(self.output_stack) == 0
53
54 def _transfer_if_needed(self) -> None:
55 """
56 Transfer elements from input_stack to output_stack when needed.
57 This maintains the FIFO order by reversing the stack order.
58 """
59 # Only transfer when output_stack is empty
60 if not self.output_stack:
61 # Move all elements from input_stack to output_stack
62 while self.input_stack:
63 self.output_stack.append(self.input_stack.pop())
64
1class MyQueue {
2 // Stack for incoming elements (LIFO)
3 private Deque<Integer> inputStack = new ArrayDeque<>();
4 // Stack for outgoing elements (converts to FIFO order)
5 private Deque<Integer> outputStack = new ArrayDeque<>();
6
7 /**
8 * Initialize the queue data structure
9 */
10 public MyQueue() {
11 }
12
13 /**
14 * Push element x to the back of queue
15 * Time Complexity: O(1)
16 * @param x the element to be added to the queue
17 */
18 public void push(int x) {
19 inputStack.push(x);
20 }
21
22 /**
23 * Remove and return the element from the front of queue
24 * Time Complexity: Amortized O(1)
25 * @return the front element of the queue
26 */
27 public int pop() {
28 transferElements();
29 return outputStack.pop();
30 }
31
32 /**
33 * Get the front element without removing it
34 * Time Complexity: Amortized O(1)
35 * @return the front element of the queue
36 */
37 public int peek() {
38 transferElements();
39 return outputStack.peek();
40 }
41
42 /**
43 * Check whether the queue is empty
44 * Time Complexity: O(1)
45 * @return true if the queue is empty, false otherwise
46 */
47 public boolean empty() {
48 return inputStack.isEmpty() && outputStack.isEmpty();
49 }
50
51 /**
52 * Transfer elements from input stack to output stack when needed
53 * This reverses the order of elements, converting LIFO to FIFO
54 * Only transfers when output stack is empty to maintain queue order
55 */
56 private void transferElements() {
57 // Only transfer if output stack is empty
58 if (outputStack.isEmpty()) {
59 // Move all elements from input stack to output stack
60 while (!inputStack.isEmpty()) {
61 outputStack.push(inputStack.pop());
62 }
63 }
64 }
65}
66
67/**
68 * Your MyQueue object will be instantiated and called as such:
69 * MyQueue obj = new MyQueue();
70 * obj.push(x);
71 * int param_2 = obj.pop();
72 * int param_3 = obj.peek();
73 * boolean param_4 = obj.empty();
74 */
75
1class MyQueue {
2public:
3 /**
4 * Initialize the queue using two stacks
5 */
6 MyQueue() {
7 // Constructor is empty as stacks are automatically initialized
8 }
9
10 /**
11 * Push element x to the back of queue
12 * @param x: Element to be pushed
13 */
14 void push(int x) {
15 input_stack.push(x);
16 }
17
18 /**
19 * Removes the element from the front of queue and returns it
20 * @return: The front element that was removed
21 */
22 int pop() {
23 transferElements();
24 int front_element = output_stack.top();
25 output_stack.pop();
26 return front_element;
27 }
28
29 /**
30 * Get the front element without removing it
31 * @return: The front element of the queue
32 */
33 int peek() {
34 transferElements();
35 return output_stack.top();
36 }
37
38 /**
39 * Check whether the queue is empty
40 * @return: true if queue is empty, false otherwise
41 */
42 bool empty() {
43 return input_stack.empty() && output_stack.empty();
44 }
45
46private:
47 stack<int> input_stack; // Stack for enqueue operations
48 stack<int> output_stack; // Stack for dequeue operations
49
50 /**
51 * Transfer elements from input_stack to output_stack
52 * Only transfers when output_stack is empty to maintain order
53 */
54 void transferElements() {
55 // Only transfer if output_stack is empty
56 if (output_stack.empty()) {
57 // Move all elements from input_stack to output_stack
58 // This reverses the order, giving us FIFO behavior
59 while (!input_stack.empty()) {
60 output_stack.push(input_stack.top());
61 input_stack.pop();
62 }
63 }
64 }
65};
66
67/**
68 * Your MyQueue object will be instantiated and called as such:
69 * MyQueue* obj = new MyQueue();
70 * obj->push(x);
71 * int param_2 = obj->pop();
72 * int param_3 = obj->peek();
73 * bool param_4 = obj->empty();
74 */
75
1// Stack 1: Used for enqueue operations (push)
2let inputStack: number[] = [];
3// Stack 2: Used for dequeue operations (pop/peek)
4let outputStack: number[] = [];
5
6/**
7 * Transfers all elements from input stack to output stack
8 * This reverses the order, making FIFO possible with LIFO stacks
9 */
10function transferStacks(): void {
11 // Only transfer if output stack is empty to maintain order
12 if (outputStack.length === 0) {
13 // Move all elements from input to output
14 while (inputStack.length > 0) {
15 outputStack.push(inputStack.pop()!);
16 }
17 }
18}
19
20/**
21 * Adds an element to the back of the queue
22 * @param x - The element to be added
23 */
24function push(x: number): void {
25 inputStack.push(x);
26}
27
28/**
29 * Removes and returns the element from the front of the queue
30 * @returns The front element of the queue
31 */
32function pop(): number {
33 transferStacks();
34 return outputStack.pop()!;
35}
36
37/**
38 * Returns the element at the front of the queue without removing it
39 * @returns The front element of the queue
40 */
41function peek(): number {
42 transferStacks();
43 // Use at(-1) to get the last element (top of stack)
44 return outputStack.at(-1)!;
45}
46
47/**
48 * Checks if the queue is empty
49 * @returns True if the queue is empty, false otherwise
50 */
51function empty(): boolean {
52 return inputStack.length === 0 && outputStack.length === 0;
53}
54
Time and Space Complexity
Time Complexity:
push(x)
:O(1)
- Simply appends an element tostk1
.pop()
: AmortizedO(1)
- While a single operation might takeO(n)
when moving all elements fromstk1
tostk2
, each element is moved at most once fromstk1
tostk2
. Overn
operations, the total cost isO(n)
, giving amortizedO(1)
per operation.peek()
: AmortizedO(1)
- Same reasoning aspop()
. Themove()
function is called which has amortizedO(1)
cost, and accessing the last element ofstk2
isO(1)
.empty()
:O(1)
- As mentioned in the reference answer, checking whether both stacks are empty is a constant time operation.move()
:O(k)
wherek
is the number of elements instk1
whenstk2
is empty, but this contributes to the amortizedO(1)
complexity ofpop()
andpeek()
.
Space Complexity:
O(n)
wheren
is the total number of elements in the queue. The implementation uses two stacks (stk1
andstk2
) to store all elements, and at any point, the total elements across both stacks equals the number of elements in the queue.
Common Pitfalls
1. Unnecessary Element Transfers
A common mistake is transferring elements between stacks on every operation, even when not needed. This destroys the amortized O(1) complexity.
Incorrect approach:
def pop(self) -> int:
# Wrong: Always transferring all elements back and forth
while self.stk1:
self.stk2.append(self.stk1.pop())
result = self.stk2.pop()
while self.stk2:
self.stk1.append(self.stk2.pop())
return result
Solution: Only transfer when output_stack
is empty. Once elements are in output_stack
, leave them there until they're all consumed.
2. Incorrect Empty Check
Checking only one stack can lead to false positives when elements exist in the other stack.
Incorrect approach:
def empty(self) -> bool:
# Wrong: Only checking one stack
return len(self.input_stack) == 0
Solution: Check both stacks: return not self.input_stack and not self.output_stack
3. Transferring on Push Operations
Some implementations mistakenly transfer elements during push operations, which is unnecessary and inefficient.
Incorrect approach:
def push(self, x: int) -> None:
# Wrong: Unnecessary transfers during push
while self.output_stack:
self.input_stack.append(self.output_stack.pop())
self.input_stack.append(x)
Solution: Simply push to input_stack
. Transfers should only happen when accessing the front element (pop/peek).
4. Not Preserving Elements in Output Stack
After a pop or peek operation, some implementations transfer remaining elements back to the input stack, which breaks the amortized complexity.
Incorrect approach:
def peek(self) -> int:
self._transfer_if_needed()
result = self.output_stack[-1]
# Wrong: Moving elements back unnecessarily
while self.output_stack:
self.input_stack.append(self.output_stack.pop())
return result
Solution: Keep elements in output_stack
after operations. They're already in the correct order for future pop/peek operations.
5. Race Conditions in Multi-threaded Environments
The implementation isn't thread-safe. Concurrent access could lead to data corruption or incorrect results.
Solution for thread safety:
import threading
class ThreadSafeQueue:
def __init__(self):
self.input_stack = []
self.output_stack = []
self.lock = threading.Lock()
def push(self, x: int) -> None:
with self.lock:
self.input_stack.append(x)
def pop(self) -> int:
with self.lock:
self._transfer_if_needed()
return self.output_stack.pop()
6. Missing Edge Case Handling
Not handling operations on empty queue can cause runtime errors.
Solution: Add validation:
def pop(self) -> int:
if self.empty():
raise IndexError("pop from empty queue")
self._transfer_if_needed()
return self.output_stack.pop()
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!