Skip to content
Commitwise
Menu
Coding Interview

Two pointers

Solve array and string problems in one pass with two indices that move toward each other or in the same direction, with Valid Palindrome, Two Sum II and 3Sum in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

Many array problems have an obvious O(n²) answer: try every pair. The two-pointer techniqueTwo pointersA technique that walks an array or string with two indices, moving toward each other or in the same direction, to avoid nested loops.Open in glossary → replaces “every pair” with two indices that walk through the data once, each step ruling out a whole group of pairs. It needs no extra memory, works on arrays and strings, and shows up in a huge share of interview questions.

New to some of these? Open what you need, or skip ahead.

Basics:What are arrays and indexes?

An array is a row of values stored one after another: [5, 8, 2, 9]. Each position has an index, starting at 0: here 5 is at index 0 and 9 at index 3. The last index is always count - 1 (Swift) or size - 1 / length - 1 (Kotlin, Java, TypeScript).

Reading nums[2] is instant, whatever the array’s size. Reading an index that doesn’t exist (like nums[4] here) crashes with an “index out of range” error (in TypeScript it quietly gives undefined, a bug of its own), which is why loops usually go from 0 up to, but not including, the length.

Basics:How do strings and characters work?

A string is text: a sequence of characters. "racecar" has 7 characters, and you can compare them, change their case (lowercased(), lowercase(), toLowerCase()) and check whether one is a letter or a digit.

One difference to know: Kotlin, Java and TypeScript let you read a character by position, s[3] or s.charAt(3) (in TypeScript, positions count UTF-16 units, so an emoji takes two). Swift doesn’t allow integer positions on String (because some characters, like emoji, take more space than others), so code often converts the string to an array first: Array(s).

Basics:What does O(n) mean?

Big-O describes how the amount of work grows with the size of the input n:

  • O(1): the same work whatever the size (reading array[5]).
  • O(log n): grows very slowly (about 20 steps for a million items).
  • O(n): grows in step with the input (one pass over the array).
  • O(n²): comparing every item with every other item, which gets slow fast.

You’ll see it next to every solution, for time and for extra space (memory). The Big-O tutorial explains it properly.

Basics:What are lists, sets and maps?

Collections hold many values:

  • A list (Swift Array, Kotlin List, Java List, a TypeScript array) keeps items in order, and allows duplicates: ["Ana", "Ben", "Ana"].
  • A set holds each value at most once and answers “is this in it?” very quickly.
  • A map (Swift Dictionary, Kotlin, Java and TypeScript Map) stores values under keys: "Ana" → 30, looked up by the key.

Lists use indexes starting at 0: in ["a", "b", "c"], "a" is at index 0 and "c" at index 2.

Part Not started

From both ends toward the middle

Picture checking whether a word reads the same backwards. You don’t write it out reversed. You put one finger on the first letter and one on the last, compare, then move both fingers one step toward the middle. If every pair matches until the fingers meet, it’s a palindrome.

Those fingers are the two pointers: two variables holding indices into the same array or string. There are two common shapes:

  1. From both ends toward the middle: palindromes, pairs in a sorted array, reversing.
  2. Both from the start, at different speeds: one pointer reads every element, the other marks where to write, for cleaning up an array in place.

Two pointers is a way to search a 2-D space of pairs (i, j) in linear time by exploiting monotonic structure: when the input is sorted (or the condition is monotonic in each index), each comparison lets you discard an entire row or column of candidate pairs. The other family, read/write (fast/slow) pointers, compacts or partitions in place with O(1) extra space.

What interviewers probe at senior level: why moving a given pointer is safe (the invariant), handling duplicates without losing or repeating answers, overflow in sums, and extending the idea to k-sum and to problems where it doesn’t apply (unsorted input with no monotonic condition). Those are in the Detailed version.

Problemeasy

Valid Palindrome

A phrase is a palindrome if, after converting uppercase letters to lowercase and removing everything that isn’t a letter or a digit, it reads the same forward and backward. Given a string s, return true if it’s a palindrome.

"A man, a plan, a canal: Panama"  → true   ("amanaplanacanalpanama")
"race a car"                      → false  ("raceacar")
" "                               → true   (empty after cleaning)
›

Start one pointer at the beginning and one at the end. What should happen when a pointer is on a space or a comma?

›

Skip characters that aren’t letters or digits by moving that pointer inward, then compare the two characters in lowercase. Stop when the pointers meet.

