282 lines
9.9 KiB
YAML
282 lines
9.9 KiB
YAML
name: Two Pointers
|
|
slug: two-pointers
|
|
difficulty_level: 2
|
|
pattern_type: algorithm
|
|
display_order: 1
|
|
|
|
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: []
|
|
|
|
visualization_examples:
|
|
- id: "find-pair-sum"
|
|
title: "Find pair with sum = 10"
|
|
input:
|
|
array: [1, 2, 4, 6, 8, 10]
|
|
target: 10
|
|
code: |
|
|
left, right = 0, len(arr) - 1
|
|
while left < right:
|
|
curr = arr[left] + arr[right]
|
|
if curr == target:
|
|
return [left, right]
|
|
elif curr < target:
|
|
left += 1
|
|
else:
|
|
right -= 1
|
|
steps:
|
|
- id: "1"
|
|
description: "Initialize pointers at both ends of the sorted array. Left starts at index 0 (value 1), right starts at index 5 (value 10)."
|
|
structures:
|
|
array:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: active, annotations: ["L"] }
|
|
- { value: 2, state: default }
|
|
- { value: 4, state: default }
|
|
- { value: 6, state: default }
|
|
- { value: 8, state: default }
|
|
- { value: 10, state: active, annotations: ["R"] }
|
|
pointers: { left: 0, right: 5 }
|
|
variables: { left: 0, right: 5, target: 10 }
|
|
codeHighlight: { startLine: 1, endLine: 1 }
|
|
|
|
- id: "2"
|
|
description: "Calculate current sum: 1 + 10 = 11. This is greater than target (10), so we need a smaller sum. Move right pointer left."
|
|
structures:
|
|
array:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: comparing, annotations: ["L"] }
|
|
- { value: 2, state: default }
|
|
- { value: 4, state: default }
|
|
- { value: 6, state: default }
|
|
- { value: 8, state: default }
|
|
- { value: 10, state: comparing, annotations: ["R"] }
|
|
pointers: { left: 0, right: 5 }
|
|
variables: { left: 0, right: 5, curr: 11, target: 10 }
|
|
codeHighlight: { startLine: 3, endLine: 8 }
|
|
|
|
- id: "3"
|
|
description: "Right pointer moved to index 4 (value 8). Now checking new sum."
|
|
structures:
|
|
array:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: active, annotations: ["L"] }
|
|
- { value: 2, state: default }
|
|
- { value: 4, state: default }
|
|
- { value: 6, state: default }
|
|
- { value: 8, state: active, annotations: ["R"] }
|
|
- { value: 10, state: visited }
|
|
pointers: { left: 0, right: 4 }
|
|
variables: { left: 0, right: 4, target: 10 }
|
|
codeHighlight: { startLine: 8, endLine: 8 }
|
|
|
|
- id: "4"
|
|
description: "Calculate current sum: 1 + 8 = 9. This is less than target (10), so we need a larger sum. Move left pointer right."
|
|
structures:
|
|
array:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: comparing, annotations: ["L"] }
|
|
- { value: 2, state: default }
|
|
- { value: 4, state: default }
|
|
- { value: 6, state: default }
|
|
- { value: 8, state: comparing, annotations: ["R"] }
|
|
- { value: 10, state: visited }
|
|
pointers: { left: 0, right: 4 }
|
|
variables: { left: 0, right: 4, curr: 9, target: 10 }
|
|
codeHighlight: { startLine: 3, endLine: 7 }
|
|
|
|
- id: "5"
|
|
description: "Left pointer moved to index 1 (value 2). Now checking new sum."
|
|
structures:
|
|
array:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: visited }
|
|
- { value: 2, state: active, annotations: ["L"] }
|
|
- { value: 4, state: default }
|
|
- { value: 6, state: default }
|
|
- { value: 8, state: active, annotations: ["R"] }
|
|
- { value: 10, state: visited }
|
|
pointers: { left: 1, right: 4 }
|
|
variables: { left: 1, right: 4, target: 10 }
|
|
codeHighlight: { startLine: 7, endLine: 7 }
|
|
|
|
- id: "6"
|
|
description: "Calculate current sum: 2 + 8 = 10. This equals the target! We found our pair at indices [1, 4]."
|
|
structures:
|
|
array:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: visited }
|
|
- { value: 2, state: found, annotations: ["L"] }
|
|
- { value: 4, state: default }
|
|
- { value: 6, state: default }
|
|
- { value: 8, state: found, annotations: ["R"] }
|
|
- { value: 10, state: visited }
|
|
pointers: { left: 1, right: 4 }
|
|
variables: { left: 1, right: 4, curr: 10, target: 10 }
|
|
codeHighlight: { startLine: 4, endLine: 5 }
|