Skip to the chapter
←DSAGGroundworkΣDSA
DSA/Beginner/B5
5 / 48 written chapters
/

Levels and plan
  1. B1Complexity analysis
  2. B2JavaScript toolkit
  3. B3Arrays & strings
  4. B4Prefix sums
  5. B5Hashing
  6. B6Two pointers
  7. B7Sliding window
  8. B8Binary search
  9. B9Basic recursion
  10. B10Sorting algorithms
  11. B11Stacks & queues
  12. B12Linked lists
  1. I1Trees
  2. I2BST operations
  3. I3Tree problems in depth
  4. I4Heaps & priority queues
  5. I5Graphs: representation
  6. I6BFS on grids
  7. I7Graph problems
  8. I8Topological patterns
  9. I9Backtracking
  10. I10DP: 1D
  11. I11DP: 2D
  12. I12DP state machines
  13. I13Greedy algorithms
  14. I14Intervals
  15. I15Bit manipulation
  16. I16Matrix problems
  17. I17Tries
  18. I18Monotonic stack & queue
  19. I19Math for interviews
  20. I20Design & simulation
  21. I21Divide and conquer
  22. I22Randomization
  1. A1Advanced DP
  2. A2Union-Find
  3. A3Advanced graph algorithms
  4. A4Minimum Spanning Tree
  5. A5Advanced graphs
  6. A6Segment & Fenwick trees
  7. A7Sparse tables
  8. A8String algorithms
  9. A9Design problems
  10. A10Advanced backtracking
  11. A11Games & probability DP
  12. A12Ordered structures
  13. A13Sweep line & geometry
  14. A14Interview strategy

ΣB5 · Part 1 · Beginner

Hashing

The single most common way an O(n²) brute force becomes O(n).

20 min read1 diagram14 sections20 exercises
5 of 48

In this chapter

  • What a hash map actually does
  • Map/Set vs plain objects
  • The pattern: trade space for time
  • Frequency counting — the other 80% of hashing questions
  • Grouping — building a Map of arrays

and 9 more

about 20 min
14On this page
  1. What a hash map actually does
  2. Map/Set vs plain objects
  3. The pattern: trade space for time
  4. Frequency counting — the other 80% of hashing questions
  5. Grouping — building a Map of arrays
  6. Set membership: asking "is the neighbour here?"
  7. Top-k frequent: count, then bucket by frequency
  8. Canonical keys: make things that are equal look equal
  9. Bijections: two maps for isomorphic strings and word patterns
  10. Composite keys: a Map compares objects by reference
  11. Complement lookups: find each value's partner
  12. How a hash map works inside: buckets, collisions and resizing
  13. Build a HashMap from scratch
  14. When hashing is the wrong tool

What a hash map actually does

A hash function turns a key into a number, and that number picks a "bucket" in an underlying array. Look-up doesn't search — it computes the bucket from the key and jumps straight there. That's why Map/Set/object lookups are O(1) average case: the cost of hashing the key doesn't grow with how many other keys are already stored.

key: "cat" hash("cat") = 2 0 1 2 3 4 "cat" lands here — no scanning
The hash turns "which bucket" into arithmetic instead of a search.

Map/Set vs plain objects

Map / SetPlain object
Key typesanything (objects, NaN, etc.)strings/symbols only — numbers get coerced
Size.size, O(1)Object.keys(o).length, O(n)
Iteration orderinsertion order, guaranteedmostly insertion order, but integer-like keys sort first
Accidental prototype keysimpossible"toString" in {} is true

In interviews, default to Map/Set unless there's a specific reason not to — it sidesteps a whole category of "wait, why is this key already there" bugs.

The pattern: trade space for time

Almost every "hashing" interview question is the same trade: spend O(n) space to remember what you've already seen, so a second O(n) pass (or even the same pass) can answer "have I seen this before?" in O(1) instead of O(n).

// Two Sum — brute force: O(n²) time, O(1) space
function twoSumSlow(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];
    }
  }
}

