Currently available · INDIA
← Writing
Algorithms · 4/7·Jul 5, 2026·14 min read

Hashing — Complete Guide with Interactive Walkthroughs

Master hash maps and sets for O(1) lookups — existence checks, frequency counting, prefix + frequency patterns, and grouping. The most important data structure in interview prep. Animations included.

0

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.

LanguageHash MapHash Set
JavaScriptMap / ObjectSet
TypeScriptMapSet
Gomap[K]Vmap[K]struct{}
Pythondictset

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.

javascript
key  →  hash(key) % tableSize  →  bucket index

Combined 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:

PatternStructureWhen to use
Existence / lookupSet or Mapif x in arr in a loop → O(n²) without hashing
Frequency countingMap key → countTrack how often elements appear in window or array
Complement lookupMap value → indexTwo Sum: search for target − num in O(1)
Prefix + frequency mapMap prefixSum → countCount subarrays with sum exactly K
Grouping by signatureMap canonicalKey → group[]Anagrams, digit sums, row/column signatures

Hash Map vs Array

OperationHash Map / SetArray (unsorted)
Check if element existsO(1) avgO(n)
Add with lookupO(1) avgO(1) append, O(n) search
Two Sum on n elementsO(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 / setArray
Existence checkO(1) averageO(n) scan
Key typeAlmost any immutable keyInteger index only
OrderUnordered (usually)Ordered by index
Space tradeoffOverhead; may waste slotsCompact for dense indices
Small inputsConstant factors can dominateOften 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

LeetCode 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

2
0
7
1
11
2
15
3

map

(empty)

lookup 7 not found

Step 1/3target = 9

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

LeetCode 2351

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

a
0
b
1
c
2
c
3
b
4
a
5

set

(empty)

Step 1/4

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

e
0
c
1
e
2
b
3
a
4

map

e1
Step 1/5k = 2

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); // 3

Complexity: O(n) time, O(k) space for the map.

Example 5 — Intersection of Multiple Arrays

LeetCode 2248

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

LeetCode 1941

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");  // false

Pattern 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.

javascript
ans += counts[curr - k]
counts[curr]++

Always initialize counts[0] = 1 (empty prefix).

Example 7 — Subarray Sum Equals K

LeetCode 560

Subarray Sum Equals K

nums = [1, 2, 1, 2, 1], k = 3 — prefix + frequency map

nums

1
0
2
1
1
2
2
3
1
4

map

01
Step 1/5k = 3, ans = 0

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); // 4

Why 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)

LeetCode 1248

Count subarrays with exactly k odd numbers. Same pattern — but curr tracks odd count instead of sum:

javascript
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:

ProblemSignature key
Group AnagramsSorted string ("eat""aet")
Digit sum pairsSum of digits
Row/column pairsTuple or string of row values

Example 9 — Group Anagrams

LeetCode 49

Group Anagrams — Key by Sorted String

strs = ["eat","tea","tan","ate","nat","bat"]

input

eat
0
tea
1
tan
2
ate
3
nat
4
bat
5

map

aet["eat"]
Step 1/4

"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

LeetCode 2260

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]); // 3

Example 11 — Equal Row and Column Pairs

LeetCode 2352

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

OperationHash MapHash SetArray scan
Add elementO(1)*O(1)*O(1) append
Check existenceO(1)*O(1)*O(n)
Delete elementO(1)*O(1)*O(n)
Two Sum (n elements)O(n)O(n)†O(n²)
Subarray sum = KO(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)

Quick Reference — Pattern Picker

Pattern Picker

Click a node to trace the path — cards below update with your selection

yes/no onlyreturn indiceshow many times?cluster itemsshortest distancein a windowsubarray sum = KNeed fast lookup?Check existenceNeed index orvalue?Count frequenciesGroup by signatureTrack last indexWindow constraintExact subarraymetric

Key Takeaways

  1. Hash map = O(1) lookup — the single biggest time-complexity upgrade in interviews.
  2. if (x in arr) in a loop → use a set or map.
  3. Count frequencies with map[key]++ — windows, intersections, anagrams.
  4. Two Sum pattern: search for target - num, not pairs of loops.
  5. Prefix + freq map: counts[curr - k] counts exact subarray constraints.
  6. Group by signature: sorted string, digit sum, or tuple as map key.
  7. Sets don't track frequency — adding the same key 100 times still yields size 1.
  8. 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

Related reading

/ Stay in the loop

Get the next one in
your inbox.

New essays and field notes sent only when there's something worth sending. No tracking, no spam, easy unsubscribe.

Join · No spam · One-click unsubscribe