Design a data structure that acts like a stack, but also supports retrieving and removing the maximum element in O(1) time (for retrieval) and O(n) for removal in the worst case.
Specifically, implement a MaxStack
class with the following methods:
MaxStack()
: Initializes an empty max stack.void push(int x)
: Pushes element x
onto the stack.int pop()
: Removes and returns the element at the top of the stack.int top()
: Returns the element at the top of the stack without removing it.int peekMax()
: Returns the maximum element currently in the stack without removing it. This should be an O(1) operation.int popMax()
: Removes and returns the maximum element currently in the stack. If there are multiple occurrences of the maximum value, only the topmost one should be removed. This operation can take O(n) time in the worst case.Example Usage:
MaxStack stack = new MaxStack();
stack.push(5);
stack.push(1);
stack.push(5);
System.out.println(stack.top()); // Output: 5
System.out.println(stack.popMax()); // Output: 5
System.out.println(stack.top()); // Output: 1
System.out.println(stack.peekMax()); // Output: 5
System.out.println(stack.pop()); // Output: 1
System.out.println(stack.top()); // Output: 5
Explain the time and space complexity of each operation. Discuss the edge cases and how your implementation handles them.
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:
The brute force method for the Max Stack problem involves simulating every single operation. We create a new stack for each operation performed and store the relevant values in the stack by traversing its contents.
Here's how the algorithm would work step-by-step:
class MaxStack:
def __init__(self):
self.stack = []
def push(self, number):
# Create a new stack and copy the current stack into it.
new_stack = self.stack[:]
new_stack.append(number)
self.stack = new_stack
def pop(self):
# Create a new stack and copy the current stack into it.
if not self.stack:
return None
new_stack = self.stack[:]
popped_value = new_stack.pop()
self.stack = new_stack
return popped_value
def top(self):
if not self.stack:
return None
return self.stack[-1]
def peekMax(self):
# Need to check every number to find the max.
if not self.stack:
return None
maximum_value = self.stack[0]
for number in self.stack:
if number > maximum_value:
maximum_value = number
return maximum_value
def popMax(self):
if not self.stack:
return None
new_stack = self.stack[:]
maximum_value = self.stack[0]
for number in self.stack:
if number > maximum_value:
maximum_value = number
# Remove the first instance of the maximum value.
for index, number in enumerate(new_stack):
if number == maximum_value:
del new_stack[index]
break
self.stack = new_stack
return maximum_value
To efficiently manage a stack and quickly find the maximum value within it, we use an auxiliary stack to track the maximums seen so far. This avoids re-calculating the maximum every time a value is requested and provides a constant time lookup.
Here's how the algorithm would work step-by-step:
class MaxStack:
def __init__(self):
self.main_stack = []
self.maximums_stack = []
def push(self, number):
self.main_stack.append(number)
# Only push onto maximums stack if larger than current max.
if not self.maximums_stack or number >= self.maximums_stack[-1]:
self.maximums_stack.append(number)
def pop(self):
if not self.main_stack:
return None
popped_value = self.main_stack.pop()
# Ensure maximums stack is consistent after popping.
if self.maximums_stack and popped_value == self.maximums_stack[-1]:
self.maximums_stack.pop()
return popped_value
def top(self):
if not self.main_stack:
return None
return self.main_stack[-1]
def peekMax(self):
# Maximums stack top is always the current maximum.
if self.maximums_stack:
return self.maximums_stack[-1]
return None
def popMax(self):
maximum_value = self.peekMax()
if maximum_value is None:
return None
temp_stack = []
while self.top() != maximum_value:
temp_stack.append(self.pop())
self.pop()
# Repopulate main stack with values from temp stack.
while temp_stack:
self.push(temp_stack.pop())
return maximum_value
Case | How to Handle |
---|---|
Stack is initially empty | Check for empty stack before any pop or peek operations to avoid errors. |
Pushing the maximum integer value | Ensure no integer overflow occurs when pushing and updating the max stack. |
Popping from the stack when only one element is present, and it is the maximum value | Update the max stack appropriately when the last element that is also the maximum is popped. |
Large number of push and pop operations exceeding memory limits | Consider using a more memory efficient data structure if memory becomes a concern. |
Pushing a value equal to current maximum | The max stack should correctly handle multiple identical maximum values. |
Pushing extremely small negative numbers | The max stack should handle negative numbers correctly when finding maximum. |
Attempting to find max when the stack contains mixed data types (if applicable in the language) | Enforce type consistency by only allowing a single data type (e.g., integers) in the stack. |
Popping all elements from the stack, leaving it empty | Verify that the max stack is also empty after all elements are popped from the main stack. |