If you could only master one data structure for coding interviews, make it the hash map. Hash maps (and their sibling sets) give you O(1) average-time add, remove, and lookup — turning nested O(n²) loops into single-pass O(n) algorithms over and over again.
This guide follows the same format as Two Pointers, Sliding Window, and Prefix Sum: core concepts, interactive walkthroughs, code in three languages, and a practice ladder. Focus on the interface — how to use built-in maps and sets — not implementing hash tables from scratch.
Interface vs Implementation
A data structure has two parts:
- Interface — what operations you can perform and what they return (
set,get,has,delete) - Implementation — how it works under the hood (hash function, collision handling, resizing)
In interviews you use the built-in hash map of your language. You won't be asked to implement one — but you should know what it gives you: key → value storage with fast lookup.
| Language | Hash Map | Hash Set |
|---|---|---|
| JavaScript | Map / Object | Set |
| TypeScript | Map | Set |
| Go | map[K]V | map[K]struct{} |
| Python | dict | set |
Keys must be immutable (or hashable). Arrays are mutable — convert to a tuple, sorted string, or delimited string before using as a key.
What Is Hashing?
A hash function converts any input (string, number, object) into an integer within a fixed range. The same key always maps to the same bucket.
key → hash(key) % tableSize → bucket indexCombined with an array, this creates a hash map: map keys to values without requiring integer indices.
Hash map — stores key-value pairs. Duplicate keys overwrite.
Hash set — stores keys only (no values). Adding the same element twice does nothing.
Five patterns cover most interview problems:
| Pattern | Structure | When to use |
|---|---|---|
| Existence / lookup | Set or Map | if x in arr in a loop → O(n²) without hashing |
| Frequency counting | Map key → count | Track how often elements appear in window or array |
| Complement lookup | Map value → index | Two Sum: search for target − num in O(1) |
| Prefix + frequency map | Map prefixSum → count | Count subarrays with sum exactly K |
| Grouping by signature | Map canonicalKey → group[] | Anagrams, digit sums, row/column signatures |
Hash Map vs Array
| Operation | Hash Map / Set | Array (unsorted) |
|---|---|---|
| Check if element exists | O(1) avg | O(n) |
| Add with lookup | O(1) avg | O(1) append, O(n) search |
| Two Sum on n elements | O(n) | O(n²) |
Tradeoffs: Hash maps have constant-factor overhead and can use more memory. For tiny inputs, a plain array scan can be faster. For unknown key ranges (e.g. value 999999999), a hash map is safer than sizing a giant array.
| Hash map / set | Array | |
|---|---|---|
| Existence check | O(1) average | O(n) scan |
| Key type | Almost any immutable key | Integer index only |
| Order | Unordered (usually) | Ordered by index |
| Space tradeoff | Overhead; may waste slots | Compact for dense indices |
| Small inputs | Constant factors can dominate | Often faster in practice |
Note: "O(1) hash map operations" are O(1) relative to map size n. Hashing a string of length m costs O(m).
Pattern 1 — Checking for Existence
Anytime you write if (x in arr) inside a loop, consider a set or map for O(1) lookup.
Example 1 — Two Sum
Given nums and target, return indices of two numbers that add to target.
Brute force: nested loops → O(n²).
Hash map: at index i, check if target - nums[i] is already in the map. Store nums[i] → i as you go.
Two Sum — Hash Map Lookup
nums = [2, 7, 11, 15], target = 9
nums
map
(empty)
lookup 7 → not found
i = 0, num = 2
Need target − num = 9 − 2 = 7. Map empty → no match.
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);
}
return [];
}
twoSum([2, 7, 11, 15], 9); // [0, 1]Why a map, not a set? We need indices, not just whether a pair exists.
Complexity: O(n) time, O(n) space.
Example 2 — First Letter to Appear Twice
Return the first character in s that appears twice.
Brute force: for each position, scan earlier characters → O(n²).
Set: if c is already in the set, return c. Otherwise add it.
First Letter to Appear Twice
s = "abccba" — O(1) existence checks with a set
input
set
(empty)
See 'a' — not in set
Add 'a' to set
function repeatedCharacter(s) {
const seen = new Set();
for (const c of s) {
if (seen.has(c)) return c;
seen.add(c);
}
return "";
}
repeatedCharacter("abccba"); // "c"Space: O(m) where m is the number of possible characters (26 for lowercase English — often called O(1) in interviews).
Example 3 — Isolated Numbers
Return all x in nums where neither x + 1 nor x - 1 exists in nums. Each valid x appears once in the answer.
Pre-process nums into a set, then check neighbors in O(1) per element.
function findIsolated(nums) {
const set = new Set(nums);
const ans = [];
for (const x of nums) {
if (!set.has(x + 1) && !set.has(x - 1) && !ans.includes(x)) {
ans.push(x);
}
}
return ans;
}
findIsolated([1, 2, 3, 5, 8]); // [5, 8]Rule of thumb: if (... in ...) inside a loop → try a hash set or map.
Pattern 2 — Counting Frequencies
Use a map key → count whenever you need to track how often something appears.
Example 4 — At Most K Distinct Characters
Given string s and integer k, find the length of the longest substring with at most k distinct characters.
This is a sliding window problem — but the constraint involves multiple characters, so an integer counter isn't enough. Use counts: Map.
counts.size= number of distinct characters in the window- Shrink from the left when
counts.size > k
At Most K Distinct Characters
s = "eceba", k = 2 — counts map tracks window
input
map
Expand — window [e]
counts = {e:1}, distinct = 1
function longestKDistinct(s, k) {
const counts = new Map();
let left = 0;
let best = 0;
for (let right = 0; right < s.length; right++) {
counts.set(s[right], (counts.get(s[right]) ?? 0) + 1);
while (counts.size > k) {
const c = s[left];
counts.set(c, counts.get(c) - 1);
if (counts.get(c) === 0) counts.delete(c);
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}
longestKDistinct("eceba", 2); // 3Complexity: O(n) time, O(k) space for the map.
Example 5 — Intersection of Multiple Arrays
Each inner array has distinct integers. Return sorted numbers that appear in all arrays.
A number appears in all n arrays ⟺ its count equals n.
function intersection(nums) {
const n = nums.length;
const counts = new Map();
for (const arr of nums) {
for (const x of arr) {
counts.set(x, (counts.get(x) ?? 0) + 1);
}
}
const ans = [];
for (const [x, c] of counts) {
if (c === n) ans.push(x);
}
return ans.sort((a, b) => a - b);
}
intersection([[3,1,2,4,5],[1,2,3,4],[3,4,5,6]]); // [3, 4]Why not an array? If max element is 1000, you'd need a size-1001 array mostly empty. A hash map handles sparse keys efficiently.
Example 6 — Equal Character Frequencies
Return true if every character in s appears the same number of times.
Count frequencies in a map, put all counts in a set, check set.size === 1.
function areOccurrencesEqual(s) {
const counts = new Map();
for (const c of s) counts.set(c, (counts.get(c) ?? 0) + 1);
return new Set(counts.values()).size === 1;
}
areOccurrencesEqual("abacbc"); // true
areOccurrencesEqual("aaabb"); // falsePattern 3 — Prefix Sum + Frequency Map
For exact subarray constraints (sum = K, exactly K odds), sliding window alone isn't enough. Combine prefix sums with a hash map counting how often each prefix has appeared.
Key idea: at index i, curr = prefix sum up to i. A subarray ending at i with sum k exists iff prefix curr - k was seen before.
ans += counts[curr - k]
counts[curr]++Always initialize counts[0] = 1 (empty prefix).
Example 7 — Subarray Sum Equals K
Subarray Sum Equals K
nums = [1, 2, 1, 2, 1], k = 3 — prefix + frequency map
nums
map
Initialize counts[0] = 1
Empty prefix has sum 0
function subarraySum(nums, k) {
const counts = new Map([[0, 1]]);
let curr = 0;
let ans = 0;
for (const num of nums) {
curr += num;
ans += counts.get(curr - k) ?? 0;
counts.set(curr, (counts.get(curr) ?? 0) + 1);
}
return ans;
}
subarraySum([1, 2, 1, 2, 1], 3); // 4Why a map, not a set? With negative numbers, the same prefix sum can occur multiple times — each occurrence is a valid start point.
Complexity: O(n) time and space.
Example 8 — Count Nice Subarrays (Exactly K Odds)
Count subarrays with exactly k odd numbers. Same pattern — but curr tracks odd count instead of sum:
curr += num % 2;
ans += counts.get(curr - k) ?? 0;The code differs by literally % 2 from the sum version.
Pattern 4 — Grouping by Signature
When items belong to the same group if they share a property (anagrams, digit sum, row pattern), use a canonical key:
| Problem | Signature key |
|---|---|
| Group Anagrams | Sorted string ("eat" → "aet") |
| Digit sum pairs | Sum of digits |
| Row/column pairs | Tuple or string of row values |
Example 9 — Group Anagrams
Group Anagrams — Key by Sorted String
strs = ["eat","tea","tan","ate","nat","bat"]
input
map
"eat" → key "aet"
groups["aet"] = ["eat"]
function groupAnagrams(strs) {
const groups = new Map();
for (const word of strs) {
const key = [...word].sort().join("");
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(word);
}
return [...groups.values()];
}
groupAnagrams(["eat","tea","tan","ate","nat","bat"]);
// [["eat","tea","ate"], ["tan","nat"], ["bat"]]Alternative key: length-26 frequency tuple — O(n·m) vs O(n·m log m) for sorting.
Example 10 — Shortest Subarray With Duplicate
Find shortest subarray containing a duplicate. Track most recent index per value — only store the last index, not all indices.
function minimumCardPickup(cards) {
const last = new Map();
let best = Infinity;
for (let i = 0; i < cards.length; i++) {
if (last.has(cards[i])) {
best = Math.min(best, i - last.get(cards[i]) + 1);
}
last.set(cards[i], i);
}
return best === Infinity ? -1 : best;
}
minimumCardPickup([1, 2, 6, 2, 1]); // 3Example 11 — Equal Row and Column Pairs
Count pairs (R, C) where row R equals column C as 1D arrays. Convert rows/columns to immutable tuple keys (string or joined values), count occurrences, multiply matching counts.
Complexity: O(n²) for an n×n grid.
Complexity Cheat Sheet
| Operation | Hash Map | Hash Set | Array scan |
|---|---|---|---|
| Add element | O(1)* | O(1)* | O(1) append |
| Check existence | O(1)* | O(1)* | O(n) |
| Delete element | O(1)* | O(1)* | O(n) |
| Two Sum (n elements) | O(n) | O(n)† | O(n²) |
| Subarray sum = K | O(n) | — | O(n²) |
* Average case; hashing strings costs O(m) for length m. † Set finds a pair but not indices.
Common Mistakes
Using an array when keys are sparse. [1, 2, 1000] doesn't mean you need an array of size 1000.
Forgetting counts[0] = 1 in prefix-frequency problems — you lose subarrays that start at index 0.
Using a set when you need frequencies. Duplicate prefix sums require counting, not just existence.
Mutable keys. Don't use arrays as map keys directly — sort/join/tuple first.
Assuming hash map order. Iteration order is not sorted (in most languages).
Ignoring O(m) string hashing. Hashing a string of length m is not O(1).
Practice Problems (Easiest → Hardest)
Easiest → Hardest
Step through the animations in this post first, then click any problem to open it on LeetCode.
Quick Reference — Pattern Picker
Pattern Picker
Click a node to trace the path — cards below update with your selection
Key Takeaways
- Hash map = O(1) lookup — the single biggest time-complexity upgrade in interviews.
if (x in arr)in a loop → use a set or map.- Count frequencies with
map[key]++— windows, intersections, anagrams. - Two Sum pattern: search for
target - num, not pairs of loops. - Prefix + freq map:
counts[curr - k]counts exact subarray constraints. - Group by signature: sorted string, digit sum, or tuple as map key.
- Sets don't track frequency — adding the same key 100 times still yields size 1.
- Step through the animations — if you can narrate each map update, you can code it.
Hash maps appear in almost every topic from here on. Pair this guide with Prefix Sum for the prefix-frequency pattern, and Sliding Window when the constraint lives inside a contiguous window.
Share this post