InterviewKata
Data structure 3 of 12

Hash maps

A hash map stores pairs: a key and its value. Give it the key and it goes straight to the value, without searching. That one trick removes the slow inner loop from a huge number of problems.

A 3-minute conversation: Sam asks, Priya explains
0:00 / 2:41

1. What it is

A post room with 100 pigeonholes. A simple rule turns any name into a hole number (say, add up its letters and keep the last two digits). To deliver a letter, apply the rule and drop it in. To collect, apply the same rule and look in one hole. Nobody walks along all 100.

A hash map (Python dict, TypeScript Map, Java HashMap) maps each key to a value: a name to a phone number, a word to how many times it appears, a number to the index where you saw it. Keys are unique; setting an existing key replaces its value.

2. How it sits in memory

Inside, a hash map is an array of buckets. A hash function turns the key into a number, and that number modulo the array's size picks the bucket. Two keys can land in the same bucket (a collision); the bucket then holds a short list, and the map checks each entry's key.

As long as buckets stay short, every operation touches one bucket: O(1) on average. When the map gets too full it doubles its buckets and re-files everything, just like a growing list. In the rare worst case, when every key collides, one bucket holds everything and operations become O(n).

3. Operations and what they cost

OperationPythonCost
Set a valuem[k] = vO(1) average
Read a valuem[k] (error if missing), m.get(k, default)O(1) average
Is the key there?k in mO(1) average
Deletedel m[k], m.pop(k)O(1) average
Visit every pairfor k, v in m.items()O(n)
Find the key with the largest valuemax(m, key=m.get)O(n): a map doesn't keep values sorted
When to reach for oneWhenever you catch yourself searching a list for something you saw earlier, store it in a map the first time instead.

4. In Python, TypeScript and Java

The same three moves (set, read with a default, count) in each language.

ages = {"ana": 31, "ben": 27}
    ages["cy"] = 40                  # add
    ages["ben"] = 28                 # replace
    print(ages.get("dee", 0))        # 0: a default instead of an error
    print("ana" in ages)             # True
    for name, age in ages.items():   # every pair
        print(name, age)

    from collections import Counter, defaultdict
    counts = Counter("banana")       # {'b': 1, 'a': 3, 'n': 2}
    groups = defaultdict(list)       # a missing key starts as []
    groups["a"].append("apple")

5. Core operations, step by step

First a tiny hash map built by hand, so the "magic" disappears; then the two jobs you will use one for most often.

A · A hash map by hand (buckets and collisions)

What it doesStores each pair in the bucket chosen by the key's hash; reads by checking only that bucket.
Picture itPigeonholes: the rule picks the hole, and a hole can hold a few letters.
CostO(1) average per put or get; O(n) if every key collides.

The code. Eight buckets, each a list of [key, value] pairs. The language's own map does the same with a far better hash.

class TinyMap:
    def __init__(self):
        self.buckets = [[] for _ in range(8)]

    def put(self, key, value):
        b = self.buckets[hash(key) % 8]
        for pair in b:
            if pair[0] == key:
                pair[1] = value
                return
        b.append([key, value])

    def get(self, key):
        b = self.buckets[hash(key) % 8]
        for k, v in b:
            if k == key:
                return v
        return None

Line by line.

LineWhat it doesWhy
b = self.buckets[hash(key) % 8]The rule picks one bucket.Integers hash to themselves in Python, so key 11 goes to bucket 3, the same as key 3: a collision.
if pair[0] == keyIs this pair our key?A bucket can hold several keys; compare each one.
pair[1] = valueSame key: replace.Keys are unique; setting again overwrites.
b.append([key, value])New key: add to the bucket.Buckets stay short when the hash spreads keys well.

Watch it run. Put 3 → cat, 11 → dog, 5 → emu and 3 → cow, then get 11, 3 and 7.

Watch out for:

  • Thinking a map is always O(1). It is O(1) on average because buckets stay short. A terrible hash (or an attacker) can make one bucket hold everything.

B · Count things

What it doesMaps each item to how many times it has appeared.
Picture itA tally sheet: find the item's line, add one mark; a new item gets a new line.
CostO(n) time, O(k) space for k different items.

The code. Read the old count with a default of 0, add one, store it back.

def count_words(words):
    counts = {}
    for w in words:
        counts[w] = counts.get(w, 0) + 1
    return counts

Line by line.

LineWhat it doesWhy
counts.get(w, 0) + 1The old count, or 0 if this is the first time, plus one.counts[w] += 1 raises KeyError the first time a word appears.
counts[w] = ...Store the new count.O(1) average.

Watch it run. Count ["red", "blue", "red", "green", "red"].

Watch out for:

  • counts[w] += 1 on a plain dict. Fails on the first occurrence. Use get with a default, Counter or defaultdict(int).

C · Group things by a key

What it doesMaps each key to a list of the items that share it.
Picture itSorting post into pigeonholes by surname initial: every letter for an 'M' goes in the M hole.
CostO(n) time and space.

The code. The key decides the group; the value is a list that grows.

def group_by_first_letter(words):
    groups = {}
    for w in words:
        key = w[0]
        if key not in groups:
            groups[key] = []
        groups[key].append(w)
    return groups

Line by line.