SolutionTry it yourself firstTime O(n)Space O(1)

Move the left pointer forward and the right pointer backward past anything that isn’t a letter or digit, compare, and step both inward. Any mismatch means it’s not a palindrome.

func isPalindrome(_ s: String) -> Bool {
    let chars = Array(s)                  // Swift strings can't be indexed by Int
    var left = 0
    var right = chars.count - 1
    while left < right {
        while left < right && !(chars[left].isLetter || chars[left].isNumber) { left += 1 }
        while left < right && !(chars[right].isLetter || chars[right].isNumber) { right -= 1 }
        if chars[left].lowercased() != chars[right].lowercased() { return false }
        left += 1
        right -= 1
    }
    return true
}
fun isPalindrome(s: String): Boolean {
    var left = 0
    var right = s.length - 1
    while (left < right) {
        while (left < right && !s[left].isLetterOrDigit()) left++
        while (left < right && !s[right].isLetterOrDigit()) right--
        if (s[left].lowercaseChar() != s[right].lowercaseChar()) return false
        left++
        right--
    }
    return true
}
boolean isPalindrome(String s) {
    int left = 0, right = s.length() - 1;
    while (left < right) {
        while (left < right && !Character.isLetterOrDigit(s.charAt(left))) left++;
        while (left < right && !Character.isLetterOrDigit(s.charAt(right))) right--;
        if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) return false;
        left++;
        right--;
    }
    return true;
}
function isPalindrome(s: string): boolean {
  const isAlnum = (c: string) => /[a-z0-9]/i.test(c);
  let left = 0;
  let right = s.length - 1;
  while (left < right) {
    while (left < right && !isAlnum(s[left])) left++;
    while (left < right && !isAlnum(s[right])) right--;
    if (s[left].toLowerCase() !== s[right].toLowerCase()) return false;
    left++;
    right--;
  }
  return true;
}

Swift note

Swift’s String uses String.Index, not integers, because characters have different sizes in memory. Converting to Array(s) costs O(n) extra space but makes integer indexing O(1). You can stay O(1) in space by walking s.startIndex and s.index(before: s.endIndex) with index(after:) and index(before:).

TypeScript note

s[i] reads one UTF-16 code unit, which is fine for this ASCII check. For text in any language, test with a Unicode property regex (the u flag with letter and number classes), and remember that emoji and some accented letters span two units.

Why moving a pointer is safe

Picture all pairs (i, j) with i < j of a sorted array as the upper triangle of a matrix whose values grow to the right and downward. Starting at (0, n − 1), the top-right corner:

  • If nums[i] + nums[j] < target, then every pair (i, k) with k ≤ j is also too small, since nums[k] ≤ nums[j]. The whole row i is eliminated: i += 1.
  • If the sum is too big, every pair (k, j) with k ≥ i is too big: the whole column j is eliminated: j -= 1.

Each step discards a row or a column, so after at most n − 1 steps every pair has been either checked or ruled out. Saying this out loud is what separates “I memorized it” from “I can adapt it”, and it’s the argument you need for variants like Container With Most Water (move the shorter wall: the taller one can’t produce a bigger area with a narrower width).

When the sum doesn’t fit

Predict the output

What does this print?

let a = 2_000_000_000
let b = 2_000_000_000
print(a + b > 0)
val a = 2_000_000_000
val b = 2_000_000_000
println(a + b > 0)
int a = 2_000_000_000;
int b = 2_000_000_000;
System.out.println(a + b > 0);
const a = 2_000_000_000;
const b = 2_000_000_000;
console.log(a + b > 0);
  • Explain the invariant: on sorted data, a too-small sum eliminates the whole row of pairs with the current left element, so moving left is safe (and a too-big sum eliminates a column). Container With Most Water: move the shorter wall.

Kotlin, Java

32-bit Int/int sums wrap silently near 2 billion: use Long sums, Math.addExact, or compare a > target - b.

Swift

Int is 64-bit on iPhone and Mac, and overflow traps instead of wrapping; with Int32 input, a big sum crashes rather than giving a wrong answer.

TypeScript

Numbers are doubles: sums are exact only up to Number.MAX_SAFE_INTEGER (2⁵³ − 1), then lose precision silently. Use BigInt beyond that. Strings index UTF-16 units, so emoji take two positions.

