feat(patterns): pointer/array tutorials
This commit is contained in:
298
backend/data/patterns/fast-slow-pointers.yaml
Normal file
298
backend/data/patterns/fast-slow-pointers.yaml
Normal file
@@ -0,0 +1,298 @@
|
||||
name: Fast & Slow Pointers
|
||||
slug: fast-slow-pointers
|
||||
difficulty_level: 2
|
||||
|
||||
description: >
|
||||
Use two pointers moving at different speeds to detect cycles, find midpoints,
|
||||
or identify patterns in sequences. The fast pointer advances twice as quickly,
|
||||
allowing detection of structural properties without extra space.
|
||||
|
||||
when_to_use: |
|
||||
- Detecting cycles in linked lists or sequences
|
||||
- Finding the middle of a linked list
|
||||
- Finding the start of a cycle
|
||||
- Happy number problem
|
||||
- Palindrome linked list verification
|
||||
|
||||
metaphor: |
|
||||
Imagine two runners on a circular track. If one runs twice as fast as the other,
|
||||
the fast runner will eventually lap the slow runner—they'll meet at some point
|
||||
on the track. This proves the track is circular (has a cycle).
|
||||
|
||||
Another analogy: finding the middle of a line of people. Have two people start
|
||||
at the front—one takes one step at a time, the other takes two. When the fast
|
||||
person reaches the end, the slow person is at the middle.
|
||||
|
||||
core_concept: |
|
||||
The **fast & slow pointers** technique (also called Floyd's cycle detection or
|
||||
"tortoise and hare") uses two pointers moving at different speeds:
|
||||
|
||||
- **Slow pointer**: Moves 1 step at a time
|
||||
- **Fast pointer**: Moves 2 steps at a time
|
||||
|
||||
Key insights:
|
||||
|
||||
1. **Cycle detection**: In a cyclic structure, fast will eventually catch up to
|
||||
slow (they'll meet inside the cycle). In a non-cyclic structure, fast will
|
||||
reach the end.
|
||||
|
||||
2. **Finding middle**: When fast reaches the end, slow is at the middle (fast
|
||||
traveled 2x the distance).
|
||||
|
||||
3. **Finding cycle start**: After detecting a cycle, reset one pointer to start.
|
||||
Move both at the same speed—they meet at the cycle start. (Mathematical proof:
|
||||
the distances work out perfectly.)
|
||||
|
||||
visualization: |
|
||||
**Cycle Detection:**
|
||||
|
||||
```
|
||||
List: 1 → 2 → 3 → 4 → 5
|
||||
↑ ↓
|
||||
7 ← 6
|
||||
|
||||
Step 1: slow=1, fast=1
|
||||
Step 2: slow=2, fast=3
|
||||
Step 3: slow=3, fast=5
|
||||
Step 4: slow=4, fast=7
|
||||
Step 5: slow=5, fast=4
|
||||
Step 6: slow=6, fast=6 ← They meet! Cycle exists.
|
||||
```
|
||||
|
||||
**Finding Middle:**
|
||||
|
||||
```
|
||||
List: 1 → 2 → 3 → 4 → 5 → null
|
||||
|
||||
Step 1: slow=1, fast=1
|
||||
Step 2: slow=2, fast=3
|
||||
Step 3: slow=3, fast=5
|
||||
Step 4: fast reaches null
|
||||
|
||||
slow is at middle (3)
|
||||
```
|
||||
|
||||
**Finding Cycle Start:**
|
||||
|
||||
```
|
||||
After detecting cycle at node X:
|
||||
|
||||
1. Reset slow to head, keep fast at meeting point
|
||||
2. Move both at same speed (1 step each)
|
||||
3. They meet at cycle start
|
||||
|
||||
Why? Math: Let's say:
|
||||
- Distance from head to cycle start = A
|
||||
- Distance from cycle start to meeting point = B
|
||||
- Cycle length = C
|
||||
|
||||
At meeting: slow traveled A + B
|
||||
fast traveled A + B + nC (some complete cycles)
|
||||
|
||||
Since fast travels 2x: 2(A + B) = A + B + nC
|
||||
Therefore: A + B = nC, so A = nC - B = (n-1)C + (C-B)
|
||||
|
||||
This means: distance from head to cycle start
|
||||
= distance from meeting point to cycle start (going forward)
|
||||
```
|
||||
|
||||
code_template: |
|
||||
class ListNode:
|
||||
def __init__(self, val=0, next=None):
|
||||
self.val = val
|
||||
self.next = next
|
||||
|
||||
|
||||
def has_cycle(head: ListNode) -> bool:
|
||||
"""Detect if linked list has a cycle."""
|
||||
if not head or not head.next:
|
||||
return False
|
||||
|
||||
slow = head
|
||||
fast = head
|
||||
|
||||
while fast and fast.next:
|
||||
slow = slow.next
|
||||
fast = fast.next.next
|
||||
|
||||
if slow == fast:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def find_cycle_start(head: ListNode) -> ListNode:
|
||||
"""Find the node where the cycle begins."""
|
||||
if not head or not head.next:
|
||||
return None
|
||||
|
||||
# Phase 1: Detect cycle
|
||||
slow = fast = head
|
||||
while fast and fast.next:
|
||||
slow = slow.next
|
||||
fast = fast.next.next
|
||||
if slow == fast:
|
||||
break
|
||||
else:
|
||||
return None # No cycle
|
||||
|
||||
# Phase 2: Find cycle start
|
||||
slow = head
|
||||
while slow != fast:
|
||||
slow = slow.next
|
||||
fast = fast.next
|
||||
|
||||
return slow
|
||||
|
||||
|
||||
def find_middle(head: ListNode) -> ListNode:
|
||||
"""Find the middle node of linked list."""
|
||||
if not head:
|
||||
return None
|
||||
|
||||
slow = fast = head
|
||||
|
||||
while fast and fast.next:
|
||||
slow = slow.next
|
||||
fast = fast.next.next
|
||||
|
||||
return slow # Middle (or second middle if even length)
|
||||
|
||||
|
||||
def is_happy_number(n: int) -> bool:
|
||||
"""Check if number is happy (sum of squared digits eventually = 1)."""
|
||||
def get_next(num: int) -> int:
|
||||
total = 0
|
||||
while num > 0:
|
||||
digit = num % 10
|
||||
total += digit * digit
|
||||
num //= 10
|
||||
return total
|
||||
|
||||
slow = n
|
||||
fast = get_next(n)
|
||||
|
||||
while fast != 1 and slow != fast:
|
||||
slow = get_next(slow)
|
||||
fast = get_next(get_next(fast))
|
||||
|
||||
return fast == 1
|
||||
|
||||
|
||||
def is_palindrome_linked_list(head: ListNode) -> bool:
|
||||
"""Check if linked list is a palindrome."""
|
||||
if not head or not head.next:
|
||||
return True
|
||||
|
||||
# Find middle
|
||||
slow = fast = head
|
||||
while fast and fast.next:
|
||||
slow = slow.next
|
||||
fast = fast.next.next
|
||||
|
||||
# Reverse second half
|
||||
prev = None
|
||||
while slow:
|
||||
next_node = slow.next
|
||||
slow.next = prev
|
||||
prev = slow
|
||||
slow = next_node
|
||||
|
||||
# Compare halves
|
||||
left, right = head, prev
|
||||
while right:
|
||||
if left.val != right.val:
|
||||
return False
|
||||
left = left.next
|
||||
right = right.next
|
||||
|
||||
return True
|
||||
|
||||
recognition_signals:
|
||||
- "linked list cycle"
|
||||
- "detect cycle"
|
||||
- "find middle"
|
||||
- "happy number"
|
||||
- "palindrome linked list"
|
||||
- "circular array"
|
||||
- "Floyd's"
|
||||
- "tortoise and hare"
|
||||
- "meeting point"
|
||||
- "cycle start"
|
||||
|
||||
common_mistakes:
|
||||
- title: Not checking fast.next before advancing
|
||||
description: |
|
||||
Accessing `fast.next.next` without first checking `fast.next` causes
|
||||
null pointer errors when the list has even length.
|
||||
fix: |
|
||||
Always check both `fast` and `fast.next`:
|
||||
```python
|
||||
while fast and fast.next:
|
||||
fast = fast.next.next
|
||||
```
|
||||
|
||||
- title: Wrong initialization for cycle detection
|
||||
description: |
|
||||
Starting slow and fast at different positions (e.g., slow=head, fast=head.next)
|
||||
changes the math for finding the cycle start.
|
||||
fix: |
|
||||
Start both at head for consistency. The algorithms are designed assuming
|
||||
both start at the same position.
|
||||
|
||||
- title: Forgetting to handle empty or single-node lists
|
||||
description: |
|
||||
Accessing head.next or head.next.next on empty or single-node lists
|
||||
causes errors.
|
||||
fix: |
|
||||
Add early returns:
|
||||
```python
|
||||
if not head or not head.next:
|
||||
return False # or appropriate value
|
||||
```
|
||||
|
||||
- title: Confusing meeting point with cycle start
|
||||
description: |
|
||||
Returning the meeting point instead of finding the actual cycle start
|
||||
gives the wrong answer.
|
||||
fix: |
|
||||
After detecting a cycle (meeting point), reset one pointer to head and
|
||||
advance both at the same speed to find the cycle start.
|
||||
|
||||
variations:
|
||||
- name: Cycle detection
|
||||
description: |
|
||||
Determine if a linked list or sequence has a cycle. If fast catches slow,
|
||||
there's a cycle.
|
||||
example: "Linked List Cycle, Circular Array Loop"
|
||||
|
||||
- name: Finding cycle start
|
||||
description: |
|
||||
After detecting a cycle, find the node where the cycle begins using the
|
||||
two-phase approach.
|
||||
example: "Linked List Cycle II"
|
||||
|
||||
- name: Finding middle
|
||||
description: |
|
||||
When fast reaches the end, slow is at the middle. Useful for divide and
|
||||
conquer on linked lists.
|
||||
example: "Middle of Linked List, Sort List (merge sort needs middle)"
|
||||
|
||||
- name: Happy number
|
||||
description: |
|
||||
Treat the sequence of digit-square sums as a linked list. Either reaches 1
|
||||
(happy) or cycles (unhappy).
|
||||
example: "Happy Number"
|
||||
|
||||
- name: Palindrome check
|
||||
description: |
|
||||
Find middle, reverse second half, compare. Combines finding middle with
|
||||
linked list reversal.
|
||||
example: "Palindrome Linked List"
|
||||
|
||||
related_patterns:
|
||||
- two-pointers
|
||||
- linkedlist-reversal
|
||||
|
||||
prerequisite_patterns:
|
||||
- two-pointers
|
||||
Reference in New Issue
Block a user