LineWhat it doesWhy
key = w[0]Decide which group the word belongs to.Change this one line to group by length, by sorted letters (anagrams), and so on.
if key not in groups: groups[key] = []First item of a new group: make its list.defaultdict(list) does this for you.
groups[key].append(w)Add the word to its group.

Watch it run. Group ["apple", "bean", "avocado", "beet", "corn"] by first letter.

Watch out for:

  • Using a list as the key. Lists aren't hashable. Convert to a tuple or a string first (e.g. the sorted letters joined).

6. How to remember it

Memory hook: "The post room"A rule turns any key into a pigeonhole number. Deliver and collect by the rule, never by searching. Two names can share a hole; the clerk checks the name on each envelope.

Picture

A wall of pigeonholes, a few holding two or three envelopes.

Chant

"Key in, value out, no searching."

Key rule

Seen it before? Store it the first time; look it up the second.

7. Problems solved with it

Three easy problems where a map turns a slow search into a lookup.

LC 1512 · Number of Good Pairs

The problemA pair (i, j) is good if nums[i] == nums[j] and i < j. Return the number of good pairs.
What it doesFor each number, every earlier copy of it makes a new good pair; a map remembers how many copies came before.
CostO(n) time instead of O(n²) for checking every pair.

The code. Add the count so far, then increase it.

def num_identical_pairs(nums):
    seen = {}
    pairs = 0
    for x in nums:
        pairs += seen.get(x, 0)
        seen[x] = seen.get(x, 0) + 1
    return pairs

Line by line.

LineWhat it doesWhy
pairs += seen.get(x, 0)Each earlier copy of x pairs with this one.Read the count before adding x itself, or x would pair with itself.
seen[x] = seen.get(x, 0) + 1Now count this copy.

Watch it run. [1, 2, 3, 1, 1, 3].

Watch out for:

  • Two nested loops. Correct but O(n²). The map version counts all pairs in one pass.

LC 1394 · Find Lucky Integer in an Array

The problemA lucky integer appears exactly as many times as its value. Return the largest lucky integer, or −1.
What it doesCounts every number, then checks each (value, count) pair.
CostO(n).

The code. Count first; decide after.

def find_lucky(arr):
    counts = {}
    for x in arr:
        counts[x] = counts.get(x, 0) + 1
    best = -1
    for value, times in counts.items():
        if value == times:
            best = max(best, value)
    return best

Line by line.

LineWhat it doesWhy
for value, times in counts.items()Look at every distinct number once.Iterating the map, not the array, checks each value once.
if value == timesIs it lucky?
best = max(best, value)Keep the largest lucky one.Map order isn't sorted, so compare.

Watch it run. [1, 2, 2, 3, 3, 3].

Watch out for:

  • Returning the first lucky number found. Maps don't iterate in sorted order. Keep the maximum.

LC 2053 · Kth Distinct String in an Array

The problemA string is distinct if it appears exactly once. Return the k-th distinct string in the original order, or "".
What it doesCounts every string, then walks the array in order counting distinct ones.
CostO(n).

The code. The map answers "how many?"; the original array keeps the order.

def kth_distinct(arr, k):
    counts = {}
    for s in arr:
        counts[s] = counts.get(s, 0) + 1
    for s in arr:
        if counts[s] == 1:
            k -= 1
            if k == 0:
                return s
    return ""

Line by line.

LineWhat it doesWhy
for s in arr: counts[s] = ...First pass: count.You can't call a string distinct until you've seen them all.
for s in arrSecond pass in the original order.Iterating the map would lose the order the problem asks for.
k -= 1; if k == 0Count down the distinct strings.

Watch it run. ["d", "b", "c", "b", "c", "a"], k = 2.

Watch out for:

  • Walking the map instead of the array. The k-th must follow the original order.

More practice, solved on the pattern pages

#ProblemLevelWhere
1Two SumEasySolved step by step on 16 · Hash Map & Set
383Ransom NoteEasySolved step by step on 16 · Hash Map & Set
242Valid AnagramEasySolved step by step on 16 · Hash Map & Set
169Majority ElementEasySolved step by step on 16 · Hash Map & Set
205Isomorphic StringsEasySolved step by step on 16 · Hash Map & Set
49Group AnagramsMediumSolved step by step on 16 · Hash Map & Set

8. Common mistakes

Reading a missing keym[k] raises KeyError in Python (TypeScript returns undefined, Java null). Use get(k, default), getOrDefault or ??.
Mutable keysA list can't be a dict key. Use a tuple or a string. In Java, don't change an object after using it as a key.
Relying on orderPython dicts keep insertion order, Java's HashMap doesn't. Neither keeps keys sorted: sort them yourself when order matters.
Changing a map while looping over itAdding or deleting keys inside for k in m raises an error. Loop over list(m) or collect changes first.
Comparing Integer values with == in JavaBoxed integers above 127 are different objects. Use .equals or unbox to int.

9. Self-check quiz

Looking up a key in a hash map with a million entries costs…

The hash picks one bucket, and buckets stay short.

Two keys hash to the same bucket. What happens?

That's a collision. The bucket holds both; lookups compare keys.

Which safely counts word w in a plain Python dict?

The first time a word appears, counts[w] doesn't exist yet.

You want to use a pair of numbers as a key. Which works?

Keys must be hashable, which means unchangeable. Tuples are; lists and sets aren't.

10. Where it shows up next

Next structure: 04 · Hash sets, a hash map with keys and no values.