Skip to main content

Implement Stack using Queues

Learn to implement a LIFO stack data structure using two queues with efficient push and pop operations.

Implementing a stack using queues demonstrates fundamental understanding of data structure transformations and operational efficiency in software design.

1. Problem Restatement

Clarifying questions to ask the interviewer:

  1. Can we mutate the input queue(s) during operations, or do we need to maintain immutability?
  2. What is the expected maximum size of the stack and number of operations?
  3. Should we optimize for push or pop operations in terms of time complexity?
  4. Are we allowed to use additional space beyond the queues themselves?
  5. Do we need to handle null values in the stack?

Restatement of problem: Implement a LIFO (Last In, First Out) stack data structure using only queue operations. The stack should support all standard operations: push, pop, top, and empty.

Clarification of inputs, outputs, and constraints:

  • Input: Integer values to be pushed onto the stack
  • Output: Values returned by pop and top operations (pop returns the value, top returns the value without removing it)
  • Constraints: Only queue operations are allowed (offer/add, poll/remove, peek/element, isEmpty)
  • Stack operations must maintain LIFO order
  • All operations should be implemented efficiently

Assumptions made:

  • The stack will only contain integer values
  • We can use one or more queue data structures as the underlying implementation
  • The queue operations follow standard FIFO behavior (offer/poll, peek/element)
  • We can assume valid stack operations will be called

Data shape and hidden structure: The problem asks to simulate a stack's LIFO behavior using FIFO queue operations. The key insight is that while queues naturally process elements in FIFO order, we need to reverse this behavior for stack operations. The structure is essentially a sequence of integers that must be accessed in reverse order, which requires either:

  1. Reordering elements during push/pop operations
  2. Maintaining a pointer to the "top" element
  3. Using multiple queues to simulate stack behavior

This creates a fundamental tension between maintaining O(1) push vs pop operations, as we must choose which operation to optimize for in our implementation.

2. Pattern Recognition

Identifying the pattern

The most likely pattern here is stack LIFO (Last In First Out) behavior simulation using FIFO (First In First Out) queue operations.

Wording that signals the pattern

  • "Implement Stack using Queues" - directly signals we need to simulate stack operations (push, pop, top) using queue fundamental operations (offer, poll, peek)
  • The constraint that we can only use standard queue operations (add, remove, peek) signals we're working with a fundamental data structure transformation problem

Why this fits the pattern

This is a classic data structure simulation/abstraction problem where we need to implement one abstract data type (stack) using another (queue). The key insight is that while queues naturally support FIFO operations, stacks require LIFO behavior. This requires us to think about how to reverse the natural order of queue operations to achieve stack semantics.

Core insight needed

The fundamental insight is that we can simulate LIFO behavior using FIFO operations by either:

  1. Rotating elements to maintain the correct order (making most recent element accessible at front)
  2. Using two queues to effectively reverse the insertion order
  3. Maintaining a "top" reference that we can access directly

The most elegant solution uses one queue with rotation to maintain the stack property, ensuring that the most recently added element is always at the front of the queue for O(1) access.

3. Approach Evolution

My first instinct is to use two queues to simulate stack behavior, but that would be O(n) for pop operations since we'd need to move all elements except the last one. Let me think about how to optimize this.

The key insight is that we want to maintain the LIFO (Last In, First Out) property of a stack. If I use two queues, I can make one queue always hold the elements in reverse order (so that the front of the queue represents the top of the stack), but that would still require moving elements between queues.

I notice that if I maintain a single queue and always ensure the most recently added element is at the front, I can reduce it to O(1) for push and O(n) for pop operations by rotating elements.

The core optimization is to keep the stack top at the front of a single queue. When pushing, I add to the back and then rotate all previous elements to the back so that the newly added element becomes the first one.

Let me trace through this approach:

  1. Push 1: queue = [1]
  2. Push 2: queue = [1,2] → rotate to get [2,1]
  3. Push 3: queue = [2,1,3] → rotate to get [3,2,1]

This way, the front of queue always represents the top of stack.

