Two-pointer techniques
Two indices that move through the data according to a simple rule can replace a nested loop. They move toward each other, in the same direction at different speeds, or as reader and writer.
Learning goals
- Solve pair problems on sorted data with pointers at both ends
- Compact or filter arrays in place with read and write pointers
- Detect cycles with fast and slow pointers
- Argue why each pointer move never skips an answer
Prerequisites
To find two people whose heights add up to exactly 3 metres in a line sorted by height, start with the shortest and the tallest. If they are too short together, swap the shortest for the next person; if too tall, swap the tallest. You never need to try every pair.
Opposite ends
Start one pointer at each end of a sorted array and move them inward. Each comparison decides which end to drop, so the loop runs at most n - 1 times instead of the n(n - 1)/2 pair checks of a nested loop.
Other problems with this shape: checking a palindrome, reversing an array in place, and container with most water, where you always move the shorter wall because the taller one cannot help a narrower container.
1static boolean isPalindrome(String s) {2int lo = 0, hi = s.length() - 1;3while (lo < hi) {4if (s.charAt(lo) != s.charAt(hi)) return false;5lo++;6hi--;7}8return true;9}
Worst
O(n)
Space
O(1)
Same direction
Both pointers move left to right. The read pointer scans every element; the write pointer marks where the next kept element goes. Everything before write is the finished result.
This compacts an array in place in one pass and O(1) extra space. Removing duplicates from a sorted array is the standard example.
Worst
O(n)
Space
O(1)
Remove duplicates in place (read / write pointers)
Interactive · edit the inputint[] arr · length 9
arr[0] is always kept. write marks the end of the unique prefix; read scans ahead.
1public static int removeDuplicates(int[] arr) { // arr is sorted2if (arr.length == 0) return 0;3int write = 1; // arr[0..write-1] is the unique prefix4for (int read = 1; read < arr.length; read++) {5if (arr[read] != arr[write - 1]) {6arr[write] = arr[read];7write++;8}9}10return write;11}
Live state
write
1
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | write=1 | arr[0] is always kept. write marks the end of the unique prefix; read scans ahead. |
| 2 | L5 | read=1 write=1 arr[read]=1 arr[write-1]=1 | Same as the last kept value: skip it. |
| 3 | L5 | read=2 write=1 arr[read]=2 arr[write-1]=1 | A new value: append it to the unique prefix. |
| 4 | L6 | read=2 write=2 | arr[1] = 2; write = 2. |
| 5 | L5 | read=3 write=2 arr[read]=3 arr[write-1]=2 | A new value: append it to the unique prefix. |
| 6 | L6 | read=3 write=3 | arr[2] = 3; write = 3. |
| 7 | L5 | read=4 write=3 arr[read]=3 arr[write-1]=3 | Same as the last kept value: skip it. |
| 8 | L5 | read=5 write=3 arr[read]=3 arr[write-1]=3 | Same as the last kept value: skip it. |
| 9 | L5 | read=6 write=3 arr[read]=4 arr[write-1]=3 | A new value: append it to the unique prefix. |
| 10 | L6 | read=6 write=4 | arr[3] = 4; write = 4. |
| 11 | L5 | read=7 write=4 arr[read]=5 arr[write-1]=4 | A new value: append it to the unique prefix. |
| 12 | L6 | read=7 write=5 | arr[4] = 5; write = 5. |
| 13 | L5 | read=8 write=5 arr[read]=5 arr[write-1]=5 | Same as the last kept value: skip it. |
| 14 | L10 | return=5 | return 5: the first 5 slots hold the unique values. One pass, O(1) extra space. Slots after that are leftovers. |
Fast and slow
Move one pointer one step at a time and another two steps. If the structure has a cycle, the fast pointer laps the slow one and they meet; if not, the fast pointer reaches the end. This is Floyd's tortoise and hare algorithm.
After they meet, reset one pointer to the start and move both one step at a time: they meet again exactly at the entrance of the cycle. The animation treats each array cell as a node pointing to index next[i], which is how “find the duplicate number” is solved in O(1) space.
1static ListNode middle(ListNode head) { // same idea: fast reaches the end when slow is halfway2ListNode slow = head, fast = head;3while (fast != null && fast.next != null) {4slow = slow.next;5fast = fast.next.next;6}7return slow;8}
Worst
O(n)
Space
O(1)
Fast and slow pointers (Floyd's cycle detection)
Interactive · edit the inputint[] arr · length 5
path from 0
Each cell holds the index of the next node. Following the arrows from node 0 must eventually loop, because there are only n nodes.
1static int cycleStart(int[] next) { // node i points to node next[i]; start at 02int slow = 0, fast = 0;3do {4slow = next[slow]; // one step5fast = next[next[fast]]; // two steps6} while (slow != fast); // they meet somewhere on the cycle7slow = 0;8while (slow != fast) { // equal speed now9slow = next[slow];10fast = next[fast];11}12return slow; // first node of the cycle13}
Live state
slow
0
fast
0
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | slow=0 fast=0 | Each cell holds the index of the next node. Following the arrows from node 0 must eventually loop, because there are only n nodes. |
| 2 | L5 | slow=1 fast=3 | slow moves 1 step to 1, fast moves 2 steps to 3. |
| 3 | L5 | slow=3 fast=4 | slow moves 1 step to 3, fast moves 2 steps to 4. |
| 4 | L5 | slow=2 fast=4 | slow moves 1 step to 2, fast moves 2 steps to 4. |
| 5 | L5 | slow=4 fast=4 | They meet at node 4. Inside a cycle the gap shrinks by one each round, so they must meet. |
| 6 | L7 | slow=0 fast=4 | Reset slow to the start. The distance from the start to the entrance equals the distance from the meeting point to the entrance (modulo the cycle length). |
| 7 | L9 | slow=1 fast=2 | Both move one step. |
| 8 | L9 | slow=3 fast=4 | Both move one step. |
| 9 | L9 | slow=2 fast=2 | They meet again at node 2: the entrance of the cycle. |
| 10 | L12 | return=2 cycle length=2 | return 2. Found with O(1) extra memory, no visited set needed. In “find the duplicate number”, this entrance is the repeated value. |
Partitioning
Partitioning moves the elements that satisfy a condition to the front. With a read pointer and a write pointer and a swap, the kept elements also keep their relative order, which is what “move zeroes” requires.
Quick sort's Lomuto partition is the same pattern with the condition arr[j] < pivot. With three regions (less, equal, greater) it becomes the Dutch national flag partition from lesson 18.02.
Partition: move zeroes to the end (stable)
Interactive · edit the inputint[] arr · length 7
Same-direction pointers: read finds non-zeros, write is where the next one belongs.
1public static void moveZeroes(int[] arr) {2int write = 0; // arr[0..write-1]: non-zeros, original order3for (int read = 0; read < arr.length; read++) {4if (arr[read] != 0) {5swap(arr, write, read);6write++;7}8}9}
Live state
write
0
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | write=0 | Same-direction pointers: read finds non-zeros, write is where the next one belongs. |
| 2 | L4 | read=0 write=0 arr[read]=0 | Zero: leave it; it will drift to the end. |
| 3 | L4 | read=1 write=0 arr[read]=1 | Non-zero: move it to the write position. |
| 4 | L5 | read=1 write=1 | Swapped arr[0] and arr[1]; write = 1. |
| 5 | L4 | read=2 write=1 arr[read]=0 | Zero: leave it; it will drift to the end. |
| 6 | L4 | read=3 write=1 arr[read]=3 | Non-zero: move it to the write position. |
| 7 | L5 | read=3 write=2 | Swapped arr[1] and arr[3]; write = 2. |
| 8 | L4 | read=4 write=2 arr[read]=12 | Non-zero: move it to the write position. |
| 9 | L5 | read=4 write=3 | Swapped arr[2] and arr[4]; write = 3. |
| 10 | L4 | read=5 write=3 arr[read]=0 | Zero: leave it; it will drift to the end. |
| 11 | L4 | read=6 write=3 arr[read]=5 | Non-zero: move it to the write position. |
| 12 | L5 | read=6 write=4 | Swapped arr[3] and arr[6]; write = 4. |
| 13 | L9 | write=4 | Non-zeros keep their relative order (watch any a/b tags) and the 3 zeros sit at the end. O(n) time, 4 swaps. |
Two-sum sorted
Given a sorted array and a target, find two indices whose values add up to the target. If the current sum is too small, the left value cannot pair with anything (even the largest remaining value is not enough), so move left right. If it is too large, move right left.
Each step permanently rules out one value, so the search is O(n). On unsorted data, either sort first (O(n log n)) or use a hash map of seen values (O(n) expected).
Two-sum on a sorted array (opposite ends)
Interactive · edit the inputint[] arr · length 8
Start with the smallest and largest values. Every step rules out one value for good.
1public static int[] twoSum(int[] arr, int target) { // arr is sorted2int lo = 0, hi = arr.length - 1;3while (lo < hi) {4int sum = arr[lo] + arr[hi];5if (sum == target) return new int[] { lo, hi };6if (sum < target) lo++; // need a bigger sum7else hi--; // need a smaller sum8}9return new int[] { -1, -1 };10}
Live state
target
19
lo
0
hi
7
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=19 lo=0 hi=7 | Start with the smallest and largest values. Every step rules out one value for good. |
| 2 | L5 | target=19 lo=0 hi=7 sum=18 | 18 is too small. arr[lo] plus even the largest remaining value is too small, so arr[lo] can never be part of an answer. |
| 3 | L6 | target=19 lo=1 hi=7 | lo = 1. |
| 4 | L5 | target=19 lo=1 hi=7 sum=20 | 20 is too large. arr[hi] plus even the smallest remaining value is too large, so arr[hi] is ruled out. |
| 5 | L7 | target=19 lo=1 hi=6 | hi = 6. |
| 6 | L5 | target=19 lo=1 hi=6 sum=17 | 17 is too small. arr[lo] plus even the largest remaining value is too small, so arr[lo] can never be part of an answer. |
| 7 | L6 | target=19 lo=2 hi=6 | lo = 2. |
| 8 | L5 | target=19 lo=2 hi=6 sum=18 | 18 is too small. arr[lo] plus even the largest remaining value is too small, so arr[lo] can never be part of an answer. |
| 9 | L6 | target=19 lo=3 hi=6 | lo = 3. |
| 10 | L5 | target=19 lo=3 hi=6 sum=20 | 20 is too large. arr[hi] plus even the smallest remaining value is too large, so arr[hi] is ruled out. |
| 11 | L7 | target=19 lo=3 hi=5 | hi = 5. |
| 12 | L5 | target=19 lo=3 hi=5 sum=17 | 17 is too small. arr[lo] plus even the largest remaining value is too small, so arr[lo] can never be part of an answer. |
| 13 | L6 | target=19 lo=4 hi=5 | lo = 4. |
| 14 | L5 | target=19 lo=4 hi=5 sum=19 | Found the pair. |
| 15 | L5 | target=19 return=[4, 5] | return [4, 5] after 7 checks. O(n) time, O(1) space. |
Deduplication
In a sorted array, equal values are adjacent, so a value is new exactly when it differs from the last kept value. Allowing each value up to twice only changes the comparison to arr[read] != arr[write - 2].
On unsorted data, deduplicate with a LinkedHashSet to keep first-seen order, or sort first if order does not matter.
1static int removeDuplicatesAtMostTwice(int[] arr) { // sorted input2int write = 0;3for (int x : arr) {4if (write < 2 || x != arr[write - 2]) arr[write++] = x;5}6return write;7}
Worst
O(n)
Space
O(1)
Practice ladder
Now try it yourself
Easy
- Valid palindrome
- Move zeroes
- Remove duplicates from sorted array
- Middle of the linked list
Medium
- 3Sum
- Container with most water
- Linked list cycle II
- Find the duplicate number
Advanced
- Trapping rain water with two pointers
- 4Sum
- Sort colors in one pass
Key takeaways
- Opposite-end pointers solve pair problems on sorted data in O(n).
- Read/write pointers filter or compact in place; the prefix before write is the result.
- Fast and slow pointers detect cycles and find their entrance in O(1) space.