Design a max stack data structure that supports the stack operations and supports finding the maximum element in the stack.
Implement the MaxStack
class:
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.You must implement the methods with O(1)
average time complexity for the top three methods and O(n)
for the last two methods.
Example 1:
Input ["MaxStack", "push", "push", "push", "top", "popMax", "top", "peekMax", "pop", "top"] [[], [5], [1], [5], [], [], [], [], [], []] Output [null, null, null, null, 5, 5, 1, 5, 1, 5] Explanation MaxStack stk = new MaxStack(); stk.push(5); // [5] stk.push(1); // [5, 1] stk.push(5); // [5, 1, 5] stk.top(); // return 5, [5, 1, 5] stk.popMax(); // return 5, [5, 1] stk.top(); // return 1, [5, 1] stk.peekMax(); // return 5, [5, 1] stk.pop(); // return 1, [5] stk.top(); // return 5, [5]
Constraints:
-107 <= x <= 107
105
calls will be made to each method.pop()
is called, the stack is not empty.popMax()
is called, the stack is not empty.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. |