questions M-R

This commit is contained in:
2025-05-25 12:43:25 +01:00
parent 917c371529
commit 68699f35ec
62 changed files with 12841 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
title: Min Stack
slug: min-stack
difficulty: medium
leetcode_id: 155
leetcode_url: https://leetcode.com/problems/min-stack/
categories:
- stack
patterns:
- monotonic-stack
description: |
Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.
Implement the `MinStack` class:
- `MinStack()` initialises the stack object.
- `void push(int val)` pushes the element `val` onto the stack.
- `void pop()` removes the element on the top of the stack.
- `int top()` gets the top element of the stack.
- `int getMin()` retrieves the minimum element in the stack.
You must implement a solution with **O(1) time complexity** for each function.
constraints: |
- `-2^31 <= val <= 2^31 - 1`
- Methods `pop`, `top` and `getMin` operations will always be called on **non-empty** stacks.
- At most `3 * 10^4` calls will be made to `push`, `pop`, `top`, and `getMin`.
examples:
- input: |
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
output: "[null,null,null,null,-3,null,0,-2]"
explanation: |
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); // return -3
minStack.pop();
minStack.top(); // return 0
minStack.getMin(); // return -2
explanation:
intuition: |
Think of this problem like maintaining two parallel records: one for the actual items on the stack, and one for "what's the smallest item from here down?"
A regular stack gives us O(1) for push, pop, and top — those operations only touch the top element. But finding the minimum normally requires scanning the entire stack, which is O(n).
The key insight is that we can **precompute the minimum at each level** of the stack. When we push a new element, we know the current minimum (from the previous level) and the new value — the new minimum is simply the smaller of the two. When we pop, the minimum "reverts" to whatever it was before we pushed that element.
Imagine each element carrying a "badge" that says: "When I'm on top, the minimum is X." This badge never changes once assigned, because elements below me will never change while I exist on the stack.
approach: |
We solve this using **two synchronised stacks**: one for values, one for minimums.
**Step 1: Design the data structure**
- `stack`: A standard stack storing all pushed values
- `min_stack`: A parallel stack where each entry holds the minimum value at that level
&nbsp;
**Step 2: Implement push**
- Push `val` onto `stack`
- Compute the new minimum: `min(val, current_min)` where `current_min` is `min_stack[-1]` (or `val` if empty)
- Push this minimum onto `min_stack`
&nbsp;
**Step 3: Implement pop**
- Pop from both `stack` and `min_stack` simultaneously
- This maintains the synchronisation — when the top value leaves, so does its associated minimum
&nbsp;
**Step 4: Implement top and getMin**
- `top()`: Return `stack[-1]`
- `getMin()`: Return `min_stack[-1]`
&nbsp;
Both stacks always have the same length, so `getMin()` always reflects the minimum considering all elements currently in the stack.
common_pitfalls:
- title: Scanning for Minimum on Every Call
description: |
A naive approach is to iterate through the entire stack each time `getMin()` is called:
```python
def getMin(self):
return min(self.stack) # O(n) - too slow!
```
This violates the O(1) requirement. With up to `3 * 10^4` operations, repeatedly scanning could result in O(n²) total time.
wrong_approach: "Scanning the stack on each getMin() call"
correct_approach: "Track minimum at each stack level"
- title: Single Variable for Minimum
description: |
Storing only a single `self.min_value` variable seems efficient, but fails on pop:
```python
def pop(self):
if self.stack[-1] == self.min_value:
# What's the new minimum? We don't know!
self.min_value = ???
```
When you pop the current minimum, you'd need to scan the remaining stack to find the new minimum — back to O(n).
wrong_approach: "Single variable tracking current minimum"
correct_approach: "Stack of minimums to restore previous min on pop"
- title: Forgetting Empty Stack Edge Case in Push
description: |
When the stack is empty, there's no previous minimum to compare against:
```python
def push(self, val):
# This crashes if min_stack is empty!
new_min = min(val, self.min_stack[-1])
```
Handle the empty case by treating the first element as the minimum.
wrong_approach: "Always accessing min_stack[-1]"
correct_approach: "Check if min_stack is empty first, or use infinity as initial comparison"
key_takeaways:
- "**Auxiliary data structures**: When one operation is too slow, consider storing precomputed information in a parallel structure"
- "**State at each level**: The minimum-at-each-level technique extends to other 'aggregate' queries (max, sum, etc.)"
- "**Space-time tradeoff**: We use O(n) extra space to achieve O(1) time for all operations"
- "**Design problems**: Often require thinking about invariants — here, `min_stack[i]` always equals `min(stack[0:i+1])`"
time_complexity: "O(1) for all operations. Push, pop, top, and getMin each perform a constant number of operations."
space_complexity: "O(n) where `n` is the number of elements in the stack. We store each element twice — once in the main stack and once in the min stack."
solutions:
- approach_name: Two Stacks
is_optimal: true
code: |
class MinStack:
def __init__(self):
# Main stack for all values
self.stack = []
# Parallel stack tracking minimum at each level
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
# New minimum is smaller of val and current min (or just val if empty)
if self.min_stack:
new_min = min(val, self.min_stack[-1])
else:
new_min = val
self.min_stack.append(new_min)
def pop(self) -> None:
# Remove from both stacks to stay synchronised
self.stack.pop()
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
# min_stack top always holds current minimum
return self.min_stack[-1]
explanation: |
**Time Complexity:** O(1) for all operations — each method performs constant-time stack operations.
**Space Complexity:** O(n) — we store each element twice.
The min_stack maintains an invariant: `min_stack[i]` equals the minimum value among `stack[0]` through `stack[i]`. When we pop, both stacks shrink together, automatically restoring the correct minimum.
- approach_name: Single Stack with Pairs
is_optimal: true
code: |
class MinStack:
def __init__(self):
# Each entry is (value, min_at_this_level)
self.stack = []
def push(self, val: int) -> None:
# Calculate minimum including this new value
if self.stack:
current_min = min(val, self.stack[-1][1])
else:
current_min = val
# Store both the value and the running minimum
self.stack.append((val, current_min))
def pop(self) -> None:
self.stack.pop()
def top(self) -> int:
return self.stack[-1][0]
def getMin(self) -> int:
return self.stack[-1][1]
explanation: |
**Time Complexity:** O(1) for all operations.
**Space Complexity:** O(n) — each stack entry stores two values.
This variation combines both pieces of information into a single stack. Each entry is a tuple of `(value, minimum_at_this_level)`. Functionally equivalent to the two-stack approach, but with slightly cleaner code.
- approach_name: Min Stack with Optimised Space
is_optimal: false
code: |
class MinStack:
def __init__(self):
self.stack = []
# Only store min when it changes
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
# Only push to min_stack if val is <= current minimum
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self) -> None:
val = self.stack.pop()
# Only pop from min_stack if we're removing the current minimum
if val == self.min_stack[-1]:
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
explanation: |
**Time Complexity:** O(1) for all operations.
**Space Complexity:** O(n) worst case, but often less in practice.
This optimisation only adds to `min_stack` when we see a new minimum (or equal value). The space savings depend on the input — if values are strictly increasing, `min_stack` holds only one element. Note the `<=` instead of `<`: we must track duplicates of the minimum value, otherwise popping one copy would incorrectly update the minimum.