Implement a last-in-first-out (LIFO) stack using only two queues. The implemented stack should support all the functions of a normal stack (push
, top
, pop
, and empty
).
Implement the MyStack
class:
void push(int x)
Pushes element x to the top of the stack.int pop()
Removes the element on the top of the stack and returns it.int top()
Returns the element on the top of the stack.boolean empty()
Returns true
if the stack is empty, false
otherwise.Notes:
push to back
, peek/pop from front
, size
and is empty
operations are valid.Example 1:
Input ["MyStack", "push", "push", "top", "pop", "empty"] [[], [1], [2], [], [], []] Output [null, null, null, 2, 2, false] Explanation MyStack myStack = new MyStack(); myStack.push(1); myStack.push(2); myStack.top(); // return 2 myStack.pop(); // return 2 myStack.empty(); // return False
Constraints:
1 <= x <= 9
100
calls will be made to push
, pop
, top
, and empty
.pop
and top
are valid.Follow-up: Can you implement the stack using only one queue?
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
We want to mimic how a stack works, but we only have queues to use. The brute force approach involves cleverly using the queues to ensure that the last element added is always the first one removed, just like a stack.
Here's how the algorithm would work step-by-step:
from collections import deque
class StackUsingQueues:
def __init__(self):
self.main_queue = deque()
self.temporary_queue = deque()
def push(self, item):
# Enqueue to temporary queue.
self.temporary_queue.append(item)
# Move all elements from main to temporary.
while self.main_queue:
self.temporary_queue.append(self.main_queue.popleft())
# Swap main and temporary queues.
self.main_queue, self.temporary_queue = self.temporary_queue, self.main_queue
def pop(self):
if not self.main_queue:
return None
# Removal from main queue gives stack behavior.
return self.main_queue.popleft()
def top(self):
if not self.main_queue:
return None
# Returns the front element of the queue.
return self.main_queue[0]
def empty(self):
# Stack is empty if main queue is empty.
return len(self.main_queue) == 0
We can simulate a stack using two queues. The trick is to ensure that when we add a new element, it becomes the first element in the queue, behaving like the top of the stack.
Here's how the algorithm would work step-by-step:
class MyStack:
def __init__(self):
self.queue_one = []
self.queue_two = []
def push(self, element_x: int) -> None:
# Move all elements from queue_one to queue_two
while self.queue_one:
self.queue_two.append(self.queue_one.pop(0))
# Add the new element to queue_one
self.queue_one.append(element_x)
# Move elements back from queue_two to queue_one
while self.queue_two:
self.queue_one.append(self.queue_two.pop(0))
def pop(self) -> int:
# queue_one maintains stack order, so remove head
return self.queue_one.pop(0)
def top(self) -> int:
# Return the first element of queue_one
return self.queue_one[0]
def empty(self) -> bool:
# Check if queue_one is empty
return not self.queue_one
# Your MyStack object will be instantiated and called as such:
# my_stack = MyStack()
# my_stack.push(element_x)
# param_2 = my_stack.pop()
# param_3 = my_stack.top()
# param_4 = my_stack.empty()
Case | How to Handle |
---|---|
Pushing null or undefined value onto the stack | Handle null or undefined push values by either disallowing them (throwing an error) or treating them as a valid data element like 0, depending on requirements. |
Popping from an empty stack | Return null, undefined, throw an exception or return a status flag indicating an error based on the problem context. |
Peeking (top) on an empty stack | Return null, undefined, throw an exception or return a status flag indicating an error based on the problem context. |
Maximum number of push operations exceeding queue's capacity (if limited) | Ensure that if the underlying queue implementation has a size limit, the push operation can appropriately handle exceeding this limit, perhaps by resizing or throwing an exception. |
Stack operations performed concurrently from multiple threads | The implementation should be thread-safe, using locks or other synchronization mechanisms to prevent race conditions when multiple threads access the queues simultaneously. |
Extremely large values being pushed onto the stack (potential memory issues) | Consider the memory footprint of each element, and ensure that pushing very large values doesn't lead to memory exhaustion or integer overflow during operations like calculating queue sizes. |
Queue implementation errors | If the underlying queue implementation has bugs (e.g., enqueue or dequeue fails intermittently), the stack implementation needs to be robust enough to handle such exceptions or inconsistencies from the queue. |
Empty queue after multiple push and pop operations | Correctly reset any internal state or temporary queues to ensure consistent behaviour of push and pop after the stack empties. |