Same-Direction Pointers
The Core Idea
In the same-direction pattern, both pointers start at (or near) the beginning of the array and move in the same direction. One pointer typically moves faster or only moves when a condition is met.
The most common variant is the read/write pattern: a "read" pointer scans every element while a "write" pointer tracks where to place the next valid element. This is the go-to approach for in-place array transformations.
Two Sub-Patterns
Same-direction problems split into two families:
- Read/Write pointer: One pointer reads input, the other writes output. Used for filtering, deduplication, and compaction.
- Fast/Slow pointer: One pointer advances one step, the other advances two steps. Used for cycle detection and finding middle elements.
General Template (Read/Write)
int write = 0;
for (int read = 0; read < n; read++) {
if (shouldKeep(arr[read])) {
arr[write] = arr[read];
write++;
}
}
// arr[0..write-1] contains the result
The write pointer is always at or behind the read pointer. Everything before write is finalized output. Everything between write and read is "garbage" that will be overwritten. Everything after read is unprocessed input.
Problem 1: Remove Duplicates from Sorted Array
Problem: Given a sorted array, remove the duplicates in-place such that each element appears at most once. Return the number of unique elements.
Example: [1, 1, 2, 2, 3, 4, 4] becomes [1, 2, 3, 4, ...] with length 4.
Why Same-Direction Works
Since the array is sorted, all duplicates are adjacent. The write pointer marks the end of the unique portion. The read pointer scans forward. Whenever the read pointer finds a value different from the last written value, we write it and advance the write pointer.
The Write Pointer Invariant
At every step, arr[0..write-1] contains the unique elements seen so far, in order. The read pointer has processed all elements up to its current position.
Step-Through Animation: Remove Duplicates
Array: [1, 1, 2, 3, 3, 4]. Write pointer (green, above) and Read pointer (orange, below).
C++ Solution
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
int write = 1; // first element is always unique
for (int read = 1; read < nums.size(); read++) {
if (nums[read] != nums[write - 1]) {
nums[write] = nums[read];
write++;
}
}
return write;
}
Time O(n), Space O(1)
The read pointer visits each element exactly once. The write pointer advances at most n times. No extra memory is used beyond the two index variables.
Problem 2: Move Zeroes
Problem: Given an array, move all zeroes to the end while maintaining the relative order of the non-zero elements. Do it in-place.
Example: [0, 1, 0, 3, 12] becomes [1, 3, 12, 0, 0].
The Read/Write Approach
This is a classic filtering problem: "keep all non-zero elements in order, then fill the rest with zeroes."
- The
readpointer scans every element. - When
readfinds a non-zero value, copy it to thewriteposition and advancewrite. - After the scan, fill positions
writethroughn-1with zeroes.
An alternative uses swapping instead of copy-then-fill: swap arr[write] and arr[read] whenever arr[read] is non-zero. This avoids the fill step.
Step-Through Animation: Move Zeroes
Array: [0, 4, 0, 5, 0, 3, 7]. Step through the swap variant.
C++ Solution (Swap Variant)
void moveZeroes(vector<int>& nums) {
int write = 0;
for (int read = 0; read < nums.size(); read++) {
if (nums[read] != 0) {
swap(nums[write], nums[read]);
write++;
}
}
}
The Fast/Slow Pointer Pattern
In the fast/slow pattern, one pointer moves one step at a time (slow) and another moves two steps (fast). This is primarily used for cycle detection and finding the middle element.
Cycle Detection: The Tortoise and Hare
The classic application is detecting cycles in linked lists, but the same concept applies to any sequence where you can "follow" a next-element function. The idea: if there is a cycle, the fast pointer will eventually lap the slow pointer and they will meet inside the cycle.
The Meeting Guarantee
If the fast pointer enters a cycle of length C, it closes the gap to the slow pointer by 1 at each step (fast gains 2, slow gains 1, net difference = 1). So they must meet within C steps after both enter the cycle.
Array Application: Find the Duplicate
Problem (LeetCode 287): Given an array of n+1 integers where each is in [1, n], find the duplicate number. You must use O(1) extra space.
Treat the array as a linked list: the value at index i points to the next index. Since there is a duplicate, there must be a cycle. Use Floyd's cycle detection to find it.
int findDuplicate(vector<int>& nums) {
// Phase 1: detect cycle
int slow = nums[0], fast = nums[0];
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// Phase 2: find entrance
slow = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
Step-Through Animation: Fast/Slow on Array
Array: [2, 4, 1, 3, 0] (values represent "next index"). Slow (green) moves 1 step, Fast (orange) moves 2 steps per click. Chain: 0 leads to 2 leads to 1 leads to 4 leads to 0 (cycle).
Happy Number (Array-Style Application)
Problem: A number is "happy" if repeatedly summing the squares of its digits eventually reaches 1. If it never reaches 1, it loops forever. Detect if a number is happy.
This is cycle detection in disguise. The "next" function computes the sum of digit squares. Use slow/fast pointers on this sequence:
int digitSquareSum(int n) {
int sum = 0;
while (n > 0) {
int d = n % 10;
sum += d * d;
n /= 10;
}
return sum;
}
bool isHappy(int n) {
int slow = n, fast = n;
do {
slow = digitSquareSum(slow);
fast = digitSquareSum(digitSquareSum(fast));
} while (slow != fast);
return slow == 1;
}
Problem 3: Partition Array
Problem: Given an array and a pivot value, rearrange elements so that all elements less than the pivot come before all elements greater than or equal to the pivot. Maintain relative order within each group (stable partition).
Read/Write for Partitioning
Use two passes or a single-pass write pointer. For a single-pass stable partition:
- The write pointer tracks where the next "less than pivot" element should go.
- The read pointer scans every element.
- When
arr[read] < pivot, swap witharr[write]and advance both.
int partition(vector<int>& arr, int pivot) {
int write = 0;
for (int read = 0; read < arr.size(); read++) {
if (arr[read] < pivot) {
swap(arr[write], arr[read]);
write++;
}
}
return write; // index where >= pivot section begins
}
Relation to Quicksort
This is exactly the partition step in Lomuto's quicksort. Understanding the read/write pointer here gives you a clear mental model for how quicksort's partition works.
Problem 4: Remove Element
Problem: Given an array and a value, remove all instances of that value in-place. Return the new length.
Example: [3, 2, 2, 3], val=3 becomes [2, 2, ...], length 2.
int removeElement(vector<int>& nums, int val) {
int write = 0;
for (int read = 0; read < nums.size(); read++) {
if (nums[read] != val) {
nums[write] = nums[read];
write++;
}
}
return write;
}
This is the simplest read/write pointer problem. The "shouldKeep" condition is just "not equal to the target value."
When to Use Same-Direction Pointers
| Signal | Pattern | Example |
|---|---|---|
| "In-place" + "remove/filter" | Read/Write | Remove Element, Move Zeroes |
| "Sorted" + "duplicates" | Read/Write | Remove Duplicates |
| "Partition" + "rearrange" | Read/Write | Sort Colors, Partition Array |
| "Cycle" + "linked list" | Fast/Slow | Linked List Cycle, Find Duplicate |
| "Middle element" | Fast/Slow | Middle of Linked List |
| "Converges or loops" | Fast/Slow | Happy Number |
Same Direction vs Opposite Ends
How to Decide
Opposite ends work when you need to combine information from both extremes of a sorted array (pair sums, palindromes, container widths).
Same direction works when you need to process elements sequentially, keeping some and discarding others (filtering, compaction, cycle detection).
If the problem says "in-place" and involves filtering or rearranging, think same-direction. If it says "sorted" and involves finding pairs, think opposite-ends.
Practice Problems
| Problem | Difficulty | Pattern |
|---|---|---|
| Remove Duplicates from Sorted Array (LC 26) | Easy | Read/Write |
| Remove Element (LC 27) | Easy | Read/Write |
| Move Zeroes (LC 283) | Easy | Read/Write with swap |
| Remove Duplicates II (LC 80) | Medium | Read/Write (keep up to 2) |
| Sort Array By Parity (LC 905) | Easy | Read/Write partition |
| Find the Duplicate Number (LC 287) | Medium | Fast/Slow cycle |
| Happy Number (LC 202) | Easy | Fast/Slow cycle |
| Linked List Cycle (LC 141) | Easy | Fast/Slow |
| Middle of Linked List (LC 876) | Easy | Fast/Slow |
Summary
- Same-direction pointers both move forward, but at different speeds or under different conditions.
- The read/write pattern is the workhorse for in-place array transformations: filtering, deduplication, compaction, and partitioning.
- The fast/slow pattern detects cycles and finds midpoints in O(n) time and O(1) space.
- The write pointer invariant ("everything before write is finalized output") is the key to understanding correctness.
- These patterns run in O(n) time and O(1) space, which is optimal for single-pass in-place operations.