My approach is to use a single queue and maintain the invariant that the most recently pushed element is always at the front of the queue. For push operations, I add to the back and then rotate all previous elements to bring the new element to front. For pop operations, I simply remove from the front.

This gives us O(1) push and O(n) pop operations, which is optimal for a stack implemented with queues. The key invariant that must hold is that the queue's front element always represents the stack top.

The alternative of using two queues would require O(n) for push operations as well, making it less efficient than the single queue approach.

4. Data Structure Choice

For implementing a stack using queues, I'll use two standard Java Queue implementations: LinkedList for the primary queue structure.

Primary Data Structures Used

1. LinkedList (as Queue) - Main Implementation

  • Used as the primary queue structure to simulate stack operations
  • Provides O(1) add/remove operations at both ends
  • Standard Java library implementation that's well-optimized

2. Additional Queue for Optimization (Optional)

  • In the optimized approach, I'll use a secondary queue to maintain the top element
  • This enables O(1) peek and pop operations by keeping track of the most recent element

Why Each Data Structure Is Needed

The LinkedList is chosen because:

  • It implements the Queue interface with add(), remove(), and peek() operations
  • It supports efficient insertion and removal from both ends (O(1) amortized)
  • It's a standard, well-understood data structure that's part of the Java Collections Framework

Time and Space Impact

Time Complexity:

  • Queue operations (add, remove, peek): O(1) for LinkedList
  • Stack push/pop: O(n) in basic approach, O(1) in optimized approach

Space Complexity:

  • O(n) for storing n elements in the queue
  • Additional O(1) space for maintaining top element pointer in optimized version

Helper Structure Benefits

The approach uses a single queue as the primary data structure, which is sufficient for the basic implementation. The key insight is that we can simulate LIFO behavior using FIFO queue operations by maintaining proper element ordering during push operations.

The choice of LinkedList over ArrayDeque or other queue implementations is pragmatic - it's the most straightforward to understand and implement, with similar performance characteristics for the required operations. The standard library queue interface provides clean abstractions that make the stack semantics clear and maintainable.

The solution avoids custom data structures or complex wrappers, focusing on clean usage of standard Java collections to achieve the required stack behavior with queue primitives.

5. Algorithm Walkthrough

Let me trace through the algorithm with a concrete example to demonstrate how it works.

Example: Implement stack operations push(1), push(2), push(3), and then pop() three times.

Step 1: Initialize empty queue: []

Step 2: Push 1

  • Add 1 to queue: [1]
  • No rotation needed since this is the first element
  • Stack top is 1

Step 3: Push 2

  • Add 2 to queue: [1, 2]
  • Rotate queue to move 2 to front: [2, 1]
  • Stack top is now 2

Step 4: Push 3

  • Add 3 to queue: [2, 1, 3]
  • Rotate queue to move 3 to front: [3, 2, 1]
  • Stack top is now 3

Step 5: Pop operation

  • Remove front element (3): [2, 1]
  • Return 3 as popped value
  • Stack top is now 2

Step 6: Pop operation

  • Remove front element (2): [1]
  • Return 2 as popped value
  • Stack top is now 1

Step 7: Pop operation

  • Remove front element (1): []
  • Return 1 as popped value
  • Stack is now empty

The key insight is that after each push, we rotate the queue so that the newly added element becomes the front of the queue, maintaining LIFO order. Each pop operation simply removes and returns the front element.

This approach correctly handles all edge cases including empty stack, single element, and multiple consecutive operations. The algorithm terminates because each operation maintains the queue in a valid state where the front always represents the top of the stack.

6. Java Solution

import java.util.Queue;
import java.util.LinkedList;

public class MyStack {
    private Queue<Integer> queue1;
    private Queue<Integer> queue2;
    
    public MyStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }
    
    public void push(int x) {
        // Add the new element to queue2
        queue2.offer(x);
        
        // Move all elements from queue1 to queue2
        while (!queue1.isEmpty()) {
            queue2.offer(queue1.poll());
        }
        
        // Swap the queues so that queue1 always contains elements in LIFO order
        Queue<Integer> temp = queue1;
        queue1 = queue2;
        queue2 = temp;
    }
    
    public int pop() {
        return queue1.poll();
    }
    
    public int top() {
        return queue1.peek();
    }
    
    public boolean empty() {
        return queue1.isEmpty();
    }
}

