Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 37

Queue from Stacks: Implementation and Amortized Analysis

Recommended resources

Hello! Welcome to your next lesson.

In our last session, we built a solid understanding of the queue data structure, implementing it with both circular arrays and linked lists to achieve efficient O(1) operations. You learned that a queue is defined by its First-In, First-Out (FIFO) behavior.

Today, we'll tackle a classic problem that's a favorite in technical interviews because it tests your understanding of data structures from a different angle. Your goal for this lesson is to implement a queue using two stacks and analyze its amortized time complexity. This exercise will force you to think creatively about how to simulate FIFO behavior using only LIFO (Last-In, First-Out) building blocks.

1. The Challenge: Simulating FIFO with LIFO

At first, this seems counterintuitive. Stacks are the opposite of queues. If you push elements 1, 2, 3 into a stack, they come out as 3, 2, 1. A queue must return them as 1, 2, 3. How can we resolve this?

The key insight is that using two stacks allows us to reverse the order of elements.

Imagine you push elements into stack1. They are in LIFO order. If you then pop every element from stack1 and push it onto stack2, the order gets reversed! The element at the bottom of stack1 is now at the top of stack2, ready to be popped in FIFO order.

Implement Queue using Stacks
This diagram illustrates the core concept. Elements are enqueued into a `stackPush`. To dequeue, they are transferred to a `stackPop`, which reverses their order, making the first element in now the first element out.

2. Developing an Efficient Algorithm

While you could transfer elements back and forth for every operation, it would be very inefficient. A much smarter approach, often called the "lazy" or "amortized" approach, optimizes this process. We'll use two stacks, which we can call inputStack and outputStack.

To understand the intuition behind this optimized method, let's first see why a single stack is not enough and then how two stacks solve the problem elegantly.

Implement A Queue Using Stacks - The Queue ADT ("Implement Queue Using Stacks" on LeetCode)

The 'Back To Back SWE' video provides an excellent step-by-step development of the two-stack queue. It begins by showing the limitations of a single stack before introducing the optimized two-stack solution.

First, watch from 01:13 to 07:34 to see a clear demonstration of why implementing a queue with a single stack results in an inefficient O(N) dequeue operation. Then, continue from 07:34 to 11:49 to see the optimized two-stack solution, which forms the basis of our implementation.

As the video explained, the logic is as follows:

  • inputStack: This stack acts as the entry point. All new elements are enqueued here.
  • outputStack: This stack is used for dequeuing. It holds elements in the correct FIFO order.

The operations are defined like this:

  1. push(int x) / enqueue(int x):

    • Always push the new element x onto inputStack.
    • This is a simple stack push, so it's always an O(1) operation.
  2. pop() / dequeue():

    • First, check outputStack. If it is not empty, it means we already have elements in the correct FIFO order. Simply pop from outputStack and return the value. This is O(1).
    • If outputStack is empty, we need to "refill" it. We do this by transferring all elements from inputStack to outputStack. As each element is popped from inputStack and pushed to outputStack, the order is reversed.
    • Once the transfer is complete, pop from outputStack. This transfer step is O(N), where N is the number of elements in inputStack.
  3. peek(): The logic is identical to pop(), but instead of popping from outputStack, we just peek at its top element.

  4. empty(): The queue is empty only if both inputStack and outputStack are empty.


3. Amortized Time Complexity

You might look at the pop operation and think, "The worst-case is O(N), so it's an O(N) operation." While the worst-case for a single pop call is indeed O(N), this is where amortized analysis becomes crucial, especially in interview discussions.

Amortized analysis considers the total cost of a sequence of operations and averages it out.

Let's think about the "journey" of a single element:

  1. It is pushed onto inputStack (1 operation).
  2. It is popped from inputStack and pushed onto outputStack (2 operations). This happens only once in its lifetime.
  3. It is popped from outputStack (1 operation).

Each element goes through a constant number of operations (at most 4). If we perform M operations in total, the total time taken will be proportional to M, not M*N. Therefore, the average cost per operation is constant.

This means the amortized time complexity of pop and peek is O(1).

Implement A Queue Using Stacks - The Queue ADT ("Implement Queue Using Stacks" on LeetCode)

Let's conclude the 'Back To Back SWE' video, which provides a concise explanation of this amortized analysis.

Watch from 11:49 to 14:09. The video connects this concept to the ArrayList in Java, which is another classic example of amortized constant time complexity.

For a detailed written explanation of the logic and complexity, the following resource is very clear.