// Two Sum — hashed: O(n) time, O(n) space
function twoSumFast(nums, target) {
  const seen = new Map(); // value → index
  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);
  }
}

Dry run: twoSumFast([2, 7, 11, 15], 9)

inums[i]need = target − nums[i]seen.has(need)?action
027no — map is emptystore {2 → 0}
172yes — seen.get(2) = 0return [0, 1] ✓

Notice the map is only ever looked up for the value we still need, and only ever written for values we've already passed. That single pass does the work a nested loop would need two passes (and O(n²) time) to do.

Say it like this → "I'll trade O(n) space for a hash map so each lookup is O(1) instead of O(n) — that turns the O(n²) nested-loop version into a single O(n) pass."

Frequency counting — the other 80% of hashing questions

function frequency(arr) {
  const counts = new Map();
  for (const x of arr) {
    counts.set(x, (counts.get(x) || 0) + 1);
  }
  return counts;
}

// anagram check: same characters, same counts
function isAnagram(a, b) {
  if (a.length !== b.length) return false;
  const counts = new Map();
  for (const c of a) counts.set(c, (counts.get(c) || 0) + 1);
  for (const c of b) {
    if (!counts.get(c)) return false;
    counts.set(c, counts.get(c) - 1);
  }
  return true;
}

Grouping — building a Map of arrays

// group anagrams: same sorted letters → same bucket
function groupAnagrams(words) {
  const groups = new Map();
  for (const word of words) {
    const key = [...word].sort().join("");
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(word);
  }
  return [...groups.values()];
}
⚠ Hash collisions aren't your problem, but worst case is V8's hash maps handle collisions internally, so in a normal solution you never write collision-resolution code (the from-scratch section below is the exception). But a hash map's O(1) is an average case — a pathological hash function can degrade to O(n) per operation. You'll never need to defend against this in an interview, but "average case, not worst case" is the correct answer if asked.

Set membership: asking "is the neighbour here?"

Many hashing problems do not need a count or an index, only the answer to "is this value present?". A Set gives that in O(1), and it changes what you can afford to do inside a loop. Two problems show it.

Longest consecutive sequence. Given unsorted integers, find the length of the longest run of consecutive values, such as 1, 2, 3, 4, in O(n) time. Sorting gives O(n log n). With a set, you can ask "does x + 1 exist?" directly, so a run can be walked one step at a time. The danger is walking the same run many times. If you start a walk from every element, the run 1, 2, ..., n is walked once from each of its n elements, n(n+1)/2 steps in all, O(n²). The fix is to start a walk only at the beginning of a run, which is a value whose predecessor is missing.

function longestConsecutive(nums) {
  const set = new Set(nums);
  let best = 0;
  for (const x of set) {
    if (set.has(x - 1)) continue;
    let length = 1;
    while (set.has(x + length)) length++;
    if (length > best) best = length;
  }
  return best;
}

Dry run: longestConsecutive([100, 4, 200, 1, 3, 2])

xset.has(x − 1)?walklength
100no, so a run starts here101 is missing1
4yes (3), skipnone-
200no, so a run starts here201 is missing1
1no, so a run starts here2, 3, 4 are present, 5 is missing4
3yes (2), skipnone-
2yes (1), skipnone-

The answer is 4. Why this is O(n) even though there is a while inside a for: the inner loop only runs from a run's first element, and every value belongs to exactly one run, so across the whole call the inner steps visit each value at most once. The outer loop adds n more. Total at most 2n set lookups.

⚠ Loop over the Set, not the array The code iterates set, not nums. With duplicates, looping over the array repeats the walk for every copy of a run's start. For [1, 1, 1, ..., 1, 2, 3, ..., n/2] (n/2 copies of 1, then a run of n/2 values) that is n/2 walks of length n/2: for n = 20,000 the inner loop steps 100,000,000 times instead of about 10,000. Deduplicating through the Set first removes the problem.

Happy number. Replace a number by the sum of the squares of its digits, and repeat. Does it reach 1, or loop forever? A set of the values already seen answers it: stop at 1 (happy), or at a value you have met before (a cycle, so never happy).

