Queue Implementation using Stacks

Medium
5 views
2 years ago

We want to assess your understanding of fundamental data structures and your ability to think creatively about how to implement one using another. Your task is to implement a queue data structure using only stacks. Remember that queues follow a First-In, First-Out (FIFO) principle, while stacks follow a Last-In, First-Out (LIFO) principle. You should implement the standard queue operations: enqueue (add an element to the rear), dequeue (remove an element from the front), peek (return the element at the front without removing it), and is_empty (check if the queue is empty).

Here are some examples to guide your implementation. Assume we are creating a queue of integers:

  • Example 1:

    • enqueue(1)
    • enqueue(2)
    • peek() should return 1
    • dequeue() should return 1
    • is_empty() should return False
    • dequeue() should return 2
    • is_empty() should return True
  • Example 2:

    • enqueue(10)
    • enqueue(5)
    • dequeue() should return 10
    • enqueue(12)
    • dequeue() should return 5
    • dequeue() should return 12
  • Example 3: (Empty Queue)

    • is_empty() should return True
    • dequeue() should return None (or raise an exception, depending on your preference and the language's conventions - please clarify your choice)
    • peek() should return None (or raise an exception - please clarify your choice)

Consider the time complexity of each operation. Aim for efficient implementations. You can use any programming language you are comfortable with. Please explain your approach before you start coding.

Sample Answer

Queue Implementation using Stacks

Let's explore how to implement a queue data structure using stacks. This is a classic interview problem that highlights understanding of both data structures and their properties.

1. Naive (Brute Force) Solution

The most straightforward approach involves using two stacks: stack1 (for enqueueing) and stack2 (for dequeueing).

  • Enqueue (add to queue): Simply push the new element onto stack1. This is O(1).
  • Dequeue (remove from queue): If stack2 is empty, transfer all elements from stack1 to stack2 one by one. Then, pop the top element from stack2. If stack2 is not empty, just pop the top element. This can be O(n) in the worst case, where n is the number of elements in the queue, because we might need to move all the elements from stack1 to stack2, but dequeue operations will be O(1) until stack2 is empty again.

Code Example (Python)

python class QueueUsingStacks: def init(self): self.stack1 = [] self.stack2 = []

def enqueue(self, item):
    self.stack1.append(item)

def dequeue(self):
    if not self.stack2:
        while self.stack1:
            self.stack2.append(self.stack1.pop())
    if self.stack2:
        return self.stack2.pop()
    else:
        return None  # Or raise an exception if queue is empty

def peek(self):
    if not self.stack2:
        while self.stack1:
            self.stack2.append(self.stack1.pop())
    if self.stack2:
        return self.stack2[-1]
    else:
        return None

def is_empty(self):
    return not (self.stack1 or self.stack2)

Big(O) Analysis (Naive)

  • Time Complexity:
    • Enqueue: O(1)
    • Dequeue: O(n) in the worst case (when stack2 is empty and all elements need to be moved), O(1) amortized.
    • Peek: O(n) in the worst case (when stack2 is empty and all elements need to be moved), O(1) amortized.
    • isEmpty: O(1)
  • Space Complexity: O(n), where n is the number of elements in the queue (because we are storing them in the stacks).

2. Optimal Solution (Amortized O(1) Dequeue)

The key optimization is to delay the transfer of elements from stack1 to stack2 until stack2 is absolutely empty and a dequeue operation is requested. This amortizes the cost of the transfer over multiple dequeue operations.

Big(O) Analysis (Optimal)

  • Time Complexity:
    • Enqueue: O(1)
    • Dequeue: O(1) amortized. While a single dequeue operation might take O(n) when elements need to be moved, over a sequence of enqueue and dequeue operations, the average time complexity per dequeue operation is O(1).
    • Peek: O(1) amortized, same reasoning as dequeue
    • isEmpty: O(1)
  • Space Complexity: O(n), where n is the number of elements in the queue.

Code Example (Python - Optimized)

python class QueueUsingStacksOptimal: def init(self): self.stack1 = [] # Used for enqueue self.stack2 = [] # Used for dequeue

def enqueue(self, item):
    self.stack1.append(item)

def dequeue(self):
    if not self.stack2:
        # Transfer elements from stack1 to stack2 only if stack2 is empty
        while self.stack1:
            self.stack2.append(self.stack1.pop())

    if self.stack2:
        return self.stack2.pop()
    else:
        return None # Or raise an exception if queue is empty

def peek(self):
    if not self.stack2:
        while self.stack1:
            self.stack2.append(self.stack1.pop())

    if self.stack2:
        return self.stack2[-1] 
    else:
        return None

def is_empty(self):
    return not (self.stack1 or self.stack2)

3. Edge Cases

  • Empty Queue: Handle the case where you attempt to dequeue or peek from an empty queue. The provided code returns None, but raising an exception (e.g., IndexError) might be more appropriate, depending on the application's requirements.
  • Large Number of Enqueue Operations Followed by Few Dequeue Operations: The amortized analysis relies on a mix of enqueue and dequeue operations. If there are many enqueue operations followed by only a few dequeue operations, the few dequeue operations will bear the cost of moving all the elements from stack1 to stack2. However, the amortized cost will still be O(1) over the entire sequence.

4. Summary

Implementing a queue with stacks is a good exercise to test understanding of data structures. While the naive approach provides a basic implementation, the optimized approach, with amortized O(1) dequeue time, is generally preferred for its efficiency. The key takeaway is understanding the trade-offs between enqueue and dequeue operations and optimizing for the common use case.