Check this part

  1. Where do the two pointers start when checking a palindrome?

  2. In "A man, a plan…", the left pointer lands on a space. What does the check do?

  3. Why can you discard a whole row of pairs when the sum is too small?

  4. let a = 2_000_000_000, b = 2_000_000_000; print(a + b > 0) prints…val a = 2_000_000_000; val b = 2_000_000_000; println(a + b > 0) prints…int a = 2_000_000_000, b = 2_000_000_000; System.out.println(a + b > 0); prints…const a = 2_000_000_000, b = 2_000_000_000; console.log(a + b > 0) prints…

Two indices start at opposite ends and move toward the middle: palindromes, reversing, pairs in sorted data. Each move must be safe, which comes from the order of the data.

Part Not started

Pairs in a sorted array

The second classic: a sorted array and a target sum. Instead of checking every pair, start with the smallest and largest numbers:

  • sum too small → the only way to grow it is to move the left pointer right (to a bigger number),
  • sum too big → move the right pointer left (to a smaller number),
  • equal → done.

Someone implemented exactly that, but the tests fail. Which line is wrong?

Spot the bugTwo Sum II, almost

Tap the line with the bug.

func twoSumSorted(_ nums: [Int], _ target: Int) -> [Int] {
    var left = 0
    var right = nums.count - 1
    while left < right {
        let sum = nums[left] + nums[right]
        if sum == target { return [left, right] }
        if sum < target { right -= 1 } else { left += 1 }
    }
    return []
}
fun twoSumSorted(nums: IntArray, target: Int): IntArray {
    var left = 0
    var right = nums.size - 1
    while (left < right) {
        val sum = nums[left] + nums[right]
        if (sum == target) return intArrayOf(left, right)
        if (sum < target) right-- else left++
    }
    return intArrayOf()
}
int[] twoSumSorted(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        int sum = nums[left] + nums[right];
        if (sum == target) return new int[] { left, right };
        if (sum < target) right--; else left++;
    }
    return new int[0];
}
function twoSumSorted(nums: number[], target: number): number[] {
  let left = 0;
  let right = nums.length - 1;
  while (left < right) {
    const sum = nums[left] + nums[right];
    if (sum === target) return [left, right];
    if (sum < target) right--; else left++;
  }
  return [];
}

Exerciseeasy

Squares of a Sorted Array

Given an array of integers sorted in non-decreasing order, which may include negative numbers, return the squares of each number, also sorted. Do it in O(n), without sorting the result.

[-4, -1, 0, 3, 10]  → [0, 1, 9, 16, 100]
[-7, -3, 2, 3, 11]  → [4, 9, 9, 49, 121]
›

Squaring then sorting is O(n log n). Where in the input are the largest squares?

›

The largest square is at one of the two ends (a big negative or a big positive number). Compare the ends, put the bigger square at the end of the result, and move that pointer inward. Fill the result from back to front.

SolutionTry it yourself firstTime O(n)Space O(n) for the result
func sortedSquares(_ nums: [Int]) -> [Int] {
    var result = Array(repeating: 0, count: nums.count)
    var left = 0
    var right = nums.count - 1
    for i in stride(from: nums.count - 1, through: 0, by: -1) {
        if abs(nums[left]) > abs(nums[right]) {
            result[i] = nums[left] * nums[left]
            left += 1
        } else {
            result[i] = nums[right] * nums[right]
            right -= 1
        }
    }
    return result
}
fun sortedSquares(nums: IntArray): IntArray {
    val result = IntArray(nums.size)
    var left = 0
    var right = nums.size - 1
    for (i in nums.indices.reversed()) {
        if (abs(nums[left]) > abs(nums[right])) {
            result[i] = nums[left] * nums[left]
            left++
        } else {
            result[i] = nums[right] * nums[right]
            right--
        }
    }
    return result
}
int[] sortedSquares(int[] nums) {
    int[] result = new int[nums.length];
    int left = 0, right = nums.length - 1;
    for (int i = nums.length - 1; i >= 0; i--) {
        if (Math.abs(nums[left]) > Math.abs(nums[right])) {
            result[i] = nums[left] * nums[left];
            left++;
        } else {
            result[i] = nums[right] * nums[right];
            right--;
        }
    }
    return result;
}
function sortedSquares(nums: number[]): number[] {
  const result = new Array<number>(nums.length);
  let left = 0;
  let right = nums.length - 1;
  for (let i = nums.length - 1; i >= 0; i--) {
    if (Math.abs(nums[left]) > Math.abs(nums[right])) {
      result[i] = nums[left] * nums[left];
      left++;
    } else {
      result[i] = nums[right] * nums[right];
      right--;
    }
  }
  return result;
}