function digitSquareSum(n) {
  let sum = 0;
  while (n > 0) {
    const digit = n % 10;
    sum += digit * digit;
    n = Math.floor(n / 10);
  }
  return sum;
}

function isHappy(n) {
  const seen = new Set();
  while (n !== 1 && !seen.has(n)) {
    seen.add(n);
    n = digitSquareSum(n);
  }
  return n === 1;
}

isHappy(19) follows 19 → 82 → 68 → 100 → 1 and returns true. isHappy(2) follows 2 → 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4 and meets the 4 again, so it returns false. The loop must end, because the sequence cannot grow forever: a number with d digits maps to at most 81 · d, which is smaller than the number itself from four digits up, and any number below 1,000 maps to at most 243. So every sequence lives in a finite range, and a finite range forces either 1 or a repeat. The set costs O(1) extra per step; remembering nothing and detecting the cycle with a slow and a fast pointer (see Two pointers) trades the set for O(1) space.

Top-k frequent: count, then bucket by frequency

"Return the k most frequent values" has a standard first half: count every value with a Map, which costs O(n). The second half, picking the top k counts, can be done in several ways, and the comparison is a classic interview discussion.

Second halfTime (m distinct values)Extra spaceNote
Sort all (value, count) pairsO(m log m)O(m)simplest; fine when m is small
Min-heap of size kO(m log k)O(k) beyond the mapbest when k is much smaller than m; see heaps
Bucket by frequencyO(n)O(n)works because a count is a small integer

The bucket idea: a count can only be a whole number from 1 to n, so make an array indexed by count, and put each value in the bucket of its count. The most frequent values then sit in the highest buckets; read from the top until you have k. It is counting sort applied to the counts themselves.

function topKFrequent(nums, k) {
  const counts = new Map();
  for (const x of nums) counts.set(x, (counts.get(x) || 0) + 1);

  const buckets = Array.from({ length: nums.length + 1 }, () => []);
  for (const [value, count] of counts) buckets[count].push(value);

  const out = [];
  for (let count = buckets.length - 1; count >= 1 && out.length < k; count--) {
    for (const value of buckets[count]) {
      out.push(value);
      if (out.length === k) break;
    }
  }
  return out;
}

Dry run: topKFrequent([1, 1, 1, 2, 2, 3], 2)

stepstate
countMap {1 → 3, 2 → 2, 3 → 1}
bucketbuckets[1] = [3], buckets[2] = [2], buckets[3] = [1], every other bucket is empty (indexes 0 to 6)
read from index 6 downbuckets 6, 5, 4 are empty
count = 3take 1; out = [1]
count = 2take 2; out = [1, 2]; length is k, stop

Time O(n): counting n values, filling at most m buckets, and reading at most n + 1 buckets, with O(n) extra space for the buckets. When the boundary bucket holds more values than there are slots left, the statement has to say which to take: if it guarantees a unique answer there is no such tie, and if it asks for, say, alphabetical order among ties, sort the values inside each bucket.

⚠ Do not fill the buckets with one shared array new Array(n + 1).fill([]) puts the same array into every slot, so a push to buckets[2] shows up in every bucket. Create each one with Array.from(..., () => []), the same fix as the shared-row bug for 2D grids in Arrays & strings.

Canonical keys: make things that are equal look equal

Grouping problems ask which items are "the same" in some sense: anagrams, shifted strings, patterns with the same shape. The hashing recipe is to compute a canonical key for each item, a value that is identical for every item in the same class and different for items in different classes, and then group by that key in a Map. The sorted-letters key from the grouping section above is one. Its cost is O(L log L) per word of length L. Counting letters gives a faster one:

function groupAnagramsByCount(words) {
  const groups = new Map();
  for (const word of words) {
    const counts = new Array(26).fill(0);
    for (const ch of word) counts[ch.charCodeAt(0) - 97]++;
    const key = counts.join("#");
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(word);
  }
  return [...groups.values()];
}

