Mastering LeetCode's "Implement Queue using Stacks": A Comprehensive Guide for Aspiring Software Engineers

Dive deep into solving a popular LeetCode challenge with optimal solutions in Python, TypeScript, and Java - perfect your interview skills today!

Mastering LeetCode's "Implement Queue using Stacks": A Comprehensive Guide for Aspiring Software Engineers

As someone passionate about helping fellow developers and aspiring software engineers navigate the often daunting world of technical interviews, I'm excited to tackle a classic problem that frequently pops up in interviews: implementing a First In First Out (FIFO) queue using only two stacks (LeetCode 232).

This challenge is not just about testing your knowledge of data structures but also about evaluating your ability to think creatively under constraints.

Introduction to the Problem

The task is seemingly straightforward yet intriguing: design a queue that supports the basic operations - push, pop, peek, and checking if the queue is empty - using only two stacks. At first glance, this might appear counterintuitive since stacks operate on a Last In First Out (LIFO) principle, which is the opposite of what we need for a queue.

A typical scenario would involve initiating your custom queue class, pushing elements into the queue, and then performing operations like peeking at the front element, popping an element from the front, and checking if the queue is empty.

Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (push, peek, pop, and empty).

Implement the MyQueue class:

  • void push(int x) Pushes element x to the back of the queue.

  • int pop() Removes the element from the front of the queue and returns it.

  • int peek() Returns the element at the front of the queue.

  • boolean empty() Returns true if the queue is empty, false otherwise.

Conceptual Solution and Big O Analysis

The essence of solving this problem efficiently lies in understanding how to reverse the order of elements. By pushing elements onto one stack and then transferring them to another stack, we can reverse their order, making the bottom element of the first stack accessible (like the front of a queue).

This operation isn't needed for every action. For pushing, we can directly push to the first stack. However, for pop and peek operations, we check if the second stack is empty. If it is, we transfer all elements from the first stack to the second, thereby reversing their order. This allows us to effectively pop and peek in FIFO order.

The Big O analysis reveals that while individual pop and peek operations might seem to have a worst-case time complexity of O(n) (due to transferring elements), the amortized time complexity for each operation is O(1). This is because each element is transferred at most twice (once into the second stack and once out of it) across all operations. Therefore, for n operations, the overall time complexity remains O(n), answering the follow-up question affirmatively.

Python Solution

class MyQueue:
    def __init__(self):
        self.stackIn = []
        self.stackOut = []

    def push(self, x: int) -> None:
        # Push element x to the back of the queue
        self.stackIn.append(x)

    def pop(self) -> int:
        # Remove and return the element from the front of the queue
        if not self.stackOut:
            while self.stackIn:
                self.stackOut.append(self.stackIn.pop())
        return self.stackOut.pop()

    def peek(self) -> int:
        # Return the element at the front of the queue
        if not self.stackOut:
            while self.stackIn:
                self.stackOut.append(self.stackIn.pop())
        return self.stackOut[-1]

    def empty(self) -> bool:
        # Return true if the queue is empty, false otherwise
        return not self.stackIn and not self.stackOut

TypeScript Solution

class MyQueue {
    private stackIn: number[];
    private stackOut: number[];

    constructor() {
        this.stackIn = [];
        this.stackOut = [];
    }

    push(x: number): void {
        // Push element x to the back of the queue
        this.stackIn.push(x);
    }

    pop(): number {
        // Remove and return the element from the front of the queue
        if (this.stackOut.length === 0) {
            while (this.stackIn.length > 0) {
                this.stackOut.push(this.stackIn.pop()!);
            }
        }
        return this.stackOut.pop()!;
    }

    peek(): number {
        // Return the element at the front of the queue
        if (this.stackOut.length === 0) {
            while (this.stackIn.length > 0) {
                this.stackOut.push(this.stackIn.pop()!);
            }
        }
        return this.stackOut[this.stackOut.length - 1];
    }

    empty(): boolean {
        // Return true if the queue is empty, false otherwise
        return this.stackIn.length === 0 && this.stackOut.length === 0;
    }
}

Java Solution

import java.util.Stack;

public class MyQueue {
    private Stack<Integer> stackIn;
    private Stack<Integer> stackOut;

    public MyQueue() {
        stackIn = new Stack<>();
        stackOut = new Stack<>();
    }

    public void push(int x) {
        // Push element

 x to the back of the queue
        stackIn.push(x);
    }

    public int pop() {
        // Remove and return the element from the front of the queue
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.pop();
    }

    public int peek() {
        // Return the element at the front of the queue
        if (stackOut.isEmpty()) {
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.peek();
    }

    public boolean empty() {
        // Return true if the queue is empty, false otherwise
        return stackIn.isEmpty() && stackOut.isEmpty();
    }
}

Conclusion

Understanding and implementing a queue using two stacks is more than just a programming exercise; it's a lesson in thinking outside the box and leveraging data structures in unconventional ways.

This challenge serves as an excellent practice for software engineering interviews, where problem-solving skills and efficiency are as critical as technical knowledge.

I hope this guide not only helps you solve this particular problem but also inspires you to approach challenges with a creative mindset. Happy coding, and best of luck with your interviews!

Did you find this article valuable?

Support Sean Coughlin by becoming a sponsor. Any amount is appreciated!