Interview Importance: ๐ด Critical โ This technique appears in 40% of frontend coding interviews for array/string manipulation problems. Understanding this pattern is essential for solving problems efficiently without nested loops.
The Two-Pointer Technique is an algorithmic pattern that uses two pointers (indices) to traverse a data structure, typically an array or string. Instead of using nested loops (O(nยฒ)), we use two pointers moving intelligently through the data to achieve O(n) time complexity.
Visual Representation:
Array: [1, 2, 3, 4, 5, 6, 7, 8, 9]
^ ^
left right
Two pointers moving towards each other or in the same direction
Real-World Analogy:
Think of it like two people searching through a bookshelf from opposite ends to find a matching pair of books. Instead of one person checking every combination (which would take forever), both people work simultaneously, making the search much faster.
| Problem Type | Without Two-Pointer | With Two-Pointer | Benefit |
|---|---|---|---|
| Find pair with sum | O(nยฒ) nested loops | O(n) single pass | 100x faster for 1000 items |
| Remove duplicates | O(nยฒ) with splice | O(n) in-place | No extra space |
| Reverse array | O(n) extra space | O(n) in-place | 50% memory saved |
| Palindrome check | O(nยฒ) substrings | O(n) convergence | Instant validation |
| Merge sorted arrays | O(n*m) brute force | O(n+m) optimal | Linear scaling |
Performance Benefits:
// Check if array is a palindrome const isPalindrome = (arr) => { let left = 0; // Start from beginning let right = arr.length - 1; // Start from end while (left < right) { // Continue until pointers meet if (arr[left] !== arr[right]) { return false; // Mismatch found } left++; // Move left pointer forward right--; // Move right pointer backward } return true; // All elements matched };
Input: [1, 2, 3, 2, 1]
Step 1: Initialize pointers
---------------------------------------------------------
arr = [1, 2, 3, 2, 1]
left = 0 (points to 1)
right = 4 (points to 1)
Condition: left < right -> true
Compare: arr[0] (1) === arr[4] (1) -> Match!
Step 2: Move pointers inward
---------------------------------------------------------
left = 1 (points to 2)
right = 3 (points to 2)
Condition: left < right -> true
Compare: arr[1] (2) === arr[3] (2) -> Match!
Step 3: Final check
---------------------------------------------------------
left = 2 (points to 3)
right = 2 (points to 3)
Condition: left < right -> false (pointers met)
Exit loop
Result: true (palindrome confirmed)
// Remove duplicates from sorted array in-place const removeDuplicates = (nums) => { if (nums.length === 0) return 0; let slow = 0; // Position for next unique element for (let fast = 1; fast < nums.length; fast++) { if (nums[fast] !== nums[slow]) { slow++; // Move to next position nums[slow] = nums[fast]; // Place unique element } } return slow + 1; // Length of unique elements };
Input: [1, 1, 2, 2, 3, 4, 4]
Initial State:
---------------------------------------------------------
nums = [1, 1, 2, 2, 3, 4, 4]
slow = 0
fast = 1
Iteration 1: fast = 1
---------------------------------------------------------
Compare: nums[1] (1) !== nums[0] (1) -> false
Action: Skip (duplicate)
State: slow = 0, fast = 2
Iteration 2: fast = 2
---------------------------------------------------------
Compare: nums[2] (2) !== nums[0] (1) -> true
Action: slow++ -> 1, nums[1] = 2
State: nums = [1, 2, 2, 2, 3, 4, 4]
slow = 1, fast = 3
Iteration 3: fast = 3
---------------------------------------------------------
Compare: nums[3] (2) !== nums[1] (2) -> false
Action: Skip (duplicate)
State: slow = 1, fast = 4
Iteration 4: fast = 4
---------------------------------------------------------
Compare: nums[4] (3) !== nums[1] (2) -> true
Action: slow++ -> 2, nums[2] = 3
State: nums = [1, 2, 3, 2, 3, 4, 4]
slow = 2, fast = 5
Iteration 5: fast = 5
---------------------------------------------------------
Compare: nums[5] (4) !== nums[2] (3) -> true
Action: slow++ -> 3, nums[3] = 4
State: nums = [1, 2, 3, 4, 3, 4, 4]
slow = 3, fast = 6
Iteration 6: fast = 6
---------------------------------------------------------
Compare: nums[6] (4) !== nums[3] (4) -> false
Action: Skip (duplicate)
Final: slow = 3
Result: slow + 1 = 4 (unique elements: [1, 2, 3, 4])
Why use TWO pointers instead of one?
Why does while (left < right) work for palindromes?
left === right, we're at the middle element (odd-length array)left > right, we've checked all pairs (even-length array)<=, we'd unnecessarily check the middle element against itselfWhat breaks if we don't increment/decrement correctly?
// โ BAD: Infinite loop while (left < right) { if (arr[left] !== arr[right]) return false; // Forgot to move pointers! } // โ GOOD: Pointers always move while (left < right) { if (arr[left] !== arr[right]) return false; left++; right--; }
Edge cases the basic implementation handles:
left === right immediately, returns true/** * Find two numbers that sum to target in a sorted array * @param {number[]} arr - Sorted array of numbers * @param {number} target - Target sum * @returns {[number, number] | null} - Pair of numbers or null */ const findPairWithSum = (arr, target) => { // Input validation if (!Array.isArray(arr) || arr.length < 2) { throw new Error('Array must contain at least 2 elements'); } if (typeof target !== 'number' || !isFinite(target)) { throw new Error('Target must be a finite number'); } let left = 0; let right = arr.length - 1; while (left < right) { const sum = arr[left] + arr[right]; if (sum === target) { return [arr[left], arr[right]]; // Found pair } else if (sum < target) { left++; // Need larger sum, move left pointer right } else { right--; // Need smaller sum, move right pointer left } } return null; // No pair found }; // With indices tracking const findPairWithSumIndices = (arr, target) => { if (!Array.isArray(arr) || arr.length < 2) return null; let left = 0; let right = arr.length - 1; while (left < right) { const sum = arr[left] + arr[right]; if (sum === target) { return { values: [arr[left], arr[right]], indices: [left, right] }; } sum < target ? left++ : right--; } return null; };
/** * Calculate maximum area in bar chart (common in data visualization) * Used in: Chart libraries, histogram rendering, layout calculations */ const maxArea = (heights) => { if (!heights || heights.length < 2) return 0; let maxArea = 0; let left = 0; let right = heights.length - 1; while (left < right) { // Area = width ร min(height at left, height at right) const width = right - left; const height = Math.min(heights[left], heights[right]); const area = width * height; maxArea = Math.max(maxArea, area); // Move pointer at shorter bar (potential for taller bar) if (heights[left] < heights[right]) { left++; } else { right--; } } return maxArea; };
import { useMemo } from 'react'; /** * Custom hook to remove duplicates from sorted data * Use case: Cleaning API responses, processing user selections */ const useUniqueSortedData = (sortedData) => { return useMemo(() => { if (!sortedData || sortedData.length <= 1) return sortedData; const result = [sortedData[0]]; let slow = 0; for (let fast = 1; fast < sortedData.length; fast++) { if (sortedData[fast].id !== sortedData[slow].id) { slow++; result.push(sortedData[fast]); } } return result; }, [sortedData]); }; // Usage in component const ProductList = ({ products }) => { const sortedProducts = products.sort((a, b) => a.id - b.id); const uniqueProducts = useUniqueSortedData(sortedProducts); return ( <div> {uniqueProducts.map(product => ( <ProductCard key={product.id} {...product} /> ))} </div> ); };
/** * Check if string is a valid palindrome (ignore case and non-alphanumeric) * Use case: Username validation, pattern matching in search */ const isValidPalindrome = (str) => { if (!str) return true; // Clean string: remove non-alphanumeric and lowercase const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, ''); let left = 0; let right = cleaned.length - 1; while (left < right) { if (cleaned[left] !== cleaned[right]) { return false; } left++; right--; } return true; }; // Usage in form validation const validateUsername = (username) => { const errors = []; if (!isValidPalindrome(username)) { errors.push('Username must be a palindrome'); } return errors; };
/** * Move all zeros to end while maintaining order * Use case: Sorting items in drag-and-drop interfaces, prioritizing active items */ const moveZerosToEnd = (arr) => { let nonZeroPos = 0; // Position for next non-zero element // First pass: move all non-zeros to front for (let i = 0; i < arr.length; i++) { if (arr[i] !== 0) { arr[nonZeroPos] = arr[i]; nonZeroPos++; } } // Second pass: fill remaining with zeros for (let i = nonZeroPos; i < arr.length; i++) { arr[i] = 0; } return arr; }; // Frontend use case: Prioritize active items const prioritizeActiveItems = (items) => { const priorities = items.map(item => item.isActive ? 1 : 0); const indices = Array.from({ length: items.length }, (_, i) => i); // Sort indices based on priority moveZerosToEnd(priorities); return priorities.map((p, i) => p === 1 ? items.find(item => item.isActive) : items.find(item => !item.isActive) ); };
| Aspect | Two-Pointer | Nested Loop | Hash Map |
|---|---|---|---|
| Time Complexity | O(n) | O(nยฒ) | O(n) |
| Space Complexity | O(1) | O(1) | O(n) |
| When to Use | Sorted data, pairs/triplets | Small datasets | Unsorted data, fast lookup |
| Frontend Use Case | Filter sorted lists | Small validation checks | Cache API responses |
| Memory Efficiency | โญโญโญโญโญ | โญโญโญโญโญ | โญโญ |
| Speed | โญโญโญโญโญ | โญโญ | โญโญโญโญ |
Decision Tree:
Is the data sorted?
+- YES -> Use Two-Pointer (best choice)
| +- Need pairs/triplets? -> Converging pointers
| +- Need to remove duplicates? -> Fast/slow pointers
|
+- NO -> Can you sort it?
+- YES -> Sort + Two-Pointer (O(n log n) + O(n))
+- NO -> Use Hash Map (O(n) time, O(n) space)
Answer: Use two-pointer when:
Use hash map when:
Answer:
// Left/Right (Converging): Moving towards each other // Use for: Palindromes, pair sums, reversing let left = 0, right = arr.length - 1; while (left < right) { // Process both ends left++; right--; } // Fast/Slow (Same direction): Moving together // Use for: Remove duplicates, cycle detection, partitioning let slow = 0; for (let fast = 0; fast < arr.length; fast++) { // Fast explores, slow places valid elements if (condition) { arr[slow++] = arr[fast]; } }
Answer:
const threeSum = (nums, target) => { nums.sort((a, b) => a - b); // Sort first const result = []; // Fix first number, use two-pointer for remaining for (let i = 0; i < nums.length - 2; i++) { // Skip duplicates for first number if (i > 0 && nums[i] === nums[i - 1]) continue; let left = i + 1; let right = nums.length - 1; while (left < right) { const sum = nums[i] + nums[left] + nums[right]; if (sum === target) { result.push([nums[i], nums[left], nums[right]]); // Skip duplicates for second number while (left < right && nums[left] === nums[left + 1]) left++; // Skip duplicates for third number while (left < right && nums[right] === nums[right - 1]) right--; left++; right--; } else if (sum < target) { left++; } else { right--; } } } return result; };
Answer:
const reverseWords = (str) => { // Step 1: Reverse entire string const reversed = str.split('').reverse().join(''); // Step 2: Reverse each word back const words = reversed.split(' '); return words.map(word => word.split('').reverse().join('') ).join(' '); }; // More efficient in-place approach const reverseWordsInPlace = (chars) => { // Helper to reverse a portion of array const reverse = (left, right) => { while (left < right) { [chars[left], chars[right]] = [chars[right], chars[left]]; left++; right--; } }; // Step 1: Reverse entire array reverse(0, chars.length - 1); // Step 2: Reverse each word let start = 0; for (let i = 0; i <= chars.length; i++) { if (i === chars.length || chars[i] === ' ') { reverse(start, i - 1); start = i + 1; } } return chars; };
Answer:
However, if data is already sorted or you can assume it's sorted (like in many frontend scenarios with pre-sorted API data), the two-pointer alone is O(n).
Answer: Yes, but only for specific problems:
// Works on unsorted: Partition even/odd const partitionArray = (arr) => { let left = 0, right = arr.length - 1; while (left < right) { while (left < right && arr[left] % 2 === 0) left++; while (left < right && arr[right] % 2 === 1) right--; if (left < right) { [arr[left], arr[right]] = [arr[right], arr[left]]; } } return arr; };
โ BAD:
const findPair = (arr, target) => { let left = 0, right = arr.length - 1; while (left < right) { const sum = arr[left] + arr[right]; if (sum === target) return [left, right]; // โ No pointer movement when sum !== target } return null; }; // Result: Infinite loop when no match at initial positions
โ GOOD:
const findPair = (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 < target) left++; // โ Move left pointer else right--; // โ Move right pointer } return null; };
Why it matters: Without pointer movement, you get an infinite loop. Always ensure pointers progress in every iteration.
โ BAD:
const isPalindrome = (arr) => { let left = 0, right = arr.length - 1; while (left <= right) { // โ Includes middle element check if (arr[left] !== arr[right]) return false; left++; right--; } return true; }; // Result: Wastes one comparison on middle element
โ GOOD:
const isPalindrome = (arr) => { let left = 0, right = arr.length - 1; while (left < right) { // โ Stops before checking middle against itself if (arr[left] !== arr[right]) return false; left++; right--; } return true; };
Why it matters: Using <= causes unnecessary middle element comparison. Use < for converging pointers.
โ BAD:
const removeDuplicates = (nums) => { let slow = 0; for (let fast = 1; fast < nums.length; fast++) { if (nums[fast] !== nums[slow]) { nums[++slow] = nums[fast]; } } return slow + 1; }; // โ Crashes on empty array: nums[0] is undefined
โ GOOD:
const removeDuplicates = (nums) => { if (!nums || nums.length === 0) return 0; // โ Handle edge case let slow = 0; for (let fast = 1; fast < nums.length; fast++) { if (nums[fast] !== nums[slow]) { nums[++slow] = nums[fast]; } } return slow + 1; };
Why it matters: Always validate inputs. Empty arrays, null values, and single-element arrays need special handling.
โ BAD:
const moveZeros = (arr) => { let nonZero = 0; for (let i = 0; i < arr.length; i++) { if (arr[i] !== 0) { arr[i] = arr[nonZero]; // โ Wrong order arr[nonZero] = arr[i]; nonZero++; } } }; // Result: Overwrites values before swapping
โ GOOD:
const moveZeros = (arr) => { let nonZero = 0; for (let i = 0; i < arr.length; i++) { if (arr[i] !== 0) { [arr[nonZero], arr[i]] = [arr[i], arr[nonZero]]; // โ Proper swap nonZero++; } } return arr; };
Why it matters: Order matters in assignment/swapping. Use destructuring for clean swaps.
| Operation | Time Complexity | Space Complexity | Explanation |
|---|---|---|---|
| Palindrome check | O(n) | O(1) | Single pass with two pointers, no extra space |
| Find pair sum (sorted) | O(n) | O(1) | Single pass, converging pointers |
| Find pair sum (unsorted) | O(n log n) | O(1) | O(n log n) for sort + O(n) for two-pointer |
| Remove duplicates | O(n) | O(1) | Single pass with fast/slow pointers |
| 3Sum problem | O(nยฒ) | O(1) | O(n) loop ร O(n) two-pointer |
| Container with water | O(n) | O(1) | Single pass with converging pointers |
| Reverse array | O(n) | O(1) | Single pass with swaps |
| Partition array | O(n) | O(1) | Single pass with condition-based movement |
Key Insight: Two-pointer technique typically achieves:
| Pattern Type | Pointer Movement | Common Uses | Complexity |
|---|---|---|---|
| Converging | Opposite directions | Palindrome, pair sum, reverse | O(n) time, O(1) space |
| Fast/Slow | Same direction, different speeds | Remove duplicates, cycle detection | O(n) time, O(1) space |
| Sliding Window | Both move right | Subarray problems, max sum | O(n) time, O(1) space |
Test your understanding with 3 quick questions