Building a key costs O(L + 26) per word, so the whole grouping is O(n · (L + 26)), against O(n · L log L) for sorting each word. The "#" separator is not decoration. Without it, the count lists [1, 11] and [11, 1] would both join to "111", and two different words would share a key. A good key has three properties: equal items give equal keys; different items give different keys (so separators or fixed widths); and building it is cheap.

The same recipe handles "shifted" strings ("abc" and "bcd" are the same shape). Describe a word by the gaps between neighbouring letters instead of the letters themselves:

function shiftKey(word) {
  const diffs = [];
  for (let i = 1; i < word.length; i++) {
    diffs.push((word.charCodeAt(i) - word.charCodeAt(i - 1) + 26) % 26);
  }
  return diffs.join(",");
}

shiftKey("abc") and shiftKey("bcd") are both "1,1"; shiftKey("az") is "25" and shiftKey("ba") is "25" too, because the + 26 and % 26 wrap the gap around the end of the alphabet. A single letter has no gaps, so its key is the empty string, and all single-letter words land in one group, which is right since each is a shift of the other.

Bijections: two maps for isomorphic strings and word patterns

Some problems need a one-to-one correspondence, not just "same group". "egg" and "add" are isomorphic: replace e with a and g with d, and no two different letters may map to the same letter. One map is not enough. With only a forward map from s to t, the strings "bar" and "foo" pass, because b → f, a → o and r → o are each consistent, even though two different letters collide on o. A second map, in the opposite direction, catches that.

function isIsomorphic(s, t) {
  if (s.length !== t.length) return false;
  const forward = new Map();
  const backward = new Map();
  for (let i = 0; i < s.length; i++) {
    const a = s[i];
    const b = t[i];
    if (forward.has(a) && forward.get(a) !== b) return false;
    if (backward.has(b) && backward.get(b) !== a) return false;
    forward.set(a, b);
    backward.set(b, a);
  }
  return true;
}

Dry run: isIsomorphic("bar", "foo")

ia → bforward checkbackward check
0b → fb is newf is new; store b→f and f→b
1a → oa is newo is new; store a→o and o→a
2r → or is newbackward.get("o") is "a", not "r": return false

Word pattern is the same problem with a pattern letter on one side and a whole word, from s.split(" "), on the other. Check that the number of words equals the pattern length first, or the loop will compare against undefined. An equivalent approach is a canonical key: replace each character by the index of its first appearance, so "egg" and "add" both become [0, 1, 1], and compare the two lists. The two-map version stops at the first conflict and is easier to explain under pressure.

Composite keys: a Map compares objects by reference

A Map or Set treats two objects as the same key only if they are the same object. Arrays are objects, so a pair of coordinates stored as an array is never found again:

const visited = new Set();
visited.add([1, 2]);
visited.has([1, 2]);  // false: a different array that merely looks equal

The fix is to turn the composite value into a primitive key that is equal exactly when the parts are equal. There are three options:

  • A string, such as x + "," + y. Easy, and works for any number of parts. The separator is required: "1" + "11" and "11" + "1" are the same string without it. Building the string costs O(length), and hashing it costs the same.
  • A number, such as row * cols + col, when both parts have known bounds. It is the fastest, and it needs 0 <= col < cols to be one-to-one.
  • Nested maps (a Map from x to a Map or Set of y). No string building, but more code.
function distinctVisited(moves) {
  let x = 0;
  let y = 0;
  const seen = new Set(["0,0"]);
  for (const move of moves) {
    if (move === "U") y++;
    else if (move === "D") y--;
    else if (move === "L") x--;
    else x++;
    seen.add(x + "," + y);
  }
  return seen.size;
}

distinctVisited("UDUD") visits (0,0), (0,1), (0,0), (0,1), (0,0), so the set holds 2 keys and the function returns 2. Negative coordinates are fine with a separator: the keys "-1,0" and "1,0" are different strings. JSON.stringify([x, y]) also works as a key but is slower, and is only safe for plain arrays and objects built in a consistent order.

⚠ The silent miss A Set of arrays never throws and never warns. It just answers "not found", so a visited-set BFS stores every state again and again and runs forever or blows up in memory. If a hashing solution is mysteriously slow or never terminates, check first whether the keys are objects.

