feat(patterns): tutorial system
This commit is contained in:
202
backend/data/patterns/binary-search.yaml
Normal file
202
backend/data/patterns/binary-search.yaml
Normal file
@@ -0,0 +1,202 @@
|
||||
name: Binary Search
|
||||
slug: binary-search
|
||||
difficulty_level: 2
|
||||
|
||||
description: >
|
||||
Efficiently search sorted data by repeatedly dividing the search space in half.
|
||||
This transforms O(n) linear search into O(log n) by eliminating half the
|
||||
remaining possibilities with each comparison.
|
||||
|
||||
when_to_use: |
|
||||
- Sorted arrays or search spaces
|
||||
- Finding boundaries (first/last occurrence)
|
||||
- Searching in rotated sorted arrays
|
||||
- Finding peak elements
|
||||
- Minimizing/maximizing with monotonic constraints
|
||||
|
||||
metaphor: |
|
||||
Imagine playing a number guessing game where someone says "higher" or "lower"
|
||||
after each guess. The optimal strategy is always guessing the middle—you
|
||||
eliminate half the possibilities each time regardless of the answer.
|
||||
|
||||
Another analogy: looking up a word in a physical dictionary. You don't read
|
||||
page by page from the start. You open roughly to the middle, see if you're
|
||||
before or after your word, then repeat in the appropriate half.
|
||||
|
||||
core_concept: |
|
||||
Binary search works because the data has **monotonic ordering**—if you find
|
||||
something too small, everything before it is also too small. If something is
|
||||
too big, everything after is also too big.
|
||||
|
||||
The key insight extends beyond simple arrays:
|
||||
|
||||
1. **Value search**: Find a specific target in sorted array
|
||||
2. **Boundary search**: Find the first/last element satisfying a condition
|
||||
3. **Search space**: Binary search over answers (e.g., "minimum capacity")
|
||||
|
||||
At each step, you make one comparison and eliminate half the space. After k
|
||||
comparisons, you've narrowed n elements down to n/2^k. Solving n/2^k = 1
|
||||
gives k = log₂(n).
|
||||
|
||||
visualization: |
|
||||
**Example: Find target = 7 in sorted array**
|
||||
|
||||
```
|
||||
Array: [1, 3, 5, 7, 9, 11, 13]
|
||||
L M R
|
||||
|
||||
Step 1: mid = 7, target = 7 → Found!
|
||||
```
|
||||
|
||||
**Example: Find target = 9**
|
||||
|
||||
```
|
||||
[1, 3, 5, 7, 9, 11, 13]
|
||||
L M R
|
||||
|
||||
Step 1: mid = 7 < 9 → Search right half
|
||||
L M R
|
||||
|
||||
Step 2: mid = 11 > 9 → Search left half
|
||||
L
|
||||
M
|
||||
R
|
||||
|
||||
Step 3: mid = 9 → Found at index 4!
|
||||
```
|
||||
|
||||
**Binary search on answer: Minimum capacity to ship packages in D days**
|
||||
|
||||
```
|
||||
Search space: [max(weights), sum(weights)]
|
||||
|
||||
mid = some capacity
|
||||
Can ship in D days with capacity mid?
|
||||
Yes → try smaller capacity (go left)
|
||||
No → need more capacity (go right)
|
||||
```
|
||||
|
||||
code_template: |
|
||||
def binary_search(arr: list, target: int) -> int:
|
||||
"""Classic binary search for exact match."""
|
||||
left, right = 0, len(arr) - 1
|
||||
|
||||
while left <= right:
|
||||
mid = left + (right - left) // 2 # Avoid overflow
|
||||
|
||||
if arr[mid] == target:
|
||||
return mid
|
||||
elif arr[mid] < target:
|
||||
left = mid + 1
|
||||
else:
|
||||
right = mid - 1
|
||||
|
||||
return -1 # Not found
|
||||
|
||||
|
||||
def lower_bound(arr: list, target: int) -> int:
|
||||
"""Find first position where arr[i] >= target."""
|
||||
left, right = 0, len(arr)
|
||||
|
||||
while left < right:
|
||||
mid = left + (right - left) // 2
|
||||
|
||||
if arr[mid] < target:
|
||||
left = mid + 1
|
||||
else:
|
||||
right = mid
|
||||
|
||||
return left # First valid position
|
||||
|
||||
|
||||
def binary_search_answer(low: int, high: int, is_valid) -> int:
|
||||
"""Binary search on answer space."""
|
||||
while low < high:
|
||||
mid = low + (high - low) // 2
|
||||
|
||||
if is_valid(mid):
|
||||
high = mid # Try smaller
|
||||
else:
|
||||
low = mid + 1 # Need bigger
|
||||
|
||||
return low
|
||||
|
||||
recognition_signals:
|
||||
- "sorted array"
|
||||
- "O(log n)"
|
||||
- "find minimum/maximum"
|
||||
- "find first/last occurrence"
|
||||
- "rotated sorted array"
|
||||
- "peak element"
|
||||
- "minimum capacity"
|
||||
- "search space"
|
||||
- "lower bound"
|
||||
- "upper bound"
|
||||
|
||||
common_mistakes:
|
||||
- title: Integer overflow in mid calculation
|
||||
description: |
|
||||
Using `(left + right) / 2` can overflow if left and right are both
|
||||
large positive integers (in languages with fixed-size integers).
|
||||
fix: |
|
||||
Use `left + (right - left) // 2` instead. This is mathematically
|
||||
equivalent but avoids overflow.
|
||||
|
||||
- title: Infinite loop with wrong boundary update
|
||||
description: |
|
||||
Using `right = mid` with `left <= right` condition, or `left = mid`
|
||||
when mid could equal left, causes infinite loops.
|
||||
fix: |
|
||||
For `left <= right`, always use `left = mid + 1` and `right = mid - 1`.
|
||||
For `left < right`, use `left = mid + 1` and `right = mid`.
|
||||
|
||||
- title: Off-by-one with boundary search
|
||||
description: |
|
||||
Returning `left` when you should return `left - 1` (or vice versa)
|
||||
gives the wrong boundary element.
|
||||
fix: |
|
||||
Think carefully about loop invariants. What does `left` represent when
|
||||
the loop ends? Test with edge cases.
|
||||
|
||||
- title: Not recognizing binary search applicability
|
||||
description: |
|
||||
Missing that a problem can use binary search because the "array" is
|
||||
implicit (search space of possible answers).
|
||||
fix: |
|
||||
If you need to minimize/maximize something and can write a function
|
||||
`is_valid(x)` that's monotonic, binary search applies.
|
||||
|
||||
variations:
|
||||
- name: Classic search
|
||||
description: |
|
||||
Find exact target in sorted array. Returns index or -1.
|
||||
example: "Binary Search, Search Insert Position"
|
||||
|
||||
- name: Lower/Upper bound
|
||||
description: |
|
||||
Find first or last position satisfying a condition. Used for ranges
|
||||
and counting occurrences.
|
||||
example: "First Bad Version, Find First and Last Position"
|
||||
|
||||
- name: Rotated array
|
||||
description: |
|
||||
Sorted array rotated at some pivot. One half is always sorted—determine
|
||||
which half and search appropriately.
|
||||
example: "Search in Rotated Sorted Array, Find Minimum"
|
||||
|
||||
- name: Binary search on answer
|
||||
description: |
|
||||
Search the space of possible answers. Need a monotonic predicate function
|
||||
to determine feasibility.
|
||||
example: "Capacity To Ship Packages, Koko Eating Bananas, Split Array Largest Sum"
|
||||
|
||||
- name: Peak finding
|
||||
description: |
|
||||
Find local maximum in bitonic array. Compare mid with neighbors to
|
||||
determine which side has the peak.
|
||||
example: "Find Peak Element, Find in Mountain Array"
|
||||
|
||||
related_patterns:
|
||||
- two-pointers
|
||||
|
||||
prerequisite_patterns: []
|
||||
224
backend/data/patterns/dynamic-programming.yaml
Normal file
224
backend/data/patterns/dynamic-programming.yaml
Normal file
@@ -0,0 +1,224 @@
|
||||
name: Dynamic Programming
|
||||
slug: dynamic-programming
|
||||
difficulty_level: 4
|
||||
|
||||
description: >
|
||||
Break problems into overlapping subproblems, storing results to avoid
|
||||
recomputation. This transforms exponential time complexity into polynomial
|
||||
by trading space for time.
|
||||
|
||||
when_to_use: |
|
||||
- Optimization problems (min/max)
|
||||
- Counting problems
|
||||
- Problems with optimal substructure
|
||||
- Sequence alignment
|
||||
- Knapsack-type problems
|
||||
|
||||
metaphor: |
|
||||
Imagine building with LEGO bricks. Instead of reconstructing the same base
|
||||
structure every time you try a new top, you save your work. Each completed
|
||||
substructure becomes a building block for larger structures.
|
||||
|
||||
Another analogy: calculating Fibonacci numbers. To find fib(5), you need
|
||||
fib(4) and fib(3). But fib(4) also needs fib(3). Rather than recalculating
|
||||
fib(3) twice, save it the first time and reuse it.
|
||||
|
||||
core_concept: |
|
||||
Dynamic programming requires two properties:
|
||||
|
||||
1. **Optimal substructure**: The optimal solution contains optimal solutions
|
||||
to its subproblems.
|
||||
|
||||
2. **Overlapping subproblems**: The same subproblems are solved multiple
|
||||
times in a naive recursive approach.
|
||||
|
||||
The key insight is identifying the **state**—what information do you need
|
||||
to solve a subproblem? And the **transition**—how do you combine smaller
|
||||
subproblems into larger ones?
|
||||
|
||||
Two implementation approaches:
|
||||
- **Top-down (memoization)**: Recursive with caching
|
||||
- **Bottom-up (tabulation)**: Iterative, filling a table from base cases
|
||||
|
||||
visualization: |
|
||||
**Example: Fibonacci with memoization**
|
||||
|
||||
```
|
||||
Without memoization (exponential calls):
|
||||
fib(5)
|
||||
/ \
|
||||
fib(4) fib(3)
|
||||
/ \ / \
|
||||
fib(3) fib(2) fib(2) fib(1)
|
||||
/ \
|
||||
fib(2) fib(1)
|
||||
...
|
||||
|
||||
With memoization:
|
||||
fib(5) → fib(4) → fib(3) → fib(2) → fib(1)
|
||||
↓ ↓
|
||||
use cached use cached
|
||||
fib(3) fib(2)
|
||||
```
|
||||
|
||||
**Example: Coin Change (minimum coins for amount 11)**
|
||||
|
||||
```
|
||||
Coins: [1, 5, 6] Amount: 11
|
||||
|
||||
dp[0] = 0 (base case: 0 coins for amount 0)
|
||||
|
||||
dp[1] = dp[0] + 1 = 1 (use coin 1)
|
||||
dp[5] = min(dp[4]+1, dp[0]+1) = 1 (use coin 5)
|
||||
dp[6] = min(dp[5]+1, dp[0]+1) = 1 (use coin 6)
|
||||
|
||||
dp[11] = min(dp[10]+1, dp[6]+1, dp[5]+1)
|
||||
= min(?, 2, 3)
|
||||
= 2 (6 + 5)
|
||||
```
|
||||
|
||||
code_template: |
|
||||
# Top-down (memoization)
|
||||
from functools import lru_cache
|
||||
|
||||
def solve_top_down(n: int) -> int:
|
||||
@lru_cache(maxsize=None)
|
||||
def dp(state):
|
||||
# Base case
|
||||
if base_condition(state):
|
||||
return base_value
|
||||
|
||||
# Recursive case with memoization
|
||||
result = initial_value
|
||||
for choice in choices(state):
|
||||
subproblem = dp(next_state(state, choice))
|
||||
result = combine(result, subproblem)
|
||||
|
||||
return result
|
||||
|
||||
return dp(initial_state(n))
|
||||
|
||||
|
||||
# Bottom-up (tabulation)
|
||||
def solve_bottom_up(n: int) -> int:
|
||||
# Initialize DP table
|
||||
dp = [initial_value] * (n + 1)
|
||||
|
||||
# Base case
|
||||
dp[0] = base_value
|
||||
|
||||
# Fill table iteratively
|
||||
for i in range(1, n + 1):
|
||||
for choice in choices(i):
|
||||
if valid(i, choice):
|
||||
dp[i] = combine(dp[i], dp[prev_state(i, choice)])
|
||||
|
||||
return dp[n]
|
||||
|
||||
|
||||
# 2D DP example (Longest Common Subsequence)
|
||||
def lcs(text1: str, text2: str) -> int:
|
||||
m, n = len(text1), len(text2)
|
||||
dp = [[0] * (n + 1) for _ in range(m + 1)]
|
||||
|
||||
for i in range(1, m + 1):
|
||||
for j in range(1, n + 1):
|
||||
if text1[i-1] == text2[j-1]:
|
||||
dp[i][j] = dp[i-1][j-1] + 1
|
||||
else:
|
||||
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
|
||||
|
||||
return dp[m][n]
|
||||
|
||||
recognition_signals:
|
||||
- "minimum/maximum"
|
||||
- "count ways"
|
||||
- "can you reach"
|
||||
- "optimal"
|
||||
- "longest/shortest"
|
||||
- "number of ways"
|
||||
- "subset sum"
|
||||
- "partition"
|
||||
- "knapsack"
|
||||
- "sequence"
|
||||
- "subsequence"
|
||||
|
||||
common_mistakes:
|
||||
- title: Incorrect state definition
|
||||
description: |
|
||||
Choosing a state that doesn't capture all necessary information leads
|
||||
to incorrect transitions or missing cases.
|
||||
fix: |
|
||||
Ask: "What do I need to know to solve this subproblem?" The answer
|
||||
defines your state. Test with small examples to verify.
|
||||
|
||||
- title: Wrong base case
|
||||
description: |
|
||||
Incorrect initialization causes wrong answers to propagate through
|
||||
the entire DP table.
|
||||
fix: |
|
||||
Think about the smallest/simplest subproblem. What's the answer when
|
||||
there's nothing left to consider? Start from there.
|
||||
|
||||
- title: Off-by-one in 2D DP
|
||||
description: |
|
||||
Confusion about whether dp[i] represents the first i elements or the
|
||||
element at index i causes index errors.
|
||||
fix: |
|
||||
Be consistent. Common convention: dp[i] = answer for first i elements,
|
||||
so dp[0] = empty case. Indices in strings/arrays are 0-based.
|
||||
|
||||
- title: Forgetting to handle impossible cases
|
||||
description: |
|
||||
Not returning infinity for minimum problems or 0 for counting when
|
||||
a state is unreachable gives wrong aggregations.
|
||||
fix: |
|
||||
Initialize dp with appropriate "impossible" values (infinity for min,
|
||||
-infinity for max, 0 for counting). Return -1 if final answer is
|
||||
still impossible.
|
||||
|
||||
- title: Space complexity not optimized
|
||||
description: |
|
||||
Using O(n*m) space when only the previous row/column is needed
|
||||
wastes memory on large inputs.
|
||||
fix: |
|
||||
If dp[i] only depends on dp[i-1], use two arrays (current and previous)
|
||||
or even a single array updated carefully.
|
||||
|
||||
variations:
|
||||
- name: 1D DP
|
||||
description: |
|
||||
Single dimension state, typically indexed by position or remaining
|
||||
capacity. Common for linear sequences.
|
||||
example: "Climbing Stairs, House Robber, Coin Change"
|
||||
|
||||
- name: 2D DP
|
||||
description: |
|
||||
Two-dimensional state, often for comparing two sequences or tracking
|
||||
two variables (position and capacity).
|
||||
example: "Longest Common Subsequence, Edit Distance, 0/1 Knapsack"
|
||||
|
||||
- name: Interval DP
|
||||
description: |
|
||||
State represents a range [i, j]. Solve for all subranges and combine.
|
||||
Often O(n^3) time.
|
||||
example: "Burst Balloons, Matrix Chain Multiplication"
|
||||
|
||||
- name: Bitmask DP
|
||||
description: |
|
||||
State includes a bitmask representing a subset. Used when order matters
|
||||
among a small set of items.
|
||||
example: "Traveling Salesman, Shortest Superstring"
|
||||
|
||||
- name: DP on Trees
|
||||
description: |
|
||||
State associated with tree nodes. Transition from children to parent
|
||||
(or vice versa).
|
||||
example: "House Robber III, Binary Tree Maximum Path Sum"
|
||||
|
||||
related_patterns:
|
||||
- greedy
|
||||
- backtracking
|
||||
|
||||
prerequisite_patterns:
|
||||
- backtracking
|
||||
194
backend/data/patterns/sliding-window.yaml
Normal file
194
backend/data/patterns/sliding-window.yaml
Normal file
@@ -0,0 +1,194 @@
|
||||
name: Sliding Window
|
||||
slug: sliding-window
|
||||
difficulty_level: 2
|
||||
|
||||
description: >
|
||||
Maintain a window of elements that slides through the data, tracking a
|
||||
constraint or computing aggregates. This transforms O(n*k) brute force into
|
||||
O(n) by incrementally updating the window instead of recalculating from scratch.
|
||||
|
||||
when_to_use: |
|
||||
- Finding subarrays/substrings with specific properties
|
||||
- Maximum/minimum sum of fixed-size windows
|
||||
- Longest substring with at most K distinct characters
|
||||
- Problems mentioning "contiguous" elements
|
||||
|
||||
metaphor: |
|
||||
Imagine looking at a landscape through a train window. As the train moves
|
||||
forward, the scenery at the back of your view disappears while new scenery
|
||||
appears at the front. You don't need to memorize the entire journey—just
|
||||
keep track of what's currently visible through your window.
|
||||
|
||||
Another analogy: a cashier's sliding tray at a bank. As new items are added
|
||||
to one end, old items fall off the other. You only count what's on the tray
|
||||
at any moment.
|
||||
|
||||
core_concept: |
|
||||
The **sliding window** technique avoids redundant computation by maintaining
|
||||
state as the window moves. Instead of recalculating the entire window each
|
||||
time, you *add* what enters and *remove* what leaves.
|
||||
|
||||
There are two main types:
|
||||
|
||||
1. **Fixed-size window**: Window size is constant (e.g., "find max sum of k elements")
|
||||
2. **Variable-size window**: Window expands and contracts based on constraints
|
||||
(e.g., "longest substring with at most 2 distinct characters")
|
||||
|
||||
The key insight is that consecutive windows share most of their elements.
|
||||
Only the edges change, so only update those.
|
||||
|
||||
visualization: |
|
||||
**Example: Maximum sum of 3 consecutive elements**
|
||||
|
||||
```
|
||||
Array: [2, 1, 5, 1, 3, 2] Window size: 3
|
||||
|
||||
Window 1: [2, 1, 5] = 8 (calculate full sum)
|
||||
└─────┘
|
||||
|
||||
Window 2: [1, 5, 1] = 8-2+1 = 7 (remove 2, add 1)
|
||||
└─────┘
|
||||
|
||||
Window 3: [5, 1, 3] = 7-1+3 = 9 (remove 1, add 3) ← Maximum!
|
||||
└─────┘
|
||||
|
||||
Window 4: [1, 3, 2] = 9-5+2 = 6 (remove 5, add 2)
|
||||
└─────┘
|
||||
```
|
||||
|
||||
**Variable window: Longest substring with at most 2 distinct chars**
|
||||
|
||||
```
|
||||
String: "eceba"
|
||||
|
||||
"e" → 1 distinct, expand → length 1
|
||||
"ec" → 2 distinct, expand → length 2
|
||||
"ece" → 2 distinct, expand → length 3 ← Answer!
|
||||
"eceb" → 3 distinct, shrink from left
|
||||
"ceb" → 3 distinct, shrink from left
|
||||
"eb" → 2 distinct, expand → length 2
|
||||
"eba" → 3 distinct, shrink...
|
||||
```
|
||||
|
||||
code_template: |
|
||||
def fixed_window(arr: list, k: int) -> int:
|
||||
"""Fixed-size sliding window."""
|
||||
n = len(arr)
|
||||
if n < k:
|
||||
return 0
|
||||
|
||||
# Calculate initial window
|
||||
window_sum = sum(arr[:k])
|
||||
max_sum = window_sum
|
||||
|
||||
# Slide the window
|
||||
for i in range(k, n):
|
||||
window_sum += arr[i] - arr[i - k] # Add new, remove old
|
||||
max_sum = max(max_sum, window_sum)
|
||||
|
||||
return max_sum
|
||||
|
||||
|
||||
def variable_window(s: str, k: int) -> int:
|
||||
"""Variable-size sliding window."""
|
||||
char_count = {}
|
||||
left = 0
|
||||
max_length = 0
|
||||
|
||||
for right in range(len(s)):
|
||||
# Expand: add character at right
|
||||
char_count[s[right]] = char_count.get(s[right], 0) + 1
|
||||
|
||||
# Contract: shrink from left if constraint violated
|
||||
while len(char_count) > k:
|
||||
char_count[s[left]] -= 1
|
||||
if char_count[s[left]] == 0:
|
||||
del char_count[s[left]]
|
||||
left += 1
|
||||
|
||||
# Update answer
|
||||
max_length = max(max_length, right - left + 1)
|
||||
|
||||
return max_length
|
||||
|
||||
recognition_signals:
|
||||
- "contiguous subarray"
|
||||
- "substring"
|
||||
- "maximum sum of k elements"
|
||||
- "window"
|
||||
- "consecutive"
|
||||
- "at most k distinct"
|
||||
- "minimum window"
|
||||
- "longest substring"
|
||||
- "sliding"
|
||||
|
||||
common_mistakes:
|
||||
- title: Forgetting to handle window smaller than required
|
||||
description: |
|
||||
When array length is less than window size k, trying to create a window
|
||||
causes index errors or incorrect results.
|
||||
fix: |
|
||||
Add an early check:
|
||||
```python
|
||||
if len(arr) < k:
|
||||
return 0 # or appropriate default
|
||||
```
|
||||
|
||||
- title: Off-by-one in variable window
|
||||
description: |
|
||||
When calculating window length, using `right - left` instead of
|
||||
`right - left + 1` gives length off by one.
|
||||
fix: |
|
||||
Window length is always `right - left + 1` (inclusive on both ends).
|
||||
|
||||
- title: Not cleaning up empty entries in hash map
|
||||
description: |
|
||||
When shrinking a variable window, decrementing a counter to 0 but not
|
||||
removing the key causes the distinct count to be wrong.
|
||||
fix: |
|
||||
Always delete keys when count reaches 0:
|
||||
```python
|
||||
if char_count[s[left]] == 0:
|
||||
del char_count[s[left]]
|
||||
```
|
||||
|
||||
- title: Updating answer at wrong time
|
||||
description: |
|
||||
For "minimum" problems, updating the answer inside the while loop
|
||||
captures invalid states. For "maximum" problems, updating only inside
|
||||
the while loop misses valid states.
|
||||
fix: |
|
||||
For maximum problems, update after expanding. For minimum problems,
|
||||
update when the constraint is first satisfied (inside the while loop).
|
||||
|
||||
variations:
|
||||
- name: Fixed-size window
|
||||
description: |
|
||||
Window size stays constant throughout. Simple slide operation: add one
|
||||
element, remove one element.
|
||||
example: "Maximum Sum Subarray of Size K, Find All Anagrams"
|
||||
|
||||
- name: Variable-size (shrinkable)
|
||||
description: |
|
||||
Window expands freely but contracts when constraints are violated.
|
||||
Uses a while loop to shrink until valid.
|
||||
example: "Longest Substring Without Repeating, Minimum Window Substring"
|
||||
|
||||
- name: Two-pointer variant
|
||||
description: |
|
||||
Some problems use two pointers that feel like sliding window but track
|
||||
different metrics. The mechanics are similar.
|
||||
example: "Container With Most Water, Trapping Rain Water"
|
||||
|
||||
- name: Caterpillar method
|
||||
description: |
|
||||
Another name for the variable sliding window, emphasizing how the window
|
||||
stretches and contracts like a caterpillar moving.
|
||||
example: "Common in competitive programming contexts"
|
||||
|
||||
related_patterns:
|
||||
- two-pointers
|
||||
- prefix-sum
|
||||
|
||||
prerequisite_patterns:
|
||||
- two-pointers
|
||||
166
backend/data/patterns/two-pointers.yaml
Normal file
166
backend/data/patterns/two-pointers.yaml
Normal file
@@ -0,0 +1,166 @@
|
||||
name: Two Pointers
|
||||
slug: two-pointers
|
||||
difficulty_level: 2
|
||||
|
||||
description: >
|
||||
Use two pointers to traverse data from different positions, often moving
|
||||
toward or away from each other. This technique transforms O(n²) brute force
|
||||
into O(n) by eliminating redundant comparisons.
|
||||
|
||||
when_to_use: |
|
||||
- Sorted arrays where you need to find pairs
|
||||
- Linked list cycle detection
|
||||
- Removing duplicates in-place
|
||||
- Partitioning arrays
|
||||
- Palindrome checking
|
||||
|
||||
metaphor: |
|
||||
Imagine two people reading a book from opposite ends, each moving toward the
|
||||
middle. The person at the back skips ahead when they find what they're looking
|
||||
for, while the person at the front moves forward when they don't match. They
|
||||
meet somewhere in the middle, having searched the entire book without either
|
||||
person reading the same page twice.
|
||||
|
||||
Another way to think about it: squeezing toothpaste from both ends of the
|
||||
tube. You apply pressure from each side, working toward the center until
|
||||
you've gotten everything out.
|
||||
|
||||
core_concept: |
|
||||
The **two pointers** technique eliminates the need for nested loops by
|
||||
maintaining two positions that move through the data based on conditions.
|
||||
|
||||
The key insight is that when data has *structure* (like being sorted), you
|
||||
can make intelligent decisions about which pointer to move. If the current
|
||||
pair is too small, moving the left pointer right increases the sum. If it's
|
||||
too large, moving the right pointer left decreases it.
|
||||
|
||||
This reduces O(n²) brute force (checking all pairs) to O(n) because each
|
||||
element is visited at most twice—once by each pointer.
|
||||
|
||||
visualization: |
|
||||
**Example: Find pair with sum = 10 in sorted array**
|
||||
|
||||
```
|
||||
Array: [1, 2, 4, 6, 8, 10] Target: 10
|
||||
L R
|
||||
|
||||
Step 1: 1 + 10 = 11 > 10 → Sum too large, move R left
|
||||
L R
|
||||
|
||||
Step 2: 1 + 8 = 9 < 10 → Sum too small, move L right
|
||||
L R
|
||||
|
||||
Step 3: 2 + 8 = 10 ✓ → Found! Return [1, 4]
|
||||
```
|
||||
|
||||
**Key insight**: Because the array is sorted, we know exactly which pointer
|
||||
to move. Too big? Decrease the larger value. Too small? Increase the smaller.
|
||||
|
||||
code_template: |
|
||||
def two_pointers(arr: list, target: int) -> list:
|
||||
"""Two pointers converging from opposite ends."""
|
||||
left, right = 0, len(arr) - 1
|
||||
|
||||
while left < right:
|
||||
current = arr[left] + arr[right]
|
||||
|
||||
if current == target:
|
||||
return [left, right] # Found!
|
||||
elif current < target:
|
||||
left += 1 # Need larger sum
|
||||
else:
|
||||
right -= 1 # Need smaller sum
|
||||
|
||||
return [] # No solution found
|
||||
|
||||
|
||||
def two_pointers_same_direction(arr: list) -> int:
|
||||
"""Two pointers moving in same direction (slow/fast)."""
|
||||
slow = 0
|
||||
|
||||
for fast in range(len(arr)):
|
||||
if some_condition(arr[fast]):
|
||||
arr[slow] = arr[fast]
|
||||
slow += 1
|
||||
|
||||
return slow # New length
|
||||
|
||||
recognition_signals:
|
||||
- "sorted array"
|
||||
- "find pair with sum"
|
||||
- "two sum"
|
||||
- "in-place modification"
|
||||
- "remove duplicates"
|
||||
- "partition array"
|
||||
- "palindrome"
|
||||
- "container with most water"
|
||||
- "trapping rain water"
|
||||
- "move zeros"
|
||||
|
||||
common_mistakes:
|
||||
- title: Off-by-one with boundaries
|
||||
description: |
|
||||
Using `<=` instead of `<` when pointers should not overlap causes
|
||||
infinite loops or double-counting elements.
|
||||
fix: |
|
||||
For converging pointers, use `while left < right`. Only use `<=` when
|
||||
the same element can be part of the answer twice.
|
||||
|
||||
- title: Not handling duplicates
|
||||
description: |
|
||||
When the problem asks for unique pairs, forgetting to skip duplicate
|
||||
values leads to repeated answers.
|
||||
fix: |
|
||||
After finding a match, skip over duplicates:
|
||||
```python
|
||||
while left < right and arr[left] == arr[left + 1]:
|
||||
left += 1
|
||||
```
|
||||
|
||||
- title: Moving both pointers at once
|
||||
description: |
|
||||
Moving both pointers simultaneously after finding a match can skip
|
||||
valid solutions.
|
||||
fix: |
|
||||
Move one pointer at a time and let the next iteration decide the other.
|
||||
After a match, move both only when you've recorded the result.
|
||||
|
||||
- title: Forgetting the sorted requirement
|
||||
description: |
|
||||
Two pointers only works predictably on sorted data. Applying it to
|
||||
unsorted arrays gives wrong results.
|
||||
fix: |
|
||||
Sort first if needed (adds O(n log n)), or use a hash map approach
|
||||
instead if sorting changes the problem semantics.
|
||||
|
||||
variations:
|
||||
- name: Opposite-direction (converging)
|
||||
description: |
|
||||
Pointers start at opposite ends and move toward each other. Used for
|
||||
pair problems in sorted arrays.
|
||||
example: "Two Sum II, Container With Most Water, Valid Palindrome"
|
||||
|
||||
- name: Same-direction (fast-slow)
|
||||
description: |
|
||||
Both pointers start at the same end but move at different speeds or
|
||||
based on different conditions. Used for in-place modifications.
|
||||
example: "Remove Duplicates, Move Zeros, Remove Element"
|
||||
|
||||
- name: Sliding window variant
|
||||
description: |
|
||||
Two pointers defining a window that expands and contracts. Technically
|
||||
a separate pattern but uses similar mechanics.
|
||||
example: "Minimum Window Substring, Longest Substring Without Repeating"
|
||||
|
||||
- name: Three pointers
|
||||
description: |
|
||||
Extension with three pointers for problems involving triplets or
|
||||
partitioning into three sections.
|
||||
example: "3Sum, Sort Colors (Dutch National Flag)"
|
||||
|
||||
related_patterns:
|
||||
- sliding-window
|
||||
- fast-slow-pointers
|
||||
- binary-search
|
||||
|
||||
prerequisite_patterns: []
|
||||
Reference in New Issue
Block a user