Each step places one square in its final position, so the loop runs exactly n times. The pattern to remember: when the extremes of the input produce the extremes of the output, fill the result from the end with two pointers.

Extending to k-sum

3Sum sorts once (O(n log n)), fixes each element as the first of the triple, and runs the sorted two-sum on the rest: O(n²) total. 4Sum adds one more loop: O(n³). The general k-sum is O(n^(k−1)), and the details that decide whether your code is correct are duplicates and early exits:

  • skip a fixed element equal to the previous one (not the next, see the puzzle below),
  • after finding a triple, move both pointers and skip equal values on at least one side,
  • break early when the fixed element is greater than the target and everything after it is non-negative.

When the input is unsorted and you need indices, sorting destroys them: sort pairs of (value, index), or use a hash map instead.

Exercisemedium

3Sum

Given an integer array nums, return all unique triplets [a, b, c] from different positions such that a + b + c == 0. The answer must not contain duplicate triplets.

[-1, 0, 1, 2, -1, -4]  → [[-1, -1, 2], [-1, 0, 1]]
[0, 1, 1]              → []
[0, 0, 0, 0]           → [[0, 0, 0]]

Give the complexity, and explain how you avoid duplicates without a set of results.

›

Sort first. Then for each index i, the problem becomes Two Sum on the sorted part to its right, with target -nums[i].

›

Duplicates come from equal values chosen in the same role. Skip nums[i] if it equals nums[i - 1], and after recording a triplet, move left past every value equal to the one you just used.

SolutionTry it yourself firstTime O(n²)Space O(1) besides the output (O(log n) to O(n) for sorting)
func threeSum(_ input: [Int]) -> [[Int]] {
    let nums = input.sorted()
    var result: [[Int]] = []
    for i in nums.indices {
        if nums[i] > 0 { break }
        if i > 0 && nums[i] == nums[i - 1] { continue }
        var left = i + 1
        var right = nums.count - 1
        while left < right {
            let sum = nums[i] + nums[left] + nums[right]
            if sum < 0 {
                left += 1
            } else if sum > 0 {
                right -= 1
            } else {
                result.append([nums[i], nums[left], nums[right]])
                left += 1
                right -= 1
                while left < right && nums[left] == nums[left - 1] { left += 1 }
            }
        }
    }
    return result
}
fun threeSum(nums: IntArray): List<List<Int>> {
    nums.sort()
    val result = mutableListOf<List<Int>>()
    for (i in nums.indices) {
        if (nums[i] > 0) break                              // everything after is positive too
        if (i > 0 && nums[i] == nums[i - 1]) continue       // same first value: same triplets
        var left = i + 1
        var right = nums.size - 1
        while (left < right) {
            val sum = nums[i] + nums[left] + nums[right]
            when {
                sum < 0 -> left++
                sum > 0 -> right--
                else -> {
                    result.add(listOf(nums[i], nums[left], nums[right]))
                    left++
                    right--
                    while (left < right && nums[left] == nums[left - 1]) left++
                }
            }
        }
    }
    return result
}
List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    List<List<Integer>> result = new ArrayList<>();
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] > 0) break;
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        int left = i + 1, right = nums.length - 1;
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            if (sum < 0) {
                left++;
            } else if (sum > 0) {
                right--;
            } else {
                result.add(List.of(nums[i], nums[left], nums[right]));
                left++;
                right--;
                while (left < right && nums[left] == nums[left - 1]) left++;
            }
        }
    }
    return result;
}
function threeSum(input: number[]): number[][] {
  const nums = input.toSorted((a, b) => a - b);   // the default sort compares as strings
  const result: number[][] = [];
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] > 0) break;
    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 < 0) {
        left++;
      } else if (sum > 0) {
        right--;
      } else {
        result.push([nums[i], nums[left], nums[right]]);
        left++;
        right--;
        while (left < right && nums[left] === nums[left - 1]) left++;
      }
    }
  }
  return result;
}