Complement lookups: find each value's partner

Two Sum is one case of a wider pattern: for each item, compute the partner that would complete the answer, then ask the map whether it exists. Two variations show how far it stretches.

4Sum II. Four arrays a, b, c, d of length n each. Count the tuples (i, j, k, l) with a[i] + b[j] + c[k] + d[l] === 0. Four nested loops cost O(n⁴). Split the problem in two halves: the sums of an a-element and a b-element on one side, and of a c-element and a d-element on the other. The tuple works exactly when the left sum is the negative of the right sum. Store the left sums with their counts, then look up the negative of each right sum.

function fourSumCount(a, b, c, d) {
  const sums = new Map();
  for (const x of a) {
    for (const y of b) {
      const sum = x + y;
      sums.set(sum, (sums.get(sum) || 0) + 1);
    }
  }
  let count = 0;
  for (const x of c) {
    for (const y of d) {
      count += sums.get(-(x + y)) || 0;
    }
  }
  return count;
}

Time O(n²) and space O(n²) for the map of pair sums. Take a = [1, 2], b = [-2, -1], c = [-1, 2] and d = [0, 2]. The sums of an a-element and a b-element are -1, 0, 0 and 1, stored as {-1: 1, 0: 2, 1: 1}. The sums of a c-element and a d-element are -1, 1, 2 and 4. Their negatives are 1, -1, -2 and -4: the lookup of 1 finds one pair and the lookup of -1 finds one, while -2 and -4 find nothing, so the count is 2. A detail to trust: when x + y is 0, -(x + y) is -0, and a Map treats 0 and -0 as the same key, so the lookup still finds a stored 0.

Contains duplicate II. Is there a value that appears twice with the two positions at most k apart? For each element, the only earlier position that matters is the nearest one, the most recent time the same value appeared. A map from value to last index holds exactly that.

function containsNearbyDuplicate(nums, k) {
  const lastIndex = new Map();
  for (let i = 0; i < nums.length; i++) {
    if (lastIndex.has(nums[i]) && i - lastIndex.get(nums[i]) <= k) {
      return true;
    }
    lastIndex.set(nums[i], i);
  }
  return false;
}

containsNearbyDuplicate([1, 0, 1, 1], 1) reaches the third element, finds last index 0 and distance 2 (too far), then overwrites the stored index with 2, and at the fourth element finds distance 1 and returns true. Skipping that overwrite, keeping the first index, is the usual bug: it would compare against 0 and miss the pair. The same question can be answered with a window: a Set of the last k values, adding nums[i] and removing nums[i − k], which uses O(k) space.

Counting subarrays whose sum is a target is the complement idea applied to running totals (the partner of a prefix sum is the prefix sum minus the target). That is the main technique of Prefix sums.

How a hash map works inside: buckets, collisions and resizing

The opening section said a key picks a bucket. Here is the rest of the story, using the textbook design. Real engines vary in the details, but the costs below are why "average O(1)" is the honest claim.

  1. Hash. A hash function turns the key into a large integer. Equal keys must give equal hashes; different keys should spread out.
  2. Bucket. Reduce the hash to a position: hash % capacity. The table is an array of capacity buckets.
  3. Collision. Two different keys can land in the same bucket. In chaining, each bucket holds a small list of its entries; a lookup goes to the bucket and walks that list, comparing keys. (The other common design, open addressing, probes neighbouring slots instead.)
  4. Resize. Let the load factor be size ÷ capacity. When it passes a threshold (0.75 is a common choice), allocate a table with twice the capacity and re-insert every entry, because the bucket depends on the capacity. This costs O(n) but happens rarely.

With a good hash and a bounded load factor, the lists stay short (the average length is the load factor, below 1), so lookup, insert and delete take O(1) on average. The resize is the same doubling argument as the array's push: expensive occasionally, amortized O(1) per insert (see Complexity analysis). The worst case is every key in one bucket: O(n) per operation. A bad hash function, or keys chosen by an adversary who knows the hash, can cause it, which is why production hash tables seed or randomize their hashing.