232. Implement Queue using Stacks - In-Depth Explanation

The AlgoMonster article 'Implement Queue using Stacks' provides a great textual breakdown of the intuition, implementation, and complexity analysis.

First, read the 'Intuition' section for a clear explanation of the two-stack reversal trick. Then, study the 'Time and Space Complexity' section for a detailed breakdown of the amortized analysis. Finally, review the 'Example Walkthrough' to trace the state of the stacks through a sequence of operations.


4. Java Implementation

Now let's translate this logic into Java code. For interview preparation, it's essential to be able to write this from scratch. We will use Java's Stack class for this implementation.

Implement Queue using Stack (0(1) amortized method)

The article 'Implement Queue using Stack' on airtribe.live provides a clean and focused Java implementation that perfectly matches our learning outcome.

Please read the sections under 'Solution Approach', 'Java Implementation', and 'Common Mistakes to Avoid'. Pay close attention to how the pop and peek methods check if outputStack is empty before triggering the transfer.

Here is the complete Java code, based on the resources, which you should be prepared to write in an interview.

import java.util.Stack;

class MyQueue {
    // Stack for enqueue operations.
    private Stack<Integer> inputStack;
    // Stack for dequeue operations. Holds elements in FIFO order.
    private Stack<Integer> outputStack;

    public MyQueue() {
        inputStack = new Stack<>();
        outputStack = new Stack<>();
    }
    
    // Pushes element x to the back of the queue.
    // Time Complexity: O(1)
    public void push(int x) {
        inputStack.push(x);
    }
    
    // Removes the element from the front of the queue and returns it.
    // Time Complexity: Amortized O(1)
    public int pop() {
        // Ensure outputStack has elements before popping.
        peek(); 
        return outputStack.pop();
    }
    
    // Gets the front element.
    // Time Complexity: Amortized O(1)
    public int peek() {
        // If outputStack is empty, transfer elements from inputStack.
        if (outputStack.isEmpty()) {
            while (!inputStack.isEmpty()) {
                outputStack.push(inputStack.pop());
            }
        }
        return outputStack.peek();
    }
    
    // Returns true if the queue is empty, false otherwise.
    // Time Complexity: O(1)
    public boolean empty() {
        return inputStack.isEmpty() && outputStack.isEmpty();
    }
}

Note: In modern Java, it's recommended to use the Deque interface with an ArrayDeque implementation to represent a stack (Deque<Integer> stack = new ArrayDeque<>();). However, using the Stack class is simple, direct, and common in interview contexts for this specific problem.

Test your understanding!

Trace the state of inputStack and outputStack after the following sequence of operations. What is the final content of each stack?

  1. push(1)
  2. push(2)
  3. pop()
  4. push(3)
  5. peek()
  6. pop()
Show answer

Initial State: inputStack = [], outputStack = []

  1. push(1): inputStack = [1], outputStack = []
  2. push(2): inputStack = [1, 2], outputStack = []
  3. pop():
    • outputStack is empty. Transfer from inputStack.
    • inputStack becomes []. outputStack becomes [1, 2]. (Remember, 2 is popped first from input, then 1. So 1 is on top of output).
    • outputStack.pop() returns 1.
    • State: inputStack = [], outputStack = [2]
  4. push(3): inputStack = [3], outputStack = [2]
  5. peek():
    • outputStack is not empty. It returns outputStack.peek(), which is 2.
    • State remains unchanged: inputStack = [3], outputStack = [2]
  6. pop():
    • outputStack is not empty. It returns outputStack.pop(), which is 2.
    • State: inputStack = [3], outputStack = []

Final State: inputStack = [3], outputStack = []


Conclusion

Excellent work! You've successfully implemented a queue using two stacks and, more importantly, analyzed its performance using the concept of amortized complexity. This is a powerful idea that separates a good developer from a great one.

Key Takeaways:

  • A queue's FIFO behavior can be simulated by two LIFO stacks by using one for input and one for output.
  • The transfer of elements from the input stack to the output stack effectively reverses their order.
  • The most efficient implementation is the "lazy" approach: only transfer elements when the output stack is empty.
  • While a single pop or peek can be O(N) in the worst case, the amortized time complexity is O(1) because each element is moved a constant number of times over its lifecycle.

Preview of the next lesson:
We have now covered stacks and queues in depth. Next, we will explore the Deque (Double-Ended Queue), a versatile data structure that combines the functionalities of both. You will learn to implement a deque using a doubly linked list, giving you efficient O(1) insertion and deletion at both the front and the back.