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
|
||||
275
backend/data/patterns/intervals.yaml
Normal file
275
backend/data/patterns/intervals.yaml
Normal file
@@ -0,0 +1,275 @@
|
||||
name: Overlapping Intervals
|
||||
slug: intervals
|
||||
difficulty_level: 2
|
||||
|
||||
description: >
|
||||
Process and manipulate intervals (ranges) that may share common regions.
|
||||
The key insight is that sorting intervals by start time allows efficient
|
||||
detection and handling of overlaps through a single linear pass.
|
||||
|
||||
when_to_use: |
|
||||
- Merging overlapping intervals
|
||||
- Inserting an interval into a sorted list
|
||||
- Finding gaps between intervals
|
||||
- Meeting room scheduling
|
||||
- Finding interval intersections
|
||||
|
||||
metaphor: |
|
||||
Imagine scheduling meeting rooms. Each meeting is an interval of time. When
|
||||
two meetings overlap, you need either two rooms or to merge them into one
|
||||
longer booking. By sorting meetings by start time, you can easily spot
|
||||
overlaps—if the next meeting starts before the current one ends, they overlap.
|
||||
|
||||
Another analogy: merging overlapping highlighter marks on a page. Sort them
|
||||
left to right, and if one mark starts before the previous ends, combine them
|
||||
into one continuous highlight.
|
||||
|
||||
core_concept: |
|
||||
The **interval pattern** relies on sorting intervals by start time. Once sorted,
|
||||
overlapping detection becomes simple:
|
||||
|
||||
**Two intervals [a, b] and [c, d] overlap if c <= b** (assuming a <= c after sorting)
|
||||
|
||||
Key operations:
|
||||
1. **Merge**: Extend the current interval's end to include overlapping intervals
|
||||
2. **Insert**: Find where the new interval overlaps and merge as needed
|
||||
3. **Count overlaps**: Track how many intervals are "active" at any point
|
||||
|
||||
For problems needing simultaneous tracking (like minimum meeting rooms), use
|
||||
a **sweep line** approach: sort all start and end points together, then sweep
|
||||
through counting active intervals.
|
||||
|
||||
visualization: |
|
||||
**Merging overlapping intervals:**
|
||||
|
||||
```
|
||||
Input: [[1,3], [2,6], [8,10], [15,18]]
|
||||
(sorted by start)
|
||||
|
||||
Process [1,3]: result = [[1,3]]
|
||||
|
||||
Process [2,6]: 2 <= 3? Yes, overlap!
|
||||
Merge: [1, max(3,6)] = [1,6]
|
||||
result = [[1,6]]
|
||||
|
||||
Process [8,10]: 8 <= 6? No, no overlap
|
||||
result = [[1,6], [8,10]]
|
||||
|
||||
Process [15,18]: 15 <= 10? No, no overlap
|
||||
result = [[1,6], [8,10], [15,18]]
|
||||
```
|
||||
|
||||
**Visualized on number line:**
|
||||
|
||||
```
|
||||
Before: [1---3]
|
||||
[2------6]
|
||||
[8--10]
|
||||
[15--18]
|
||||
|
||||
After: [1--------6] [8--10] [15--18]
|
||||
```
|
||||
|
||||
**Meeting rooms (sweep line):**
|
||||
|
||||
```
|
||||
Meetings: [[0,30], [5,10], [15,20]]
|
||||
|
||||
Events (sorted):
|
||||
time=0: +1 (start) active=1
|
||||
time=5: +1 (start) active=2 ← max
|
||||
time=10: -1 (end) active=1
|
||||
time=15: +1 (start) active=2 ← max
|
||||
time=20: -1 (end) active=1
|
||||
time=30: -1 (end) active=0
|
||||
|
||||
Max concurrent = 2 → need 2 meeting rooms
|
||||
```
|
||||
|
||||
code_template: |
|
||||
def merge_intervals(intervals: list[list[int]]) -> list[list[int]]:
|
||||
"""Merge overlapping intervals."""
|
||||
if not intervals:
|
||||
return []
|
||||
|
||||
# Sort by start time
|
||||
intervals.sort(key=lambda x: x[0])
|
||||
|
||||
result = [intervals[0]]
|
||||
|
||||
for start, end in intervals[1:]:
|
||||
last_end = result[-1][1]
|
||||
|
||||
if start <= last_end: # Overlap
|
||||
result[-1][1] = max(last_end, end) # Extend
|
||||
else:
|
||||
result.append([start, end])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def insert_interval(intervals: list[list[int]],
|
||||
new: list[int]) -> list[list[int]]:
|
||||
"""Insert and merge a new interval into sorted list."""
|
||||
result = []
|
||||
i = 0
|
||||
n = len(intervals)
|
||||
|
||||
# Add all intervals before new interval
|
||||
while i < n and intervals[i][1] < new[0]:
|
||||
result.append(intervals[i])
|
||||
i += 1
|
||||
|
||||
# Merge overlapping intervals with new
|
||||
while i < n and intervals[i][0] <= new[1]:
|
||||
new[0] = min(new[0], intervals[i][0])
|
||||
new[1] = max(new[1], intervals[i][1])
|
||||
i += 1
|
||||
|
||||
result.append(new)
|
||||
|
||||
# Add remaining intervals
|
||||
while i < n:
|
||||
result.append(intervals[i])
|
||||
i += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def min_meeting_rooms(intervals: list[list[int]]) -> int:
|
||||
"""Find minimum meeting rooms needed (sweep line)."""
|
||||
events = []
|
||||
|
||||
for start, end in intervals:
|
||||
events.append((start, 1)) # +1 for start
|
||||
events.append((end, -1)) # -1 for end
|
||||
|
||||
# Sort by time, with ends before starts at same time
|
||||
events.sort(key=lambda x: (x[0], x[1]))
|
||||
|
||||
max_rooms = 0
|
||||
current_rooms = 0
|
||||
|
||||
for _, delta in events:
|
||||
current_rooms += delta
|
||||
max_rooms = max(max_rooms, current_rooms)
|
||||
|
||||
return max_rooms
|
||||
|
||||
|
||||
def interval_intersection(A: list[list[int]],
|
||||
B: list[list[int]]) -> list[list[int]]:
|
||||
"""Find intersection of two sorted interval lists."""
|
||||
result = []
|
||||
i = j = 0
|
||||
|
||||
while i < len(A) and j < len(B):
|
||||
# Find overlap
|
||||
start = max(A[i][0], B[j][0])
|
||||
end = min(A[i][1], B[j][1])
|
||||
|
||||
if start <= end:
|
||||
result.append([start, end])
|
||||
|
||||
# Advance the interval that ends first
|
||||
if A[i][1] < B[j][1]:
|
||||
i += 1
|
||||
else:
|
||||
j += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def can_attend_all(intervals: list[list[int]]) -> bool:
|
||||
"""Check if a person can attend all meetings."""
|
||||
intervals.sort(key=lambda x: x[0])
|
||||
|
||||
for i in range(1, len(intervals)):
|
||||
if intervals[i][0] < intervals[i-1][1]:
|
||||
return False # Overlap found
|
||||
|
||||
return True
|
||||
|
||||
recognition_signals:
|
||||
- "intervals"
|
||||
- "merge"
|
||||
- "overlapping"
|
||||
- "meeting rooms"
|
||||
- "schedule"
|
||||
- "time slots"
|
||||
- "range"
|
||||
- "insert interval"
|
||||
- "non-overlapping"
|
||||
- "intersection"
|
||||
|
||||
common_mistakes:
|
||||
- title: Not sorting first
|
||||
description: |
|
||||
Trying to process unsorted intervals leads to incorrect results because
|
||||
overlaps aren't detected properly.
|
||||
fix: |
|
||||
Always sort intervals by start time before processing:
|
||||
```python
|
||||
intervals.sort(key=lambda x: x[0])
|
||||
```
|
||||
|
||||
- title: Wrong overlap condition
|
||||
description: |
|
||||
Using `start < last_end` instead of `start <= last_end` misses adjacent
|
||||
intervals that should be merged (like [1,2] and [2,3]).
|
||||
fix: |
|
||||
Use `<=` for touching intervals, `<` for strict overlap only. Check problem
|
||||
requirements for whether touching counts as overlapping.
|
||||
|
||||
- title: Not updating end correctly when merging
|
||||
description: |
|
||||
Setting `end = new_end` instead of `end = max(old_end, new_end)` fails when
|
||||
a smaller interval is contained within a larger one.
|
||||
fix: |
|
||||
Always take the maximum:
|
||||
```python
|
||||
result[-1][1] = max(result[-1][1], end)
|
||||
```
|
||||
|
||||
- title: Off-by-one with closed vs open intervals
|
||||
description: |
|
||||
Confusion about whether interval endpoints are inclusive `[a, b]` or
|
||||
exclusive `[a, b)` causes incorrect overlap detection.
|
||||
fix: |
|
||||
Clarify the convention from the problem. Most problems use closed intervals
|
||||
where both endpoints are included.
|
||||
|
||||
variations:
|
||||
- name: Merge intervals
|
||||
description: |
|
||||
Combine all overlapping intervals into non-overlapping intervals.
|
||||
example: "Merge Intervals"
|
||||
|
||||
- name: Insert interval
|
||||
description: |
|
||||
Insert a new interval into a sorted list, merging with any overlapping
|
||||
intervals.
|
||||
example: "Insert Interval"
|
||||
|
||||
- name: Meeting rooms
|
||||
description: |
|
||||
Find minimum resources needed for concurrent intervals using sweep line
|
||||
or min-heap.
|
||||
example: "Meeting Rooms II, Car Pooling"
|
||||
|
||||
- name: Interval intersection
|
||||
description: |
|
||||
Find common regions between two lists of intervals using two pointers.
|
||||
example: "Interval List Intersections"
|
||||
|
||||
- name: Non-overlapping intervals
|
||||
description: |
|
||||
Find minimum removals to make all intervals non-overlapping. Greedy
|
||||
approach: keep intervals that end earliest.
|
||||
example: "Non-overlapping Intervals, Erase Overlap Intervals"
|
||||
|
||||
related_patterns:
|
||||
- greedy
|
||||
- two-pointers
|
||||
|
||||
prerequisite_patterns: []
|
||||
279
backend/data/patterns/linkedlist-reversal.yaml
Normal file
279
backend/data/patterns/linkedlist-reversal.yaml
Normal file
@@ -0,0 +1,279 @@
|
||||
name: LinkedList In-Place Reversal
|
||||
slug: linkedlist-reversal
|
||||
difficulty_level: 2
|
||||
|
||||
description: >
|
||||
Reverse linked list nodes in-place by manipulating pointers without allocating
|
||||
extra space. This technique uses three pointers to track the previous, current,
|
||||
and next nodes while systematically reversing the direction of links.
|
||||
|
||||
when_to_use: |
|
||||
- Reversing an entire linked list
|
||||
- Reversing a portion of a linked list
|
||||
- Reversing in groups of K nodes
|
||||
- Palindrome linked list verification
|
||||
- Reordering list problems
|
||||
|
||||
metaphor: |
|
||||
Imagine a conga line where everyone faces forward. To reverse it, you don't
|
||||
rearrange people—you have each person turn around and grab the shoulders of
|
||||
whoever was behind them. You process one person at a time: they turn around,
|
||||
the next person steps forward, and so on until everyone faces the opposite direction.
|
||||
|
||||
Another analogy: reversing a chain of paper clips. You unclip each one from its
|
||||
forward neighbor and clip it to its backward neighbor, working through the chain.
|
||||
|
||||
core_concept: |
|
||||
Linked list reversal uses **three pointers** moving through the list:
|
||||
|
||||
- **prev**: Points to the already-reversed portion (starts as null)
|
||||
- **curr**: The node currently being processed
|
||||
- **next**: Temporarily stores the next node before we break the link
|
||||
|
||||
At each step:
|
||||
1. Save `curr.next` in `next` (before we lose it)
|
||||
2. Reverse the link: `curr.next = prev`
|
||||
3. Advance: `prev = curr`, `curr = next`
|
||||
|
||||
The key insight is that we're not moving nodes—we're redirecting pointers.
|
||||
This achieves O(n) time with O(1) space.
|
||||
|
||||
For **partial reversal** (reversing between positions m and n), we:
|
||||
1. Navigate to position m-1 (the node before reversal starts)
|
||||
2. Reverse nodes from m to n
|
||||
3. Reconnect the reversed portion to the rest of the list
|
||||
|
||||
visualization: |
|
||||
**Full list reversal:**
|
||||
|
||||
```
|
||||
Initial: 1 → 2 → 3 → 4 → null
|
||||
prev=null, curr=1
|
||||
|
||||
Step 1: Save next=2, reverse 1's link
|
||||
null ← 1 2 → 3 → 4
|
||||
prev curr
|
||||
|
||||
Step 2: Save next=3, reverse 2's link
|
||||
null ← 1 ← 2 3 → 4
|
||||
prev curr
|
||||
|
||||
Step 3: Save next=4, reverse 3's link
|
||||
null ← 1 ← 2 ← 3 4
|
||||
prev curr
|
||||
|
||||
Step 4: Save next=null, reverse 4's link
|
||||
null ← 1 ← 2 ← 3 ← 4
|
||||
prev curr=null
|
||||
|
||||
Result: 4 → 3 → 2 → 1 → null
|
||||
```
|
||||
|
||||
**Partial reversal (positions 2 to 4):**
|
||||
|
||||
```
|
||||
Initial: 1 → 2 → 3 → 4 → 5
|
||||
positions: 1 2 3 4 5
|
||||
|
||||
Goal: 1 → 4 → 3 → 2 → 5
|
||||
|
||||
Step 1: Find node before position 2
|
||||
before = node 1
|
||||
|
||||
Step 2: Reverse nodes 2, 3, 4
|
||||
1 null ← 2 ← 3 ← 4 5
|
||||
↑ ↑ ↑
|
||||
before prev curr
|
||||
|
||||
Step 3: Reconnect
|
||||
before.next.next = curr (2 → 5)
|
||||
before.next = prev (1 → 4)
|
||||
|
||||
Result: 1 → 4 → 3 → 2 → 5
|
||||
```
|
||||
|
||||
code_template: |
|
||||
class ListNode:
|
||||
def __init__(self, val=0, next=None):
|
||||
self.val = val
|
||||
self.next = next
|
||||
|
||||
|
||||
def reverse_list(head: ListNode) -> ListNode:
|
||||
"""Reverse entire linked list."""
|
||||
prev = None
|
||||
curr = head
|
||||
|
||||
while curr:
|
||||
next_node = curr.next # Save next
|
||||
curr.next = prev # Reverse link
|
||||
prev = curr # Advance prev
|
||||
curr = next_node # Advance curr
|
||||
|
||||
return prev # New head
|
||||
|
||||
|
||||
def reverse_between(head: ListNode, m: int, n: int) -> ListNode:
|
||||
"""Reverse nodes from position m to n (1-indexed)."""
|
||||
if not head or m == n:
|
||||
return head
|
||||
|
||||
dummy = ListNode(0)
|
||||
dummy.next = head
|
||||
before = dummy
|
||||
|
||||
# Move to node before reversal starts
|
||||
for _ in range(m - 1):
|
||||
before = before.next
|
||||
|
||||
# Reverse n - m + 1 nodes
|
||||
prev = None
|
||||
curr = before.next
|
||||
for _ in range(n - m + 1):
|
||||
next_node = curr.next
|
||||
curr.next = prev
|
||||
prev = curr
|
||||
curr = next_node
|
||||
|
||||
# Reconnect
|
||||
before.next.next = curr # tail of reversed → rest of list
|
||||
before.next = prev # before → new head of reversed
|
||||
|
||||
return dummy.next
|
||||
|
||||
|
||||
def reverse_k_group(head: ListNode, k: int) -> ListNode:
|
||||
"""Reverse nodes in groups of k."""
|
||||
# Count total nodes
|
||||
count = 0
|
||||
node = head
|
||||
while node:
|
||||
count += 1
|
||||
node = node.next
|
||||
|
||||
dummy = ListNode(0)
|
||||
dummy.next = head
|
||||
before = dummy
|
||||
|
||||
while count >= k:
|
||||
# Reverse k nodes
|
||||
prev = None
|
||||
curr = before.next
|
||||
for _ in range(k):
|
||||
next_node = curr.next
|
||||
curr.next = prev
|
||||
prev = curr
|
||||
curr = next_node
|
||||
|
||||
# Reconnect
|
||||
tail = before.next
|
||||
tail.next = curr
|
||||
before.next = prev
|
||||
|
||||
# Move to next group
|
||||
before = tail
|
||||
count -= k
|
||||
|
||||
return dummy.next
|
||||
|
||||
|
||||
def reverse_list_recursive(head: ListNode) -> ListNode:
|
||||
"""Reverse list using recursion."""
|
||||
if not head or not head.next:
|
||||
return head
|
||||
|
||||
new_head = reverse_list_recursive(head.next)
|
||||
head.next.next = head # Reverse link
|
||||
head.next = None # Prevent cycle
|
||||
|
||||
return new_head
|
||||
|
||||
recognition_signals:
|
||||
- "reverse linked list"
|
||||
- "reverse between"
|
||||
- "reverse in groups"
|
||||
- "reverse k-group"
|
||||
- "palindrome linked list"
|
||||
- "reorder list"
|
||||
- "swap nodes"
|
||||
- "rotate list"
|
||||
|
||||
common_mistakes:
|
||||
- title: Losing reference to next node
|
||||
description: |
|
||||
Reversing `curr.next` before saving it means you can't advance to the
|
||||
next node.
|
||||
fix: |
|
||||
Always save the next node first:
|
||||
```python
|
||||
next_node = curr.next # Save FIRST
|
||||
curr.next = prev # Then reverse
|
||||
```
|
||||
|
||||
- title: Forgetting to update connections in partial reversal
|
||||
description: |
|
||||
Reversing the middle portion without reconnecting it to the beginning
|
||||
and end of the list breaks the list.
|
||||
fix: |
|
||||
After reversing, reconnect both ends:
|
||||
```python
|
||||
before.next.next = curr # tail → rest
|
||||
before.next = prev # before → new head
|
||||
```
|
||||
|
||||
- title: Not using a dummy node
|
||||
description: |
|
||||
When the reversal might include the head, handling the head separately
|
||||
adds complexity and edge cases.
|
||||
fix: |
|
||||
Use a dummy node pointing to head. This simplifies edge cases:
|
||||
```python
|
||||
dummy = ListNode(0)
|
||||
dummy.next = head
|
||||
# ... reversal logic ...
|
||||
return dummy.next
|
||||
```
|
||||
|
||||
- title: Off-by-one with positions
|
||||
description: |
|
||||
Confusing 0-indexed vs 1-indexed positions causes reversal of wrong nodes.
|
||||
fix: |
|
||||
Clarify indexing convention. For 1-indexed positions, loop `m-1` times
|
||||
to reach the node *before* position m.
|
||||
|
||||
variations:
|
||||
- name: Full reversal
|
||||
description: |
|
||||
Reverse the entire linked list. Simplest form—just walk through and
|
||||
reverse each link.
|
||||
example: "Reverse Linked List"
|
||||
|
||||
- name: Partial reversal
|
||||
description: |
|
||||
Reverse only nodes between positions m and n. Need to track connection
|
||||
points before and after the reversed section.
|
||||
example: "Reverse Linked List II"
|
||||
|
||||
- name: K-group reversal
|
||||
description: |
|
||||
Reverse every k consecutive nodes. Often requires counting total nodes
|
||||
first to know when to stop.
|
||||
example: "Reverse Nodes in k-Group"
|
||||
|
||||
- name: Alternating reversal
|
||||
description: |
|
||||
Reverse every other group of k nodes. Combines k-group logic with
|
||||
skip logic.
|
||||
example: "Reverse Alternate K Nodes"
|
||||
|
||||
- name: Recursive reversal
|
||||
description: |
|
||||
Elegant recursive solution that reverses by relying on the recursive
|
||||
call to reverse the rest, then fixing up the current node.
|
||||
example: "Reverse Linked List (recursive approach)"
|
||||
|
||||
related_patterns:
|
||||
- fast-slow-pointers
|
||||
- two-pointers
|
||||
|
||||
prerequisite_patterns: []
|
||||
245
backend/data/patterns/prefix-sum.yaml
Normal file
245
backend/data/patterns/prefix-sum.yaml
Normal file
@@ -0,0 +1,245 @@
|
||||
name: Prefix Sum
|
||||
slug: prefix-sum
|
||||
difficulty_level: 2
|
||||
|
||||
description: >
|
||||
Precompute cumulative sums to answer range sum queries in O(1) time. This
|
||||
transforms repeated O(n) range calculations into O(n) preprocessing plus
|
||||
O(1) per query, making it essential for problems involving subarray sums.
|
||||
|
||||
when_to_use: |
|
||||
- Range sum queries
|
||||
- Subarray sum equals target
|
||||
- Count subarrays with given sum
|
||||
- Product of array except self
|
||||
- 2D matrix region sums
|
||||
|
||||
metaphor: |
|
||||
Imagine tracking your total running distance over a year. Instead of adding up
|
||||
daily distances each time someone asks "how far did you run from day 50 to day
|
||||
75?", you keep a running total. The cumulative distance on day 75 minus day 49
|
||||
instantly gives you the answer.
|
||||
|
||||
Another analogy: a bank account balance. To find how much you spent between
|
||||
two dates, you subtract the earlier balance from the later balance—no need to
|
||||
sum individual transactions.
|
||||
|
||||
core_concept: |
|
||||
A **prefix sum array** stores cumulative sums where `prefix[i]` = sum of all
|
||||
elements from index 0 to i-1. This enables O(1) range sum queries:
|
||||
|
||||
**sum(i, j) = prefix[j+1] - prefix[i]**
|
||||
|
||||
The key insight is that any range sum can be computed from two prefix sums.
|
||||
Instead of iterating through the range (O(n) per query), we do O(n) preprocessing
|
||||
once and answer unlimited queries in O(1) each.
|
||||
|
||||
This pattern extends to:
|
||||
- **Prefix products**: For multiplication-based problems
|
||||
- **Prefix counts**: For counting occurrences
|
||||
- **2D prefix sums**: For matrix region queries
|
||||
- **Prefix sum + hash map**: For "subarray sum equals K"
|
||||
|
||||
visualization: |
|
||||
**Building prefix sum array:**
|
||||
|
||||
```
|
||||
Array: [3, 1, 4, 1, 5, 9]
|
||||
Index: 0 1 2 3 4 5
|
||||
|
||||
prefix[0] = 0 (empty prefix)
|
||||
prefix[1] = 0 + 3 = 3 (sum of first 1 element)
|
||||
prefix[2] = 3 + 1 = 4 (sum of first 2 elements)
|
||||
prefix[3] = 4 + 4 = 8
|
||||
prefix[4] = 8 + 1 = 9
|
||||
prefix[5] = 9 + 5 = 14
|
||||
prefix[6] = 14 + 9 = 23
|
||||
|
||||
Prefix: [0, 3, 4, 8, 9, 14, 23]
|
||||
Index: 0 1 2 3 4 5 6
|
||||
```
|
||||
|
||||
**Range sum query: sum(2, 4) = elements at indices 2, 3, 4**
|
||||
|
||||
```
|
||||
sum(2, 4) = prefix[5] - prefix[2]
|
||||
= 14 - 4
|
||||
= 10
|
||||
|
||||
Verification: arr[2] + arr[3] + arr[4] = 4 + 1 + 5 = 10 ✓
|
||||
```
|
||||
|
||||
**Subarray sum equals K using hash map:**
|
||||
|
||||
```
|
||||
Array: [1, 2, 3, -2, 5] K = 4
|
||||
|
||||
As we iterate, track prefix sums and look for prefix_sum - K:
|
||||
|
||||
i=0: prefix=1, need 1-4=-3, not found
|
||||
i=1: prefix=3, need 3-4=-1, not found
|
||||
i=2: prefix=6, need 6-4=2, not found
|
||||
i=3: prefix=4, need 4-4=0, found! (empty prefix)
|
||||
→ subarray [0:4] sums to 4
|
||||
i=4: prefix=9, need 9-4=5, not found
|
||||
|
||||
Wait, also: prefix at i=1 is 3, at i=4 is 9-4=5... let me recalculate
|
||||
Actually arr[1:3] = 2+3-2=3, arr[2:4]=3-2+5=6... checking for K=4:
|
||||
subarray [0:4] → 1+2+3-2=4 ✓
|
||||
```
|
||||
|
||||
code_template: |
|
||||
def build_prefix_sum(arr: list[int]) -> list[int]:
|
||||
"""Build prefix sum array. prefix[i] = sum of arr[0:i]."""
|
||||
prefix = [0] * (len(arr) + 1)
|
||||
for i in range(len(arr)):
|
||||
prefix[i + 1] = prefix[i] + arr[i]
|
||||
return prefix
|
||||
|
||||
|
||||
def range_sum(prefix: list[int], i: int, j: int) -> int:
|
||||
"""Sum of elements from index i to j (inclusive)."""
|
||||
return prefix[j + 1] - prefix[i]
|
||||
|
||||
|
||||
def subarray_sum_equals_k(nums: list[int], k: int) -> int:
|
||||
"""Count subarrays with sum equal to k."""
|
||||
count = 0
|
||||
prefix_sum = 0
|
||||
# Map: prefix_sum -> count of occurrences
|
||||
sum_count = {0: 1} # Empty prefix has sum 0
|
||||
|
||||
for num in nums:
|
||||
prefix_sum += num
|
||||
|
||||
# If (prefix_sum - k) exists, those prefixes form valid subarrays
|
||||
if prefix_sum - k in sum_count:
|
||||
count += sum_count[prefix_sum - k]
|
||||
|
||||
# Record current prefix sum
|
||||
sum_count[prefix_sum] = sum_count.get(prefix_sum, 0) + 1
|
||||
|
||||
return count
|
||||
|
||||
|
||||
def product_except_self(nums: list[int]) -> list[int]:
|
||||
"""Product of array except self without division."""
|
||||
n = len(nums)
|
||||
result = [1] * n
|
||||
|
||||
# Prefix products (left to right)
|
||||
prefix = 1
|
||||
for i in range(n):
|
||||
result[i] = prefix
|
||||
prefix *= nums[i]
|
||||
|
||||
# Suffix products (right to left)
|
||||
suffix = 1
|
||||
for i in range(n - 1, -1, -1):
|
||||
result[i] *= suffix
|
||||
suffix *= nums[i]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def matrix_region_sum(matrix: list[list[int]],
|
||||
row1: int, col1: int,
|
||||
row2: int, col2: int) -> int:
|
||||
"""2D prefix sum for matrix region queries."""
|
||||
# Build 2D prefix sum
|
||||
m, n = len(matrix), len(matrix[0])
|
||||
prefix = [[0] * (n + 1) for _ in range(m + 1)]
|
||||
|
||||
for i in range(1, m + 1):
|
||||
for j in range(1, n + 1):
|
||||
prefix[i][j] = (matrix[i-1][j-1]
|
||||
+ prefix[i-1][j]
|
||||
+ prefix[i][j-1]
|
||||
- prefix[i-1][j-1])
|
||||
|
||||
# Query region sum using inclusion-exclusion
|
||||
return (prefix[row2+1][col2+1]
|
||||
- prefix[row1][col2+1]
|
||||
- prefix[row2+1][col1]
|
||||
+ prefix[row1][col1])
|
||||
|
||||
recognition_signals:
|
||||
- "range sum"
|
||||
- "subarray sum"
|
||||
- "cumulative"
|
||||
- "sum equals k"
|
||||
- "count subarrays"
|
||||
- "product except self"
|
||||
- "matrix region sum"
|
||||
- "running total"
|
||||
- "continuous subarray"
|
||||
|
||||
common_mistakes:
|
||||
- title: Off-by-one in prefix array indexing
|
||||
description: |
|
||||
Confusion about whether prefix[i] includes arr[i] or not leads to
|
||||
incorrect range sums.
|
||||
fix: |
|
||||
Convention: `prefix[i]` = sum of first i elements = `arr[0:i]`.
|
||||
So `prefix[0] = 0` (empty), and range sum is `prefix[j+1] - prefix[i]`.
|
||||
|
||||
- title: Forgetting the empty prefix for "sum equals K"
|
||||
description: |
|
||||
Not initializing the hash map with `{0: 1}` misses subarrays starting
|
||||
from index 0.
|
||||
fix: |
|
||||
Always initialize: `sum_count = {0: 1}`. This handles the case where the
|
||||
subarray from the beginning has sum K.
|
||||
|
||||
- title: Integer overflow with large sums
|
||||
description: |
|
||||
Prefix sums can grow very large when array elements are big, causing
|
||||
overflow in some languages.
|
||||
fix: |
|
||||
In Python this isn't an issue. In Java/C++, use `long` for prefix sums
|
||||
or check constraints carefully.
|
||||
|
||||
- title: Not handling negative numbers
|
||||
description: |
|
||||
Prefix sum works with negative numbers, but some may expect only positive
|
||||
sums and use wrong optimization (like sliding window).
|
||||
fix: |
|
||||
Prefix sum handles negatives correctly. But problems like "minimum sum
|
||||
subarray of size k" need different approaches when negatives are present.
|
||||
|
||||
variations:
|
||||
- name: Basic prefix sum
|
||||
description: |
|
||||
Precompute cumulative sums for O(1) range queries. Foundation for other
|
||||
variations.
|
||||
example: "Range Sum Query - Immutable, Running Sum of 1d Array"
|
||||
|
||||
- name: Prefix sum with hash map
|
||||
description: |
|
||||
Track prefix sums in a hash map to find subarrays summing to a target.
|
||||
Key insight: `prefix[j] - prefix[i] = target` means subarray [i,j] works.
|
||||
example: "Subarray Sum Equals K, Contiguous Array"
|
||||
|
||||
- name: Prefix product
|
||||
description: |
|
||||
Same idea but with multiplication. Watch for zeros and consider using
|
||||
left/right products separately.
|
||||
example: "Product of Array Except Self"
|
||||
|
||||
- name: 2D prefix sum
|
||||
description: |
|
||||
Extend to matrices for O(1) region sum queries. Uses inclusion-exclusion
|
||||
principle.
|
||||
example: "Range Sum Query 2D, Matrix Block Sum"
|
||||
|
||||
- name: Difference array
|
||||
description: |
|
||||
Inverse of prefix sum. Store differences to support O(1) range updates.
|
||||
Prefix sum of difference array gives original.
|
||||
example: "Range Addition, Corporate Flight Bookings"
|
||||
|
||||
related_patterns:
|
||||
- sliding-window
|
||||
- two-pointers
|
||||
|
||||
prerequisite_patterns: []
|
||||
Reference in New Issue
Block a user