What counts as "the same key" is a language decision, and JavaScript has specific rules:

QuestionMap / SetPlain object
Is NaN found again?yes: new Set([NaN]).has(NaN) is truethe key becomes the string "NaN"
Are 0 and -0 the same key?yes, they are the same keyyes, both become "0"
Are 1 and "1" the same key?no, two different keysyes, both become "1"
Are two equal-looking arrays the same key?no, only the same object isboth become the same string, such as "1,2"

Map and Set compare keys with a rule called SameValueZero. It differs from === in exactly one way that matters: NaN equals NaN. That is also why [NaN].includes(NaN) is true while [NaN].indexOf(NaN) is -1: includes uses SameValueZero and indexOf uses ===. A Map also remembers insertion order, and re-inserting an existing key keeps its place, but deleting a key and adding it again moves it to the end.

Build a HashMap from scratch

Interviewers like "implement a hash map" because it checks that you know what the library hides. The design below uses string keys, chaining, a polynomial string hash, and doubling at load factor 0.75.

function hashString(s) {
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = (h * 31 + s.charCodeAt(i)) >>> 0;
  }
  return h;
}

class ChainedMap {
  constructor() {
    this.capacity = 8;
    this.size = 0;
    this.buckets = Array.from({ length: this.capacity }, () => []);
  }

  indexFor(key, capacity) {
    return hashString(key) % capacity;
  }

  get(key) {
    const bucket = this.buckets[this.indexFor(key, this.capacity)];
    for (const entry of bucket) {
      if (entry[0] === key) return entry[1];
    }
    return undefined;
  }

  has(key) {
    const bucket = this.buckets[this.indexFor(key, this.capacity)];
    return bucket.some((entry) => entry[0] === key);
  }

  put(key, value) {
    const bucket = this.buckets[this.indexFor(key, this.capacity)];
    for (const entry of bucket) {
      if (entry[0] === key) {
        entry[1] = value;
        return;
      }
    }
    bucket.push([key, value]);
    this.size++;
    if (this.size > this.capacity * 0.75) this.grow();
  }

  remove(key) {
    const bucket = this.buckets[this.indexFor(key, this.capacity)];
    for (let i = 0; i < bucket.length; i++) {
      if (bucket[i][0] === key) {
        bucket.splice(i, 1);
        this.size--;
        return true;
      }
    }
    return false;
  }

  grow() {
    const bigger = this.capacity * 2;
    const next = Array.from({ length: bigger }, () => []);
    for (const bucket of this.buckets) {
      for (const entry of bucket) {
        next[this.indexFor(entry[0], bigger)].push(entry);
      }
    }
    this.buckets = next;
    this.capacity = bigger;
  }
}

The hash multiplies the running value by 31 and adds the next character code, the same polynomial idea as the string hashes of the string algorithms chapter. The >>> 0 keeps the value an unsigned 32-bit integer, and the product stays far below 253, so the arithmetic is exact. For "cat" the hash is 98,262, which is bucket 6 of 8; for "act" it is bucket 2; and for "tac" it is bucket 6 again.

Dry run: three puts, then a collision and a lookup

callbucketbuckets afterwards (non-empty)size
put("cat", 1)66: [cat]1
put("act", 2)22: [act], 6: [cat]2
put("tac", 3)62: [act], 6: [cat, tac]3
get("tac")6walks cat (no match), then tac: returns 33
put("cat", 9)6cat found, value replaced, nothing appended3

The two clashing keys share bucket 6, and the get has to compare keys to tell them apart: the bucket only narrows the search. The resizes happen at the 7th insertion (size 7 is more than 0.75 × 8 = 6, so capacity goes 8 → 16) and the 13th (13 is more than 12, so 16 → 32). With 1,000 keys such as "user0" through "user999" the table ends at capacity 2,048 with a longest chain of 3.

