Chapter 7: Foundational Problem-Solving Patterns
7.03Beginner6 topics20 min4 interactive animations

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

ArraysSortingLinked lists help for fast and slow pointers

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.

7.03.1

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.

Java
1static boolean isPalindrome(String s) {
2 int lo = 0, hi = s.length() - 1;
3 while (lo < hi) {
4 if (s.charAt(lo) != s.charAt(hi)) return false;
5 lo++;
6 hi--;
7 }
8 return true;
9}

Worst

O(n)

Space

O(1)

7.03.2

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 input

Your input

Try

int[] arr · length 9

1
1
2
3
3
3
4
5
5
012345678
Being comparedUnique prefixJust written
Setup

arr[0] is always kept. write marks the end of the unique prefix; read scans ahead.

Step 1 / 14
Java
1public static int removeDuplicates(int[] arr) { // arr is sorted
2 if (arr.length == 0) return 0;
3 int write = 1; // arr[0..write-1] is the unique prefix
4 for (int read = 1; read < arr.length; read++) {
5 if (arr[read] != arr[write - 1]) {
6 arr[write] = arr[read];
7 write++;
8 }
9 }
10 return write;
11}

Live state

0 comparisons0 writes

write

1

No condition evaluated on this step
Execution trace (dry run table)Show 14 steps
StepLineStateWhat happens
1L3write=1arr[0] is always kept. write marks the end of the unique prefix; read scans ahead.
2L5read=1 write=1 arr[read]=1 arr[write-1]=1Same as the last kept value: skip it.
3L5read=2 write=1 arr[read]=2 arr[write-1]=1A new value: append it to the unique prefix.
4L6read=2 write=2arr[1] = 2; write = 2.
5L5read=3 write=2 arr[read]=3 arr[write-1]=2A new value: append it to the unique prefix.
6L6read=3 write=3arr[2] = 3; write = 3.
7L5read=4 write=3 arr[read]=3 arr[write-1]=3Same as the last kept value: skip it.
8L5read=5 write=3 arr[read]=3 arr[write-1]=3Same as the last kept value: skip it.
9L5read=6 write=3 arr[read]=4 arr[write-1]=3A new value: append it to the unique prefix.
10L6read=6 write=4arr[3] = 4; write = 4.
11L5read=7 write=4 arr[read]=5 arr[write-1]=4A new value: append it to the unique prefix.
12L6read=7 write=5arr[4] = 5; write = 5.
13L5read=8 write=5 arr[read]=5 arr[write-1]=5Same as the last kept value: skip it.
14L10return=5return 5: the first 5 slots hold the unique values. One pass, O(1) extra space. Slots after that are leftovers.
7.03.3

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.

Java
1static ListNode middle(ListNode head) { // same idea: fast reaches the end when slow is halfway
2 ListNode slow = head, fast = head;
3 while (fast != null && fast.next != null) {
4 slow = slow.next;
5 fast = fast.next.next;
6 }
7 return slow;
8}

Worst

O(n)

Space

O(1)

Fast and slow pointers (Floyd's cycle detection)

Interactive · edit the input

Your input

Try

int[] arr · length 5

1
3
4
2
2
01234
Pointer positionOn the cycleCycle entrance

path from 0

0
1
3
2
4
2↺
Setup

Each cell holds the index of the next node. Following the arrows from node 0 must eventually loop, because there are only n nodes.

Step 1 / 10
Java
1static int cycleStart(int[] next) { // node i points to node next[i]; start at 0
2 int slow = 0, fast = 0;
3 do {
4 slow = next[slow]; // one step
5 fast = next[next[fast]]; // two steps
6 } while (slow != fast); // they meet somewhere on the cycle
7 slow = 0;
8 while (slow != fast) { // equal speed now
9 slow = next[slow];
10 fast = next[fast];
11 }
12 return slow; // first node of the cycle
13}

Live state

0 steps

slow

0

fast

0

No condition evaluated on this step
Execution trace (dry run table)Show 10 steps
StepLineStateWhat happens
1L2slow=0 fast=0Each cell holds the index of the next node. Following the arrows from node 0 must eventually loop, because there are only n nodes.
2L5slow=1 fast=3slow moves 1 step to 1, fast moves 2 steps to 3.
3L5slow=3 fast=4slow moves 1 step to 3, fast moves 2 steps to 4.
4L5slow=2 fast=4slow moves 1 step to 2, fast moves 2 steps to 4.
5L5slow=4 fast=4They meet at node 4. Inside a cycle the gap shrinks by one each round, so they must meet.
6L7slow=0 fast=4Reset 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).
7L9slow=1 fast=2Both move one step.
8L9slow=3 fast=4Both move one step.
9L9slow=2 fast=2They meet again at node 2: the entrance of the cycle.
10L12return=2 cycle length=2return 2. Found with O(1) extra memory, no visited set needed. In “find the duplicate number”, this entrance is the repeated value.
7.03.4

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 input

