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.
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.
The most straightforward approach involves using two stacks: stack1
(for enqueueing) and stack2
(for dequeueing).
stack1
. This is O(1).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.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)
stack2
is empty and all elements need to be moved), O(1) amortized.stack2
is empty and all elements need to be moved), O(1) amortized.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.
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)
None
, but raising an exception (e.g., IndexError
) might be more appropriate, depending on the application's requirements.stack1
to stack2
. However, the amortized cost will still be O(1) over the entire sequence.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.