Part 5 · Lesson 26 · 22 min read

Two Sum and “Seen Before?”

Trading memory for speed with a map of what you have already met.

On this page

  1. Trade memory for speed
  2. Two Sum, three ways
  3. Traced: Two Sum with a Map
  4. Check first, then store
  5. What should the Map remember?
  6. Duplicates within distance k
  7. Longest consecutive sequence
  8. Counting subarrays with a Map
  9. Practice questions (6)
  10. Make it stick
  11. What's next

Trade memory for speed

Part 5 is about one idea that solves many problems: as you walk through the input, remember what you have seen in a Map or a Set. A Map is a collection of key → value pairs, like a phone book that finds a number from a name. A Set is a collection of unique values that can tell you quickly whether a value is inside, like a guest list. Both find an item in O(1) time, which means the time does not grow when the collection gets bigger.

Now a question that would need an inner loop, such as “is there an earlier value that…?”, becomes one O(1) lookup. You pay O(n) extra memory, and in return O(n²) time becomes O(n) time.

Two Sum, three ways

The task: find the two values in an unsorted array that add up to a target, and return their indices (positions). It is the most famous interview question. It shows three levels of solution clearly. The first is the brute force, which means trying every possibility without being clever:

JavaScript
function twoSumBrute(nums, target) {  for (let i = 0; i < nums.length; i++)    for (let j = i + 1; j < nums.length; j++)      if (nums[i] + nums[j] === target) return [i, j];}function twoSumSorted(nums, target) {  // Sort indices by value so the original positions are not lost  const idx = nums.map((_, i) => i).sort((a, b) => nums[a] - nums[b]);  let l = 0, r = idx.length - 1;  while (l < r) {    const sum = nums[idx[l]] + nums[idx[r]];    if (sum === target) return [idx[l], idx[r]].sort((a, b) => a - b);    if (sum < target) l++;    else r--;  }}console.log(twoSumBrute([3, 8, 4, 5], 9));  // [ 2, 3 ]console.log(twoSumSorted([3, 8, 4, 5], 9)); // [ 2, 3 ]
ApproachTimeExtra spaceNotes
Check every pairO(n²)O(1)The brute force
Sort + two pointers (Lesson 21)O(n log n)O(n)Must sort indices to keep the original positions
Map of seen valuesO(n)O(n)The expected answer

Traced: Two Sum with a Map

For each value x, the partner it needs is target − x. Do not search the whole array for the partner. Look it up in the Map of values you have already passed:

step 1 / 13
Set up
seen = new Map()

It maps each value to its index, for every value we have already passed.

1const nums = [3, 8, 4, 5], target = 9;▶const seen = new Map();3for (let i = 0; i < nums.length; i++) {4  const need = target - nums[i];5  if (seen.has(need)) {6    console.log([seen.get(need), i]);7    break;8  }9  seen.set(nums[i], i);10}
Variables
nums[3, 8, 4, 5]
target9
seenMap(0) {}
Console
(nothing printed yet)
One pass. Each value first asks “has my partner appeared?”. Then it adds itself to the Map for the values that come later. · interactive

Check first, then store

The order of the two lines inside the loop matters. If you check before you store, a value can never pair with itself. Take nums = [3, 5] and target 6. If you stored first, the Map would already contain 3 when you look for the partner of 3, and you would wrongly return [0, 0]. With [3, 3], checking first still works: the second 3 finds the first 3.

The “seen before?” loop
for (let i = 0; i < nums.length; i++) {
  if (seen has what nums[i] needs) → answer
  record nums[i] in seen
}

What should the Map remember?

The key is usually the value from the array. What you store with it depends on what the question asks for:

Dry run · choosing what to store
Question asks for…StoreExample
whether it appeareda Setcontains duplicate
where it appearedvalue → indexTwo Sum
how close the last copy isvalue → latest index (overwrite)duplicates within k
the longest spanvalue → first index (never overwrite)longest subarray with sum 0
how many times it appearedvalue → countcount pairs, subarray sum = k

Duplicates within distance k

The task: is there a value that repeats within k positions? Store each value's most recent index. An older copy is never closer than the latest copy, so overwriting the old index loses nothing.

JavaScript
function containsNearbyDuplicate(nums, k) {  const last = new Map();                        // value → most recent index  for (let i = 0; i < nums.length; i++) {    if (last.has(nums[i]) && i - last.get(nums[i]) <= k) return true;    last.set(nums[i], i);                        // overwrite: only the latest index matters  }  return false;}console.log(containsNearbyDuplicate([1, 2, 3, 1], 3));       // trueconsole.log(containsNearbyDuplicate([1, 2, 3, 1, 2, 3], 2)); // false

Longest consecutive sequence

The task: find the longest run of consecutive integers (numbers that follow each other, like 1, 2, 3, 4). The numbers can be in any order in the array. For example, [100, 4, 200, 1, 3, 2] contains 1, 2, 3, 4, so the answer is 4. Sorting takes O(n log n) time. A Set takes O(n) time with one clever rule: only start counting from a value x when x − 1 is not in the Set. That means x is the start of a run.

JavaScript
function longestConsecutive(nums) {  const set = new Set(nums);  let best = 0;  for (const x of set) {    if (set.has(x - 1)) continue;          // not the start of a run: skip    let len = 1;    while (set.has(x + len)) len++;        // walk up the run: x, x+1, x+2 …    best = Math.max(best, len);  }  return best;}console.log(longestConsecutive([100, 4, 200, 1, 3, 2]));        // 4   (1, 2, 3, 4)console.log(longestConsecutive([0, 3, 7, 2, 5, 8, 4, 6, 0, 1])); // 9

Without that rule, counting from every value would take O(n²) time on an input like 1, 2, 3 … n. With the rule, the inner while loop visits each value only once, from the run it belongs to. So the total work is O(n).

Dry run · [100, 4, 200, 1, 3, 2]
xx − 1 in set?ActionRun length
100nocount 1001
4yes (3)skip
200nocount 2001
1nocount 1, 2, 3, 44
3yesskip
2yesskip

Counting subarrays with a Map

A subarray is a block of neighbouring items inside an array. A prefix sum is the total of all items from the start up to the current position. Lesson 20 counted subarrays with sum k by storing counts of prefix sums. The same “seen before” idea works for any running total. For “subarrays with exactly k odd numbers”, the running total is the number of odd values so far:

JavaScript
// Count subarrays with exactly k odd numbers: prefix counts of odd numbersfunction numberOfSubarrays(nums, k) {  const seen = new Map([[0, 1]]);  let odds = 0, count = 0;  for (const x of nums) {    if (x % 2 === 1) odds++;    count += seen.get(odds - k) ?? 0;    seen.set(odds, (seen.get(odds) ?? 0) + 1);  }  return count;}console.log(numberOfSubarrays([1, 1, 2, 1, 1], 3)); // 2

The pattern is always the same. Keep a running value. Ask how many earlier running values would make the current subarray valid. Then record the current running value.

Practice questions

Before you write code for each question, say what the key is and what value the Map stores with it.

Q1Two SumEasy

Return the indices of the two values that add up to target. Exactly one answer exists.

Example 1
Input: nums = [2, 7, 11, 15], target = 9
Output: [0, 1]
Explanation: 2 + 7 = 9.
Example 2
Input: nums = [3, 2, 4], target = 6
Output: [1, 2]
Explanation: 2 + 4. Not [0, 0]: a value cannot be used twice.
Example 3
Input: nums = [3, 3], target = 6
Output: [0, 1]
Explanation: Two different positions with the same value are fine.
Stuck? Open one hint at a time
Hint 1
For each x, the partner is target − x. Look it up among the values already seen.
Show the solution with explanations
Approach 1Map of value → index
The idea

Check for the partner, then store the current value.

JavaScript
function twoSum(nums, target) {  const seen = new Map();  for (let i = 0; i < nums.length; i++) {    const need = target - nums[i];    if (seen.has(need)) return [seen.get(need), i];    seen.set(nums[i], i);  }}console.log(twoSum([2, 7, 11, 15], 9)); // [ 0, 1 ]console.log(twoSum([3, 2, 4], 6));      // [ 1, 2 ]console.log(twoSum([3, 3], 6));         // [ 0, 1 ]
How it works

O(n) time, O(n) space. In an interview, say the O(n²) brute force first, then improve it.

Which approach should you use?

The same pattern appears in many other questions. Learn the pattern, not just this answer. (LeetCode 1.)

Q2Contains duplicate within kEasy

Return true if two equal values are at most k positions apart.

Example 1
Input: nums = [1, 0, 1, 1], k = 1
Output: true
Explanation: The 1s at indices 2 and 3 are 1 apart.
Example 2
Input: nums = [1, 2, 3, 1, 2, 3], k = 2
Output: false
Explanation: Copies are always 3 apart.
Stuck? Open one hint at a time
Hint 1
Store each value's latest index.
Show the solution with explanations
Approach 1Map of latest index
The idea

When a value repeats, compare the distance to its latest copy. Then update the stored index.

JavaScript
function containsNearbyDuplicate(nums, k) {  const last = new Map();  for (let i = 0; i < nums.length; i++) {    if (i - (last.get(nums[i]) ?? -Infinity) <= k) return true;    last.set(nums[i], i);  }  return false;}console.log(containsNearbyDuplicate([1, 0, 1, 1], 1));       // trueconsole.log(containsNearbyDuplicate([1, 2, 3, 1, 2, 3], 2)); // false
How it works

O(n) time, O(n) space. For a value never seen, we use -Infinity. This makes the distance infinitely large, so the check fails and we do not need a separate has check.

Which approach should you use?

Lesson 22 solved this with a Set window of size k (O(k) space). This version is simpler to write. (LeetCode 219.)

Q3Longest consecutive sequenceMedium

Return the length of the longest run of consecutive integers that can be formed from the values of nums, in O(n) time.

Example 1
Input: [100, 4, 200, 1, 3, 2]
Output: 4
Explanation: 1, 2, 3, 4.
Example 2
Input: [0, 3, 7, 2, 5, 8, 4, 6, 0, 1]
Output: 9
Explanation: 0 to 8.
Example 3
Input: []
Output: 0
Explanation: Edge case.
Stuck? Open one hint at a time
Hint 1
Sorting is O(n log n). The problem asks for O(n).
Hint 2
Put everything in a Set. Only start counting from values whose predecessor is missing.
Show all 2 solutions with explanations
Approach 1Sort, then scan
The idea

Sort the array. Then walk through it and make the run longer when the next value is exactly one more. Skip duplicates.

JavaScript
function longestConsecutive(nums) {  if (nums.length === 0) return 0;  const a = [...nums].sort((x, y) => x - y);  let best = 1, run = 1;  for (let i = 1; i < a.length; i++) {    if (a[i] === a[i - 1]) continue;    run = a[i] === a[i - 1] + 1 ? run + 1 : 1;    best = Math.max(best, run);  }  return best;}console.log(longestConsecutive([100, 4, 200, 1, 3, 2])); // 4
How it works

O(n log n).

Approach 2Set, count from run starts
The idea

The method from the lesson.

JavaScript
function longestConsecutive(nums) {  const set = new Set(nums);  let best = 0;  for (const x of set) {    if (set.has(x - 1)) continue;    let len = 1;    while (set.has(x + len)) len++;    best = Math.max(best, len);  }  return best;}console.log(longestConsecutive([100, 4, 200, 1, 3, 2]));         // 4console.log(longestConsecutive([0, 3, 7, 2, 5, 8, 4, 6, 0, 1])); // 9console.log(longestConsecutive([]));                             // 0
How it works

O(n) time, O(n) space. Looping over the Set instead of the array also skips duplicate values.

Which approach should you use?

Explain why the inner while loop does not make it O(n²). The interviewer wants to hear this explanation. (LeetCode 128.)

Q4Maximum number of k-sum pairsMedium

In one operation you remove two values that add up to k. Return the maximum number of operations.

Example 1
Input: nums = [1, 2, 3, 4], k = 5
Output: 2
Explanation: Remove (1, 4) and (2, 3).
Example 2
Input: nums = [3, 1, 3, 4, 3], k = 6
Output: 1
Explanation: Only one (3, 3) pair can be formed. The third 3 has no partner left.
Stuck? Open one hint at a time
Hint 1
This is like Two Sum, but each value can be used only once. Store the counts of values that are still unpaired.
Show all 2 solutions with explanations
Approach 1Map of unpaired counts
The idea

If an unpaired partner exists, use it and decrease its count. Otherwise, store this value as unpaired.

JavaScript
function maxOperations(nums, k) {  const waiting = new Map();  let pairs = 0;  for (const x of nums) {    const need = k - x;    if ((waiting.get(need) ?? 0) > 0) {      waiting.set(need, waiting.get(need) - 1);      pairs++;    } else {      waiting.set(x, (waiting.get(x) ?? 0) + 1);    }  }  return pairs;}console.log(maxOperations([1, 2, 3, 4], 5));    // 2console.log(maxOperations([3, 1, 3, 4, 3], 6)); // 1
How it works
Dry run · [3, 1, 3, 4, 3], k = 6
xneedpartner waiting?pairswaiting after
33no0{3: 1}
15no0{3: 1, 1: 1}
33yes1{3: 0, 1: 1}
42no1{3: 0, 1: 1, 4: 1}
33no (count 0)1{3: 1, 1: 1, 4: 1}
Approach 2Sort + two pointers
The idea

Use the sorted pair-sum method, but count every match and move both pointers.

JavaScript
function maxOperations(nums, k) {  const a = [...nums].sort((x, y) => x - y);  let l = 0, r = a.length - 1, pairs = 0;  while (l < r) {    const s = a[l] + a[r];    if (s === k) { pairs++; l++; r--; }    else if (s < k) l++;    else r--;  }  return pairs;}console.log(maxOperations([1, 2, 3, 4], 5)); // 2
How it works

O(n log n) time, O(1) extra space after sorting.

Which approach should you use?

Map: O(n) time, O(n) space. Sort: O(n log n) time, less memory. Mention both in an interview. (LeetCode 1679.)

Q5Continuous subarray sumMedium

Return true if some subarray of length at least 2 has a sum that is a multiple of k.

Example 1
Input: nums = [23, 2, 4, 6, 7], k = 6
Output: true
Explanation: [2, 4] sums to 6.
Example 2
Input: nums = [23, 2, 6, 4, 7], k = 13
Output: false
Explanation: No subarray of length ≥ 2 has a sum divisible by 13.
Example 3
Input: nums = [5, 0, 0], k = 3
Output: true
Explanation: [0, 0] sums to 0, and 0 is a multiple of every k.
Stuck? Open one hint at a time
Hint 1
The remainder is what is left after dividing (the % operator). If two prefix sums have the same remainder when divided by k, the subarray between them has a sum that is a multiple of k.
Hint 2
The subarray must have at least 2 items. So store the first index of each remainder.
Show the solution with explanations
Approach 1Remainder → first index
The idea

Walk through the array with a running remainder. If the same remainder appeared at least 2 positions earlier, the part in between is a valid answer.

JavaScript
function checkSubarraySum(nums, k) {  const first = new Map([[0, -1]]);       // remainder 0 "seen" before index 0  let sum = 0;  for (let i = 0; i < nums.length; i++) {    sum = (sum + nums[i]) % k;    if (first.has(sum)) {      if (i - first.get(sum) >= 2) return true;    } else {      first.set(sum, i);    }  }  return false;}console.log(checkSubarraySum([23, 2, 4, 6, 7], 6));  // trueconsole.log(checkSubarraySum([23, 2, 6, 4, 7], 13)); // falseconsole.log(checkSubarraySum([5, 0, 0], 3));         // true
How it works

O(n) time, O(min(n, k)) space. Keeping only the first index gives the longest possible subarray, so the length check is as easy to pass as possible. The values here are not negative, so we do not need to fix negative remainders.

Which approach should you use?

Three ideas meet here: prefix sums (Lesson 20), remainders (Lesson 13) and “store the first index” (this lesson). (LeetCode 523.)

Q6Count nice subarraysMedium

Return the number of subarrays that contain exactly k odd numbers.

Example 1
Input: nums = [1, 1, 2, 1, 1], k = 3
Output: 2
Explanation: [1, 1, 2, 1] and [1, 2, 1, 1].
Example 2
Input: nums = [2, 4, 6], k = 1
Output: 0
Explanation: No odd numbers.
Stuck? Open one hint at a time
Hint 1
Turn each value into 1 (if odd) or 0 (if even). Now the question is “how many subarrays have a sum equal to k?”.
Show the solution with explanations
Approach 1Prefix count of odds + Map
The idea

The code from the lesson.

JavaScript
function numberOfSubarrays(nums, k) {  const seen = new Map([[0, 1]]);  let odds = 0, count = 0;  for (const x of nums) {    odds += x % 2;    count += seen.get(odds - k) ?? 0;    seen.set(odds, (seen.get(odds) ?? 0) + 1);  }  return count;}console.log(numberOfSubarrays([1, 1, 2, 1, 1], 3)); // 2console.log(numberOfSubarrays([2, 4, 6], 1));       // 0
How it works

O(n) time. Lesson 23's “at most k minus at most (k − 1)” window also works, because the values we count are never negative.

Which approach should you use?

The main skill this part teaches is to change a new problem into one you already know (here, subarray sum = k). (LeetCode 1248.)

Make it stick

Make it stick — close this page, then
  1. Write Two Sum with a Map from memory and explain why you check before storing.
  2. Name four things a Map can store with each value, with one problem for each.
  3. Explain why longest-consecutive-sequence is O(n) even with a while loop inside the for loop.

Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.

What's next

Lesson 27 uses Maps to group things: anagrams (words made from the same letters) that share a key, the top k most frequent values, and the majority element. It also shows a clever voting algorithm that uses O(1) extra space.

Practise on LeetCode

Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series