This implementation uses two queues to simulate stack behavior. The key insight is that we always maintain the elements in queue1 in LIFO order by moving all existing elements to queue2 before adding the new element, then swapping the queues. This ensures that the most recently added element is always at the front of queue1, enabling O(1) pop and top operations.

7. Complexity Analysis

Time Complexity: O(n) for push operations, O(1) for pop and top operations

The push operation is O(n) because in the worst case, we need to transfer all existing elements from the primary queue to a temporary queue, then move them back to maintain LIFO order. This requires n operations for n elements.

Pop and top operations are O(1) because they simply involve removing from or peeking at the front of the queue, which are constant-time operations in a well-implemented queue.

Space Complexity: O(n)

We use two queues to store all n elements, so the space complexity is linear. Each element is stored once in one of the queues at any given time.

Why these bounds are correct:

The O(n) push cost is amortized over multiple operations - while individual pushes can be expensive, the overall operation sequence remains efficient. The queue-based approach requires maintaining the LIFO property by rotating elements, which inherently costs linear time for each insertion.

The solution is optimal for the constraint that we must use only queues to implement stack operations. Any approach that maintains the stack's LIFO behavior using only queue operations will require at least O(n) time for push operations in the worst case, since we must ensure the newest element ends up at the front of the queue.

The space complexity is optimal because we must store all elements, and using two queues is necessary to maintain the required ordering properties.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty stack operations: The implementation must handle pop() and top() on an empty stack gracefully. Our solution throws NoSuchElementException for these cases, which is appropriate for a standard stack interface.
  • Single element operations: When pushing one element and immediately popping it, the internal queue state must be correctly managed to avoid leaving stale references.

Duplicate Values and Repeated States

  • Duplicate elements: The stack should properly handle duplicate values. Our implementation treats all elements equally regardless of their value.
  • Repeated push/pop cycles: Multiple consecutive operations should not cause internal queue corruption or incorrect state management.

Boundary Conditions

  • Single element stack: After pushing one element, top() should return that element correctly.
  • Stack underflow: Attempting to pop or peek from an empty stack should throw an appropriate exception (we use NoSuchElementException).
  • Queue size management: When elements are moved between queues, we must ensure the active queue always contains exactly one element for top().

Common Implementation Mistakes

  • Incorrect queue management: Failing to properly maintain which queue holds the current stack top can lead to incorrect top() results.
  • Not maintaining LIFO order: A common mistake is using the wrong queue for pushing elements, which breaks the Last-In-First-Out property.
  • Memory leaks: Not properly clearing queues or maintaining stale references can cause memory issues in long-running applications.
  • Race conditions: In multi-threaded environments, concurrent access to the stack would require proper synchronization mechanisms (not handled in this single-threaded solution).
  • Misunderstanding queue operations: Confusing offer() vs add(), or poll() vs remove() can lead to runtime exceptions or incorrect behavior.

The solution carefully handles all these cases by maintaining strict invariants: one queue always holds the stack elements in correct order, and the other is used only for temporary operations during push.

9. Alternative Approaches

There are several alternative approaches to implementing a stack using queues, each with different trade-offs:

Approach 1: Using Two Queues Instead of maintaining a single queue, we can use two queues - one for the main storage and another as a temporary buffer. When pushing an element, we add it to the auxiliary queue, then move all elements from the main queue to the auxiliary queue, and finally swap their roles. This ensures the newest element is always at the front. This approach has O(n) push time but O(1) pop and top operations.

Approach 2: Lazy Push with Queue Rotation In this approach, we maintain the stack property by rotating elements during push operations. When pushing element x, we first add it to the queue, then rotate all existing elements so that x becomes the front. This maintains the LIFO property but requires O(n) time for each push operation.

Approach 3: Using Deque Instead of Queue If we were allowed to use ArrayDeque or similar double-ended queue implementations, we could achieve O(1) operations for all stack operations by using the deque's addFirst(), removeFirst(), and getFirst() methods. However, since the problem specifically asks for queue-based implementation, this is not a valid solution.

