feat(patterns): data structure tutorials
This commit is contained in:
269
backend/data/patterns/monotonic-stack.yaml
Normal file
269
backend/data/patterns/monotonic-stack.yaml
Normal file
@@ -0,0 +1,269 @@
|
||||
name: Monotonic Stack
|
||||
slug: monotonic-stack
|
||||
difficulty_level: 3
|
||||
|
||||
description: >
|
||||
Maintain a stack where elements are always in sorted order (either increasing or
|
||||
decreasing). This enables efficient solutions for "next greater element" problems
|
||||
by leveraging the stack's ability to track candidates that might be the answer
|
||||
for future elements.
|
||||
|
||||
when_to_use: |
|
||||
- Next greater/smaller element
|
||||
- Previous greater/smaller element
|
||||
- Largest rectangle in histogram
|
||||
- Daily temperatures
|
||||
- Stock span problems
|
||||
|
||||
metaphor: |
|
||||
Imagine standing in a line of people of varying heights, all facing forward.
|
||||
You want to know who's the next taller person for each person in line. The
|
||||
trick: as you walk backward through the line, keep track of "potentially
|
||||
useful" tall people. When you encounter someone taller than people you're
|
||||
tracking, those shorter people will never be the answer—remove them. The
|
||||
remaining stack always contains candidates in decreasing height order.
|
||||
|
||||
Another analogy: a bouncer at a club with height requirements. As people line
|
||||
up, anyone shorter than the person in front can be removed from consideration—
|
||||
they'll never be visible from the front.
|
||||
|
||||
core_concept: |
|
||||
A **monotonic stack** maintains elements in sorted order by popping elements
|
||||
that violate the ordering when pushing new ones:
|
||||
|
||||
- **Monotonically decreasing**: Pop elements smaller than current before pushing
|
||||
- **Monotonically increasing**: Pop elements larger than current before pushing
|
||||
|
||||
The key insight is that when we pop an element, we've found its "next
|
||||
greater/smaller"—it's the current element we're about to push. The stack
|
||||
efficiently tracks candidates that might be answers for future elements.
|
||||
|
||||
**Pattern recognition:**
|
||||
- "Next greater" → decreasing stack (pop when current > top)
|
||||
- "Next smaller" → increasing stack (pop when current < top)
|
||||
- "Previous greater/smaller" → process elements and query stack before pushing
|
||||
|
||||
visualization: |
|
||||
**Next Greater Element:**
|
||||
|
||||
```
|
||||
Array: [4, 5, 2, 10, 8]
|
||||
Find next greater element for each
|
||||
|
||||
Process right to left (or left to right with index tracking):
|
||||
|
||||
Process 8: stack=[] → no greater, push 8
|
||||
stack=[8] → answer[4] = -1
|
||||
|
||||
Process 10: stack=[8] → 10 > 8, pop 8
|
||||
stack=[] → no greater, push 10
|
||||
stack=[10] → answer[3] = -1
|
||||
|
||||
Process 2: stack=[10] → 2 < 10, don't pop
|
||||
stack=[10,2] → answer[2] = 10
|
||||
|
||||
Process 5: stack=[10,2] → 5 > 2, pop 2
|
||||
stack=[10] → 5 < 10, don't pop
|
||||
stack=[10,5] → answer[1] = 10
|
||||
|
||||
Process 4: stack=[10,5] → 4 < 5, don't pop
|
||||
stack=[10,5,4] → answer[0] = 5
|
||||
|
||||
Result: [5, 10, 10, -1, -1]
|
||||
```
|
||||
|
||||
**Largest Rectangle in Histogram:**
|
||||
|
||||
```
|
||||
Heights: [2, 1, 5, 6, 2, 3]
|
||||
|
||||
Use increasing stack (pop when current < top)
|
||||
When popping, calculate rectangle with popped height as the smallest bar.
|
||||
|
||||
Process each bar:
|
||||
- 2: push (0,2)
|
||||
- 1: 1 < 2, pop (0,2) → width=1, area=2×1=2
|
||||
push (0,1) [take popped index]
|
||||
- 5: push (2,5)
|
||||
- 6: push (3,6)
|
||||
- 2: 2 < 6, pop (3,6) → width=1, area=6×1=6
|
||||
2 < 5, pop (2,5) → width=2, area=5×2=10
|
||||
push (2,2)
|
||||
- 3: push (5,3)
|
||||
- end: pop remaining, calculate areas
|
||||
|
||||
Max area = 10
|
||||
```
|
||||
|
||||
code_template: |
|
||||
def next_greater_element(nums: list[int]) -> list[int]:
|
||||
"""Find next greater element for each position."""
|
||||
n = len(nums)
|
||||
result = [-1] * n
|
||||
stack = [] # Stack of indices
|
||||
|
||||
for i in range(n):
|
||||
# Pop elements smaller than current
|
||||
while stack and nums[stack[-1]] < nums[i]:
|
||||
idx = stack.pop()
|
||||
result[idx] = nums[i]
|
||||
|
||||
stack.append(i)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def next_smaller_element(nums: list[int]) -> list[int]:
|
||||
"""Find next smaller element for each position."""
|
||||
n = len(nums)
|
||||
result = [-1] * n
|
||||
stack = []
|
||||
|
||||
for i in range(n):
|
||||
# Pop elements larger than current
|
||||
while stack and nums[stack[-1]] > nums[i]:
|
||||
idx = stack.pop()
|
||||
result[idx] = nums[i]
|
||||
|
||||
stack.append(i)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def daily_temperatures(temperatures: list[int]) -> list[int]:
|
||||
"""Days until warmer temperature."""
|
||||
n = len(temperatures)
|
||||
result = [0] * n
|
||||
stack = [] # Stack of indices
|
||||
|
||||
for i in range(n):
|
||||
while stack and temperatures[stack[-1]] < temperatures[i]:
|
||||
idx = stack.pop()
|
||||
result[idx] = i - idx # Days difference
|
||||
|
||||
stack.append(i)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def largest_rectangle_histogram(heights: list[int]) -> int:
|
||||
"""Largest rectangle area in histogram."""
|
||||
stack = [] # Stack of (index, height)
|
||||
max_area = 0
|
||||
|
||||
for i, h in enumerate(heights):
|
||||
start = i
|
||||
|
||||
while stack and stack[-1][1] > h:
|
||||
idx, height = stack.pop()
|
||||
max_area = max(max_area, height * (i - idx))
|
||||
start = idx # This index can extend back
|
||||
|
||||
stack.append((start, h))
|
||||
|
||||
# Process remaining in stack
|
||||
for idx, height in stack:
|
||||
max_area = max(max_area, height * (len(heights) - idx))
|
||||
|
||||
return max_area
|
||||
|
||||
|
||||
def stock_span(prices: list[int]) -> list[int]:
|
||||
"""Days since last higher price (inclusive of today)."""
|
||||
n = len(prices)
|
||||
result = [0] * n
|
||||
stack = [] # Stack of indices
|
||||
|
||||
for i in range(n):
|
||||
while stack and prices[stack[-1]] <= prices[i]:
|
||||
stack.pop()
|
||||
|
||||
# Span = distance to previous higher (or from start)
|
||||
result[i] = i - stack[-1] if stack else i + 1
|
||||
|
||||
stack.append(i)
|
||||
|
||||
return result
|
||||
|
||||
recognition_signals:
|
||||
- "next greater element"
|
||||
- "next smaller element"
|
||||
- "previous greater"
|
||||
- "daily temperatures"
|
||||
- "stock span"
|
||||
- "largest rectangle"
|
||||
- "histogram"
|
||||
- "trapping rain water"
|
||||
- "132 pattern"
|
||||
- "buildings with ocean view"
|
||||
|
||||
common_mistakes:
|
||||
- title: Wrong comparison direction
|
||||
description: |
|
||||
Using `<` when you should use `>` (or vice versa) results in the wrong
|
||||
type of monotonic stack.
|
||||
fix: |
|
||||
Remember: "next greater" needs decreasing stack, so pop when `nums[top] < current`.
|
||||
"Next smaller" needs increasing stack, so pop when `nums[top] > current`.
|
||||
|
||||
- title: Storing values instead of indices
|
||||
description: |
|
||||
Storing just values makes it impossible to calculate distances (like
|
||||
"how many days until...").
|
||||
fix: |
|
||||
Store indices in the stack. You can always access `nums[stack[-1]]` for
|
||||
the value when needed.
|
||||
|
||||
- title: Not processing remaining stack elements
|
||||
description: |
|
||||
Elements left in the stack after processing all input have no "next
|
||||
greater/smaller" in the array.
|
||||
fix: |
|
||||
After the main loop, process remaining elements. For histogram problems,
|
||||
their rectangle extends to the end. For "next greater," their answer is -1.
|
||||
|
||||
- title: Off-by-one with span calculations
|
||||
description: |
|
||||
Forgetting whether to include the current element in span calculations
|
||||
gives wrong results.
|
||||
fix: |
|
||||
For span problems, if stack is empty, span = i + 1 (from beginning).
|
||||
If stack has elements, span = i - stack[-1] (not +1 because previous
|
||||
greater is exclusive).
|
||||
|
||||
variations:
|
||||
- name: Next greater element
|
||||
description: |
|
||||
Find the first element to the right that is greater than current.
|
||||
Decreasing monotonic stack.
|
||||
example: "Next Greater Element I/II, Daily Temperatures"
|
||||
|
||||
- name: Next smaller element
|
||||
description: |
|
||||
Find the first element to the right that is smaller than current.
|
||||
Increasing monotonic stack.
|
||||
example: "Next Smaller Element"
|
||||
|
||||
- name: Previous greater/smaller
|
||||
description: |
|
||||
Query the stack before pushing to find the previous greater/smaller.
|
||||
The top of stack is the answer.
|
||||
example: "Stock Span, Buildings With Ocean View"
|
||||
|
||||
- name: Largest rectangle
|
||||
description: |
|
||||
Use increasing stack. When popping, calculate area using popped height
|
||||
and width from popped index to current index.
|
||||
example: "Largest Rectangle in Histogram, Maximal Rectangle"
|
||||
|
||||
- name: Trapping rain water
|
||||
description: |
|
||||
Can use monotonic stack to track left boundaries, calculating trapped
|
||||
water when finding right boundary. (Alternative: two-pointer approach)
|
||||
example: "Trapping Rain Water"
|
||||
|
||||
related_patterns:
|
||||
- two-pointers
|
||||
- sliding-window
|
||||
|
||||
prerequisite_patterns: []
|
||||
Reference in New Issue
Block a user