Design a data structure called a MaxStack
that extends the functionality of a regular stack. In addition to the standard stack operations (push
, pop
, top
), it should also support:
peekMax()
: Retrieve the maximum element in the stack without removing it.popMax()
: Retrieve the maximum element in the stack and remove it. If there are multiple instances of the maximum element, remove only the top-most one.Your task is to implement the MaxStack
class with the following methods:
MaxStack()
: Initializes the stack object.void push(int x)
: Pushes element x
onto the stack.int pop()
: Removes the element on top of the stack and returns it.int top()
: Gets the element on the top of the stack without removing it.int peekMax()
: Retrieves the maximum element in the stack without removing it.int popMax()
: Retrieves the maximum element in the stack and removes it. If there is more than one maximum element, only remove the top-most one.For example:
MaxStack maxStack = new MaxStack();
maxStack.push(5);
maxStack.push(1);
maxStack.push(5);
maxStack.top(); // Returns 5
maxStack.popMax(); // Returns 5, stack becomes [5, 1]
maxStack.top(); // Returns 1
maxStack.peekMax(); // Returns 5
maxStack.pop(); // Returns 1, stack becomes [5]
maxStack.top(); // Returns 5
Explain the time and space complexity of each operation. Consider edge cases such as an empty stack or multiple maximum values.
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. |