The primary trade-off between these approaches is between push vs pop/top operation costs. Our chosen solution optimizes for pop and top operations (O(1) time) at the cost of push operations being O(n). The alternative approaches might optimize differently but generally sacrifice either time complexity or code clarity for the stack abstraction.

The chosen solution is preferred because it provides the most intuitive mapping between queue operations and stack semantics, with clear invariants maintained throughout the implementation.

10. Test Cases

import java.util.*;

public class StackUsingQueuesTest {
    public static void main(String[] args) {
        // Test Case 1: Basic functionality
        StackUsingQueues stack = new StackUsingQueues();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        assert stack.pop() == 3;
        assert stack.pop() == 2;
        assert stack.pop() == 1;
        System.out.println("Test 1 passed: Basic functionality");

        // Test Case 2: Empty stack operations
        StackUsingQueues emptyStack = new StackUsingQueues();
        assert emptyStack.empty() == true;
        try {
            emptyStack.pop();
            assert false; // Should throw exception
        } catch (Exception e) {
            // Expected
        }
        try {
            emptyStack.top();
            assert false; // Should throw exception
        } catch (Exception e) {
            // Expected
        }
        System.out.println("Test 2 passed: Empty stack operations");

        // Test Case 3: Mixed operations
        StackUsingQueues mixedStack = new StackUsingQueues();
        mixedStack.push(10);
        mixedStack.push(20);
        assert mixedStack.top() == 20;
        mixedStack.push(30);
        assert mixedStack.pop() == 30;
        assert mixedStack.top() == 20;
        assert mixedStack.pop() == 20;
        assert mixedStack.pop() == 10;
        assert mixedStack.empty() == true;
        System.out.println("Test 3 passed: Mixed operations");

        // Test Case 4: Single element
        StackUsingQueues singleStack = new StackUsingQueues();
        singleStack.push(42);
        assert singleStack.top() == 42;
        assert singleStack.pop() == 42;
        assert singleStack.empty() == true;
        System.out.println("Test 4 passed: Single element");

        // Test Case 5: Large sequence
        StackUsingQueues largeStack = new StackUsingQueues();
        for (int i = 1; i <= 1000; i++) {
            largeStack.push(i);
        }
        for (int i = 1000; i >= 1; i--) {
            assert largeStack.pop() == i;
        }
        assert largeStack.empty() == true;
        System.out.println("Test 5 passed: Large sequence");
    }
}

Sample Test Cases:

  1. Basic functionality: Push elements 1, 2, 3 and verify LIFO behavior by popping in reverse order
  2. Empty stack operations: Verify that pop/top on empty stack throws appropriate exception
  3. Mixed operations: Test interleaved push/pop/top operations with proper state management
  4. Single element: Verify stack works correctly with just one element
  5. Large sequence: Stress test with 1000 elements to ensure efficiency and correctness

Tricky Case:

  • Edge case with single element: Ensure that push/pop/top work correctly when only one element exists in the stack

Boundary Case:

  • Empty stack handling: Verify proper exception handling and empty state detection for edge cases like pop/top on empty stack

11. Final Summary

The core idea is to simulate LIFO (Last In First Out) behavior using FIFO (First In First Out) queue operations by rotating elements to maintain the correct order. This solution efficiently implements all stack operations with O(1) time complexity for push and top, and O(n) for pop, which is optimal given the constraint of using only queue operations. The key insight is that we can maintain the stack's top element at the front of the queue by rotating all but the last element to the back during push operations, making this approach both intuitive and efficient for the given constraints.

Was this article helpful?

Topics in this article

More from DSA Problems

  • DSA Problems

    Delete Node in a BST

    Learn how to delete a node from a Binary Search Tree while maintaining BST properties and optimal time complexity.

  • DSA Problems

    Validate Binary Search Tree Algorithm Solution

    Complete Java implementation to validate if a binary tree is a valid BST with optimal time and space complexity considerations.

  • DSA Patterns

    Find Kth Smallest Element in BST

    Learn to efficiently find the kth smallest element in a binary search tree using inorder traversal and iterative approaches.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.