⚠ Where hand-written hash maps go wrong Copying the buckets on resize. Each entry's bucket depends on the capacity, so it must be re-hashed into the new table. Moving the old lists across unchanged puts entries where lookups will not find them. Appending on a repeated key. put must update an existing entry, or the size over-counts and the old value is still returned. Forgetting to shrink size on remove, which makes the load factor drift upward. Using the raw hash as an index: it can be larger than the table, or negative if you let the arithmetic go signed.

When hashing is the wrong tool

  • You need order — a hash map doesn't sort. If the question wants sorted output or range queries, you likely want a sorted structure (or a heap) instead.
  • You need the closest match, not an exact one — hashing only answers "is this exact key present." Nearest-value questions want binary search on a sorted structure.
  • Memory is the actual constraint — if the problem explicitly asks for O(1) space, a hash map is disqualified by definition.
Before you move on
  • Explain why a hash map lookup is O(1) average case, in terms of what the hash function actually does.
  • Trace twoSumFast on a new array by hand, tracking exactly what's in seen at each step.
  • Write a frequency counter and an anagram check from memory, using the same "count then compare" shape.
  • Say out loud why "average case, not worst case" is the correct caveat on hash map complexity.
  • Given a new problem, recognize when hashing is the wrong tool — because it needs order or a closest match, not an exact key.
  • Solve longest consecutive sequence by starting a walk only where a run begins, and explain why looping over the array instead of the Set can turn it quadratic.
  • Pick between sorting, a size-k heap and frequency buckets for top-k, and say what each costs.
  • Design a canonical key for anagrams or shifted strings, and say why the key needs a separator; use two maps when the mapping must be one-to-one.
  • Explain why new Set([[1, 2]]).has([1, 2]) is false, and turn a coordinate pair into a key that works.
  • Split a four-way sum into two pair-sum maps, and keep the nearest index when a window limit matters.
  • Describe buckets, collisions, load factor and resizing, and write a chained hash map whose resize re-hashes every entry.

20Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsTwo Sumbeginner
  • 8 testsTop K Frequent Elementsintermediate
  • 8 testsLongest Consecutive Sequenceintermediate
  • 8 testsHappy Numberbeginner
  • 8 testsRansom Notebeginner
  • 8 testsIntersection of Two Arraysbeginner
  • 8 testsIntersection of Two Arrays IIbeginner
  • 8 testsFirst Unique Character in a Stringbeginner
  • 8 testsWord Frequency Top-Kintermediate
  • 8 testsValid Anagrambeginner
  • 8 testsGroup Anagramsintermediate
  • 8 testsIsomorphic Stringsbeginner
  • 8 testsWord Patternbeginner
  • 8 testsContains Duplicate IIbeginner
  • 8 tests4Sum IIintermediate
  • 7 testsGroup Shifted Stringsintermediate
  • 9 testsTop K Frequent Wordsintermediate
  • 8 testsNumber of Boomerangsintermediate
  • 9 testsBuild a Hash Map with Chainingintermediate
  • 8 testsSubarray Sum Equals Kintermediate
All 217 for this level →

✓Check yourself

5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.

B5

Finished reading?

0 of 48 chapters read in this series

Mark as read

→Next stop

  1. B4Previous · B4Prefix sums and difference arrays
  2. B5you are here
  3. Next · B6Two pointersOne pass, two positions — how an O(n²) search collapses to O(n).29 min read

01On this page

14 sections · 0%

  1. 01What a hash map actually does
  2. 02Map/Set vs plain objects
  3. 03The pattern: trade space for time
  4. 04Frequency counting — the other 80% of hashing questions
  5. 05Grouping — building a Map of arrays
  6. 06Set membership: asking "is the neighbour here?"
  7. 07Top-k frequent: count, then bucket by frequency
  8. 08Canonical keys: make things that are equal look equal
  9. 09Bijections: two maps for isomorphic strings and word patterns
  10. 10Composite keys: a Map compares objects by reference
  11. 11Complement lookups: find each value's partner
  12. 12How a hash map works inside: buckets, collisions and resizing
  13. 13Build a HashMap from scratch
  14. 14When hashing is the wrong tool
Mark as read

[] chapters · t top