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
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:
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 ]| Approach | Time | Extra space | Notes |
|---|---|---|---|
| Check every pair | O(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 values | O(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:
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}| nums | [3, 8, 4, 5] |
| target | 9 |
| seen | Map(0) {} |
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.
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:
| Question asks for… | Store | Example |
|---|---|---|
| whether it appeared | a Set | contains duplicate |
| where it appeared | value → index | Two Sum |
| how close the last copy is | value → latest index (overwrite) | duplicates within k |
| the longest span | value → first index (never overwrite) | longest subarray with sum 0 |
| how many times it appeared | value → count | count 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.
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)); // falseLongest 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.
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])); // 9Without 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).
| x | x − 1 in set? | Action | Run length |
|---|---|---|---|
| 100 | no | count 100 | 1 |
| 4 | yes (3) | skip | |
| 200 | no | count 200 | 1 |
| 1 | no | count 1, 2, 3, 4 | 4 |
| 3 | yes | skip | |
| 2 | yes | skip |
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:
// 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)); // 2The 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.
Return the indices of the two values that add up to target. Exactly one answer exists.
Hint 1
Show the solution with explanations
Check for the partner, then store the current value.
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 ]O(n) time, O(n) space. In an interview, say the O(n²) brute force first, then improve it.
The same pattern appears in many other questions. Learn the pattern, not just this answer. (LeetCode 1.)
Return true if two equal values are at most k positions apart.
Hint 1
Show the solution with explanations
When a value repeats, compare the distance to its latest copy. Then update the stored index.
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)); // falseO(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.
Lesson 22 solved this with a Set window of size k (O(k) space). This version is simpler to write. (LeetCode 219.)
Return the length of the longest run of consecutive integers that can be formed from the values of nums, in O(n) time.
Hint 1
Hint 2
Show all 2 solutions with explanations
Sort the array. Then walk through it and make the run longer when the next value is exactly one more. Skip duplicates.
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])); // 4O(n log n).
The method from the lesson.
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([])); // 0O(n) time, O(n) space. Looping over the Set instead of the array also skips duplicate values.
Explain why the inner while loop does not make it O(n²). The interviewer wants to hear this explanation. (LeetCode 128.)
In one operation you remove two values that add up to k. Return the maximum number of operations.
Hint 1
Show all 2 solutions with explanations
If an unpaired partner exists, use it and decrease its count. Otherwise, store this value as unpaired.
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| x | need | partner waiting? | pairs | waiting after |
|---|---|---|---|---|
| 3 | 3 | no | 0 | {3: 1} |
| 1 | 5 | no | 0 | {3: 1, 1: 1} |
| 3 | 3 | yes | 1 | {3: 0, 1: 1} |
| 4 | 2 | no | 1 | {3: 0, 1: 1, 4: 1} |
| 3 | 3 | no (count 0) | 1 | {3: 1, 1: 1, 4: 1} |
Use the sorted pair-sum method, but count every match and move both pointers.
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)); // 2O(n log n) time, O(1) extra space after sorting.
Map: O(n) time, O(n) space. Sort: O(n log n) time, less memory. Mention both in an interview. (LeetCode 1679.)
Return true if some subarray of length at least 2 has a sum that is a multiple of k.
Hint 1
% 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
Show the solution with explanations
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.
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)); // trueO(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.
Three ideas meet here: prefix sums (Lesson 20), remainders (Lesson 13) and “store the first index” (this lesson). (LeetCode 523.)
Return the number of subarrays that contain exactly k odd numbers.
Hint 1
Show the solution with explanations
The code from the lesson.
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)); // 0O(n) time. Lesson 23's “at most k minus at most (k − 1)” window also works, because the values we count are never negative.
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
- Write Two Sum with a Map from memory and explain why you check before storing.
- Name four things a Map can store with each value, with one problem for each.
- 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