Two pointers is the highest-ROI technique in coding interviews. The entire playbook is one idea: two indices walk through the data, and at each step a decision rule tells you which one moves. No extra arrays — O(1) extra space in nearly every pattern, usually O(n) time. Interviewers love it because it tests whether you can discard search space, not just whether you can code.
Five patterns cover nearly every two-pointer question you’ll meet. Learn the decision rule for each, not just the code. (One honest exception up front: the sliding-window pattern trades O(alphabet) space for its single pass — the cheat sheet says so. Everything else is O(1).)
Pattern 1: Opposite ends — Two Sum II
Sorted array, find two numbers summing to a target. The brute force is O(n²); the two-pointer version is O(n).
static int[] twoSum(int[] numbers, int target) {
int l = 0, r = numbers.length - 1;
while (l < r) {
int sum = numbers[l] + numbers[r];
if (sum == target) return new int[]{l + 1, r + 1}; // 1-indexed
else if (sum < target) l++; // need a bigger sum → move left up
else r--; // need a smaller sum → move right down
}
throw new IllegalArgumentException("no solution");
}
The decision rule: the array is sorted, so the sum tells you exactly which side is wrong. Too small? The left pointer is the smallest unused value — advancing it is the only way to grow the sum. That “only way” reasoning is what makes it O(n) instead of O(n²): each step eliminates one index permanently.
Pattern 2: Greedy shrink — Container With Most Water
Two lines, maximize min(height[l], height[r]) × (r - l). Same opposite-ends setup, subtler decision rule:
static int maxArea(int[] height) {
int l = 0, r = height.length - 1, best = 0;
while (l < r) {
best = Math.max(best, Math.min(height[l], height[r]) * (r - l));
if (height[l] < height[r]) l++;
else r--;
}
return best;
}
The question interviewers always ask: why move the shorter one? Because the width shrinks every step no matter what, so the area can only grow via height — and height is capped by the shorter line. Moving the taller line keeps the same cap with less width: strictly worse. Moving the shorter line is the only move that can find a taller cap. Two sentences, and you’ve shown you understand the proof, not just the code.
Pattern 3: Fast and slow — Linked list cycle
Floyd’s tortoise and hare. One pointer moves one step, the other moves two; if there’s a cycle, they meet.
static class ListNode {
int val; ListNode next;
ListNode(int val) { this.val = val; }
}
static boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
Why they must meet: once both pointers are inside the cycle, the fast pointer gains one node per step on the slow one — the gap shrinks by exactly 1 each iteration, so it hits 0 within one lap. No gap size can be skipped. O(n) time, O(1) space — the “no extra space” constraint is the whole point; a HashSet of visited nodes is the O(n)-space answer they don’t want.
The same fast/slow idea finds the middle of a list in one pass (when fast hits the end, slow is at the middle) and removes the nth node from the end. One technique, three questions.
Pattern 4: Sliding window — longest substring without repeating characters
The window [l, r] is always valid; r expands every step, l jumps only when a duplicate appears:
static int lengthOfLongestSubstring(String s) {
Map<Character, Integer> last = new HashMap<>();
int best = 0, l = 0;
for (int r = 0; r < s.length(); r++) {
char c = s.charAt(r);
if (last.containsKey(c)) l = Math.max(l, last.get(c) + 1);
last.put(c, r);
best = Math.max(best, r - l + 1);
}
return best;
}
Two details that separate a clean answer from a buggy one:
l = Math.max(l, last.get(c) + 1)— theMath.maxmatters. Without it, a duplicate whose previous occurrence is behind the current window start would draglbackwards. (Trace"abba"by hand if you don’t believe it.)- Each index is visited at most twice (once by
r, once byl), so it’s O(n) time despite the nested-looking structure. Space is O(min(n, alphabet)).
Pattern 5: Fix one, two-pointer the rest — 3Sum
Find all unique triplets summing to zero. Sort (O(n log n)), fix the first element, run Pattern 1 on the remainder — with deduplication:
static List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // skip duplicate first elements
int l = i + 1, r = nums.length - 1;
while (l < r) {
int sum = nums[i] + nums[l] + nums[r];
if (sum == 0) {
res.add(List.of(nums[i], nums[l], nums[r]));
while (l < r && nums[l] == nums[l + 1]) l++;
while (l < r && nums[r] == nums[r - 1]) r--;
l++; r--;
} else if (sum < 0) l++;
else r--;
}
}
return res;
}
The decision rule is Pattern 1’s, but the interview signal is the dedup: skip duplicate i values and duplicate l/r values after a hit, or you’ll return the same triplet five times. Total: O(n²) time — the nested two-pointer search dominates the O(n log n) sort — and O(1) extra space besides the output.
Prove it: one test per pattern
The five snippets above are the members of a single Solutions class — static methods, with ListNode nested inside it — shown one pattern at a time. Assembled that way, they are directly testable:
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class TwoPointerTest {
@Test
void twoSumSorted() {
assertArrayEquals(new int[]{1, 2}, Solutions.twoSum(new int[]{2, 7, 11, 15}, 9));
}
@Test
void containerExample() {
assertEquals(49, Solutions.maxArea(new int[]{1, 8, 6, 2, 5, 4, 8, 3, 7}));
}
@Test
void cycleDetected() {
Solutions.ListNode a = new Solutions.ListNode(1),
b = new Solutions.ListNode(2),
c = new Solutions.ListNode(3);
a.next = b; b.next = c; c.next = b; // cycle
assertTrue(Solutions.hasCycle(a));
c.next = null;
assertFalse(Solutions.hasCycle(a));
}
@Test
void slidingWindow() {
assertEquals(3, Solutions.lengthOfLongestSubstring("abcabcbb"));
assertEquals(1, Solutions.lengthOfLongestSubstring("bbbbb"));
assertEquals(3, Solutions.lengthOfLongestSubstring("pwwkew"));
}
@Test
void threeSumExample() {
var res = Solutions.threeSum(new int[]{-1, 0, 1, 2, -1, -4});
assertEquals(2, res.size()); // [-1,-1,2] and [-1,0,1]
}
}
(The "abba" case from Pattern 4 makes a good extra assertion — it’s the input that catches the missing Math.max.)
The 2-minute interview answer
“Two pointers is one idea: two indices, one decision rule, and mostly O(1) extra space. Opposite ends on a sorted array gives Two Sum II — the sum tells you which pointer is wrong. Container With Most Water is the same setup with a greedy rule: always move the shorter line, because width only shrinks so height is the only thing that can grow. Fast and slow gives Floyd’s cycle detection — once both pointers are in the cycle, the gap between them shrinks by exactly one node per step, so they must meet. Sliding window keeps the window valid while the right pointer expands — longest substring without repeats, jumping the left pointer past the last duplicate. And 3Sum is sort plus fix-one-and-two-pointer with dedup at all three positions. Every one is O(n) or O(n²) time, with O(1) extra space except the sliding window’s O(alphabet) last-seen map.”
Cheat sheet
| Pattern | Decision rule | Example | Complexity |
|---|---|---|---|
| Opposite ends | Sum tells you which side is wrong | Two Sum II (sorted) | O(n) time, O(1) space |
| Greedy shrink | Move the shorter line — width only shrinks | Container With Most Water | O(n) time, O(1) space |
| Fast and slow | Gap shrinks by 1 per step → pointers must meet in a cycle | Linked list cycle | O(n) time, O(1) space |
| Sliding window | Expand right always; jump left past violations | Longest substring, no repeats | O(n) time, O(alphabet) space |
| Fix one + two pointers | Sort, fix first, two-pointer the rest, dedup all three | 3Sum | O(n²) time, O(1) space |
Five patterns, one idea: at each step, know exactly which pointer moves and why. If you can state the “why” — the shorter line, the shrinking gap, the jumping left pointer — you’ve already passed.