Sorting is O(n log n) and the double loop is O(n²), so O(n²) total; no substantially faster algorithm is known, and none is expected in an interview. Skipping equal values on the left after a match is enough: if left changes value, right must change too to keep the sum at zero. Follow-ups to expect: 3Sum Closest (track the best sum instead of matching), 4Sum (one more outer loop, O(n³), and use long sums), and “what if the array doesn’t fit in memory?” (sort externally, or partition by value range).

Skipping the wrong duplicates

Spot the bug3Sum that misses answers

Tap the line with the bug.

for i in nums.indices {
    if i + 1 < nums.count && nums[i] == nums[i + 1] { continue }
    var left = i + 1
    var right = nums.count - 1
    // … two-pointer search for -nums[i] …
}
for (i in nums.indices) {
    if (i + 1 < nums.size && nums[i] == nums[i + 1]) continue
    var left = i + 1
    var right = nums.size - 1
    // … two-pointer search for -nums[i] …
}
for (int i = 0; i < nums.length; i++) {
    if (i + 1 < nums.length && nums[i] == nums[i + 1]) continue;
    int left = i + 1, right = nums.length - 1;
    // … two-pointer search for -nums[i] …
}
for (let i = 0; i < nums.length; i++) {
  if (i + 1 < nums.length && nums[i] === nums[i + 1]) continue;
  let left = i + 1;
  let right = nums.length - 1;
  // … two-pointer search for -nums[i] …
}

  • k-sum: sort once, fix k − 2 elements in loops, two-pointer the rest: 3Sum O(n²), 4Sum O(n³). Skip a fixed value equal to the previous one; after a match, move both pointers and skip equal values.
  • Sorting destroys original indices: sort (value, index) pairs, or use a hash map on unsorted input.
var left = 0, right = nums.count - 1
while left < right {
    let sum = nums[left] + nums[right]
    if sum == target { return (left, right) }
    if sum < target { left += 1 } else { right -= 1 }
}
var left = 0
var right = nums.size - 1
while (left < right) {
    val sum = nums[left].toLong() + nums[right]
    if (sum == target.toLong()) return left to right
    if (sum < target) left++ else right--
}
int left = 0, right = nums.length - 1;
while (left < right) {
    long sum = (long) nums[left] + nums[right];
    if (sum == target) return new int[] { left, right };
    if (sum < target) left++; else right--;
}
let left = 0;
let right = nums.length - 1;
while (left < right) {
  const sum = nums[left] + nums[right];          // exact up to 2^53
  if (sum === target) return [left, right];
  if (sum < target) left++; else right--;
}

Check this part

  1. In a sorted array, the sum of the two pointed-to numbers is too small. What do you do?

  2. What extra space does the two-pointer Two Sum on a sorted array use?

  3. In 3Sum, why compare nums[i] with nums[i - 1] rather than nums[i + 1] to skip duplicates?

  4. What is the time complexity of 3Sum with sorting and two pointers?

On sorted input, a too-small sum moves the left pointer and a too-big sum moves the right one: Two Sum II in O(n) time and O(1) space. 3Sum fixes one element and runs the same search, with duplicates skipped against the previous element.

Part Not started

Same direction: read and write

The second shape uses two pointers that both move forward: read looks at every element, write marks the next free slot for elements you want to keep. It cleans up an array in place, without a second array.

Removing duplicates from a sorted array is the standard example. Trace it on [1, 1, 2, 3, 3]:

readnums[read]Compared with nums[write − 1]ActionArraywrite
———start: the first element is always kept[1, 1, 2, 3, 3]1
111same: skip[1, 1, 2, 3, 3]1
221new: copy to index 1[1, 2, 2, 3, 3]2
332new: copy to index 2[1, 2, 3, 3, 3]3
433same: skip[1, 2, 3, 3, 3]3

Predict the output

What gets printed?

func removeDuplicates(_ nums: inout [Int]) -> Int {
    guard !nums.isEmpty else { return 0 }
    var write = 1
    for read in 1..<nums.count where nums[read] != nums[write - 1] {
        nums[write] = nums[read]
        write += 1
    }
    return write
}

var nums = [1, 1, 2, 3, 3]
let k = removeDuplicates(&nums)
print(k, nums)
fun removeDuplicates(nums: IntArray): Int {
    if (nums.isEmpty()) return 0
    var write = 1
    for (read in 1 until nums.size) {
        if (nums[read] != nums[write - 1]) {
            nums[write] = nums[read]
            write++
        }
    }
    return write
}

