Two Pointers
Think of it like this
You need to find two friends in a lineup who together weigh exactly 100kg. The lineup is sorted lightest-to-heaviest. Instead of checking every possible pair (O(n²)), you put one finger at the lightest end and one at the heaviest. Their combined weight tells you which direction to move:
- Combined too light → move the left pointer right (get heavier)
- Combined too heavy → move the right pointer left (get lighter)
- Combined exactly 100 → found it!
This is the core idea: two pointers working together eliminate half the search space at each step.
The Three Variants
Variant 1 — Opposite Ends (Inward)
Both pointers start at opposite ends and move toward each other. Works on sorted arrays.
When to use: pair problems on sorted arrays
- Two Sum (sorted input)
- Container With Most Water
- 3Sum (fix one, two-pointer the rest)
arr = [-3, -1, 2, 4, 7, 9] target = 6
↑L ↑R
Step 1: -3 + 9 = 6 ✓ FOUND at [0, 5]
arr = [-3, -1, 2, 4, 7, 9] target = 3
↑L ↑R
Step 1: -3 + 9 = 6 > 3 → R--
Step 2: -3 + 7 = 4 > 3 → R--
Step 3: -3 + 4 = 1 < 3 → L++
Step 4: -1 + 4 = 3 ✓ FOUND at [1, 3]// Two Sum on sorted array — O(n) time, O(1) space
function twoSumSorted(arr, target) {
let left = 0, right = arr.length - 1;
while (left < right) {
const sum = arr[left] + arr[right];
if (sum === target) return [left, right];
if (sum
Variant 2 — Same Direction (Fast & Slow / Read & Write)
Both pointers start at the same end and move in the same direction at different speeds or for different purposes.
When to use:
- Remove duplicates in-place
- Move zeros to end
- Partition array around a value
- Linked list cycle detection
Remove duplicates from sorted array [1,1,2,3,3,4]:
write=0, read=0
read=0: arr[0]=1 → write 1 at write=0, write++ → [1,1,2,3,3,4] write=1
read=1: arr[1]=1 = arr[write-1] → SKIP (duplicate)
read=2: arr[2]=2 ≠ arr[write-1] → write 2 at write=1, write++ → [1,2,2,3,3,4] write=2
read=3: arr[3]=3 ≠ arr[write-1] → write 3 at write=2, write++ → [1,2,3,3,3,4] write=3
read=4: arr[4]=3 = arr[write-1] → SKIP
read=5: arr[5]=4 ≠ arr[write-1] → write 4 at write=3, write++ → [1,2,3,4,3,4] write=4
Result: first 4 elements are [1,2,3,4] ✓// Remove duplicates in-place — O(n) time, O(1) space
function removeDuplicates(nums) {
if (!nums.length) return 0;
let write = 1; // next position to write unique value
for (let read = 1; read < nums.length; read++) {
if (nums[read] !== nums[write
Variant 3 — Fast & Slow (Linked Lists)
Fast pointer moves 2× speed of slow. When fast reaches the end, slow is at the midpoint. If they ever meet before the end, there's a cycle.
Find middle of list: 1 → 2 → 3 → 4 → 5
slow: 1 → 2 → 3 (moves 1 step per iteration)
fast: 1 → 3 → 5 (moves 2 steps per iteration)
When fast = 5 (end), slow = 3 (middle) ✓function findMiddle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
function hasCycle(head) {
let slow = head, fast = head;
Recognizing When to Apply Two Pointers
Ask these questions:
- Is the input sorted? → Try opposite-ends pointers for pair/target problems
- "In-place" with O(1) space? → Try read/write (same-direction) pointers
- Linked list cycle or middle? → Fast/slow pointers
- Palindrome check? → Expand from center (a.k.a. two pointers expanding outward)
Palindrome — Expand from Center
// Longest palindromic substring
function longestPalindrome(s) {
let start = 0, maxLen = 1;
function expand(l, r) {
while (l >= 0 && r < s.length && s[l] === s[r]) {
if (r - l
Two Pointers (Two Sum on Sorted Array)
Target = 9. Start with left pointer at the beginning and right pointer at the end.
Real-World Frontend Application
- Diff algorithms: Comparing two versions of a string (like in code editors) — two pointers scan from both ends to find the unchanged prefix/suffix, then only diff the middle
- Merging sorted data: When combining paginated API results that are already sorted (merge two sorted arrays in O(n))
- Input validation: Palindrome check on user-entered text, bracket matching in syntax highlighting parsers
- Virtual scrolling: Two pointers mark the first and last visible row indices; update both as the user scrolls
Time and Space Complexity
| Variant | Time | Space | vs Brute Force |
|---|---|---|---|
| Opposite ends | O(n) | O(1) | vs O(n²) nested loops |
| Same direction (read/write) | O(n) | O(1) | vs O(n) extra array |
| Fast/slow (linked list) | O(n) | O(1) | Cycle detection otherwise impossible |
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Using on unsorted input | Wrong pairs returned | Sort first, then apply two pointers |
| Not skipping duplicates in 3Sum | Duplicate triplets in result | After finding a match, skip all identical elements |
left < right vs left <= right | Off-by-one, process same element twice | Use left < right for pair problems |
| Moving wrong pointer | Algorithm doesn't converge |
Key Problems to Solve
| # | Problem | Variant | Difficulty |
|---|---|---|---|
| 1 | Valid Palindrome | Opposite ends | Easy |
| 2 | Two Sum II (sorted) | Opposite ends | Easy |
| 3 | Remove Duplicates from Sorted Array | Same direction | Easy |
| 4 | Move Zeroes | Same direction | Easy |