Your input

Try

int[] arr · length 7

0a
1
0b
3
12
0c
5
0123456
Being comparedNon-zero, final orderJust swappedaEqual keys in original order
Setup

Same-direction pointers: read finds non-zeros, write is where the next one belongs.

Step 1 / 13
Java
1public static void moveZeroes(int[] arr) {
2 int write = 0; // arr[0..write-1]: non-zeros, original order
3 for (int read = 0; read < arr.length; read++) {
4 if (arr[read] != 0) {
5 swap(arr, write, read);
6 write++;
7 }
8 }
9}

Live state

0 swaps

write

0

No condition evaluated on this step
Execution trace (dry run table)Show 13 steps
StepLineStateWhat happens
1L2write=0Same-direction pointers: read finds non-zeros, write is where the next one belongs.
2L4read=0 write=0 arr[read]=0Zero: leave it; it will drift to the end.
3L4read=1 write=0 arr[read]=1Non-zero: move it to the write position.
4L5read=1 write=1Swapped arr[0] and arr[1]; write = 1.
5L4read=2 write=1 arr[read]=0Zero: leave it; it will drift to the end.
6L4read=3 write=1 arr[read]=3Non-zero: move it to the write position.
7L5read=3 write=2Swapped arr[1] and arr[3]; write = 2.
8L4read=4 write=2 arr[read]=12Non-zero: move it to the write position.
9L5read=4 write=3Swapped arr[2] and arr[4]; write = 3.
10L4read=5 write=3 arr[read]=0Zero: leave it; it will drift to the end.
11L4read=6 write=3 arr[read]=5Non-zero: move it to the write position.
12L5read=6 write=4Swapped arr[3] and arr[6]; write = 4.
13L9write=4Non-zeros keep their relative order (watch any a/b tags) and the 3 zeros sit at the end. O(n) time, 4 swaps.
7.03.5

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 input

Your input

Try

int[] arr · length 8

1
3
4
6
8
11
14
17
01234567
Being comparedRuled outMatch / answer
Setup

Start with the smallest and largest values. Every step rules out one value for good.

Step 1 / 15
Java
1public static int[] twoSum(int[] arr, int target) { // arr is sorted
2 int lo = 0, hi = arr.length - 1;
3 while (lo < hi) {
4 int sum = arr[lo] + arr[hi];
5 if (sum == target) return new int[] { lo, hi };
6 if (sum < target) lo++; // need a bigger sum
7 else hi--; // need a smaller sum
8 }
9 return new int[] { -1, -1 };
10}

Live state

0 checks

target

19

lo

0

hi

7

No condition evaluated on this step
Execution trace (dry run table)Show 15 steps
StepLineStateWhat happens
1L2target=19 lo=0 hi=7Start with the smallest and largest values. Every step rules out one value for good.
2L5target=19 lo=0 hi=7 sum=1818 is too small. arr[lo] plus even the largest remaining value is too small, so arr[lo] can never be part of an answer.
3L6target=19 lo=1 hi=7lo = 1.
4L5target=19 lo=1 hi=7 sum=2020 is too large. arr[hi] plus even the smallest remaining value is too large, so arr[hi] is ruled out.
5L7target=19 lo=1 hi=6hi = 6.
6L5target=19 lo=1 hi=6 sum=1717 is too small. arr[lo] plus even the largest remaining value is too small, so arr[lo] can never be part of an answer.
7L6target=19 lo=2 hi=6lo = 2.
8L5target=19 lo=2 hi=6 sum=1818 is too small. arr[lo] plus even the largest remaining value is too small, so arr[lo] can never be part of an answer.
9L6target=19 lo=3 hi=6lo = 3.
10L5target=19 lo=3 hi=6 sum=2020 is too large. arr[hi] plus even the smallest remaining value is too large, so arr[hi] is ruled out.
11L7target=19 lo=3 hi=5hi = 5.
12L5target=19 lo=3 hi=5 sum=1717 is too small. arr[lo] plus even the largest remaining value is too small, so arr[lo] can never be part of an answer.
13L6target=19 lo=4 hi=5lo = 4.
14L5target=19 lo=4 hi=5 sum=19Found the pair.
15L5target=19 return=[4, 5]return [4, 5] after 7 checks. O(n) time, O(1) space.
7.03.6

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.

Java
1static int removeDuplicatesAtMostTwice(int[] arr) { // sorted input
2 int write = 0;
3 for (int x : arr) {
4 if (write < 2 || x != arr[write - 2]) arr[write++] = x;
5 }
6 return 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.