val nums = intArrayOf(1, 1, 2, 3, 3)
val k = removeDuplicates(nums)
println("$k ${nums.contentToString()}")
static int removeDuplicates(int[] nums) {
    if (nums.length == 0) return 0;
    int write = 1;
    for (int read = 1; read < nums.length; read++) {
        if (nums[read] != nums[write - 1]) {
            nums[write] = nums[read];
            write++;
        }
    }
    return write;
}

int[] nums = {1, 1, 2, 3, 3};
int k = removeDuplicates(nums);
System.out.println(k + " " + Arrays.toString(nums));
function removeDuplicates(nums: number[]): number {
  if (nums.length === 0) return 0;
  let write = 1;
  for (let read = 1; read < nums.length; read++) {
    if (nums[read] !== nums[write - 1]) {
      nums[write] = nums[read];
      write++;
    }
  }
  return write;
}

const nums = [1, 1, 2, 3, 3];
const k = removeDuplicates(nums);
console.log(k, nums);

The same read/write shape solves “move all zeros to the end”, “remove every occurrence of a value” and “keep at most two of each number”: only the condition for keeping an element changes.

Practice: recognizing two-pointer problems

Before writing code, look for these signals in the problem statement:

  1. The input is sorted (or you’re allowed to sort it), and you’re looking for a pair or triple with some sum or difference.
  2. You compare from both ends: palindromes, reversing, “container” or “trapping water” problems.
  3. You must modify the array in place with O(1) extra space: remove, deduplicate, partition.
  4. You’d naturally write two nested loops over the same array, and the inner loop’s start could move forward instead of restarting.

If none of these apply, for example an unsorted array where order matters, a hash map is usually the better tool.

Deep dives

Deep diveDutch national flag: three pointers, one passInterview favorite

Sort an array of 0s, 1s and 2s in place, in one pass (Sort Colors). Dijkstra’s solution keeps three regions: low (everything before it is 0), mid (the element being looked at), high (everything after it is 2).

  • nums[mid] == 0: swap with low, advance both low and mid.
  • nums[mid] == 1: advance mid.
  • nums[mid] == 2: swap with high and move high back, but don’t advance mid: the swapped-in value hasn’t been examined yet.

It runs until mid > high. The same three-way partition is what makes quicksort fast on arrays with many duplicates.

ShapeUse forMove rule
Opposite endssorted pair/triple sums, palindromes, Container With Most Water, Trapping Rain Waterdiscard the side that can’t improve (smaller sum → left++, shorter wall → move it)
Read/writededupe, remove value, move zeros, partition (Dutch flag)write advances only on kept elements
Fast/slowcycle detection, middle of a linked listfast moves 2, slow moves 1
Two arraysmerging sorted arrays, intersectionadvance the pointer at the smaller value

Complexities: pair O(n), 3Sum O(n²), k-sum O(n^(k−1)), all O(1) extra space besides sorting. Gotchas: sum overflow in 32-bit ints, duplicates (compare with the previous element), sorting loses original indices, Swift strings need Array(s) or String.Index, TypeScript’s default sort() compares numbers as strings.

Check this part

  1. After an in-place removeDuplicates returns k, which part of the array is valid?

  2. Which signal suggests a two-pointer solution?

  3. You adapt removeDuplicates to "keep at most two of each value". What changes?

  4. The input is unsorted, order matters, and you need the original indices of a pair with a given sum. Best tool?

Read and write pointers both move forward: read sees every element, write marks the next slot for the ones you keep. It cleans up an array in place, and the returned length says which part is valid.

Key takeaways

  • Two pointers are two indices into the same array or string, moving toward each other or in the same direction.
  • Opposite ends: palindromes and pair sums in sorted arrays, O(n) time and O(1) space.
  • Read/write pointers clean up an array in place; the function returns the new length.
  • Look for the signals: sorted input, comparing from both ends, or “in place with O(1) extra space”.
  • Each pointer move must discard candidates that provably can’t be the answer; be ready to explain the invariant.
  • k-sum is sort plus nested two-pointer search, O(n^(k−1)); duplicates are handled by comparing with the previous element.
  • Guard sums against 32-bit overflow on the JVM (use long), remember Swift traps on overflow, and TypeScript numbers are exact only up to 2⁵³.
  • Sorting destroys indices and costs O(n log n): when you need original positions on unsorted data, prefer a hash map.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.