Skip to content

Beginner: foundations · Hashing, two pointers and sliding window · topic 11 of 61 · 35 min

Hash maps and hash sets

Explanation

A hash map stores key → value pairs and a hash set stores keys only. A hash function turns each key into a number that points almost straight at its slot, so insert, lookup and delete are O(1) on average (O(n) in the rare worst case). Python's dict and set and JavaScript's Map and Set are hash tables, and they turn many O(n²) "have I seen this?" solutions into O(n).

Why hashing

Many problems ask the same small question again and again: "have I seen this before?" or "what do I know about this key?". With a list, each check scans the list, O(n), so checking every item costs O(n²). A hash map answers in O(1) on average, which turns many O(n²) solutions into O(n). That one swap is the most common speed-up in coding interviews.

A hash map stores key → value pairs, like roll number → student name. A hash set stores only keys and answers one question: is this key present? Both are built the same way, on a structure called a hash table.

The hash function, in plain words

A hash function turns a key into a whole number, called its hash. It must always give the same number for the same key, and it should spread different keys over many different numbers. The table keeps an array of slots called buckets, and a key goes to bucket hash(key) % number_of_buckets. Finding a key means: compute its hash, jump to that bucket, and look only there.

Think of it like this: A school library has 26 shelves, one for each first letter of a book's title. To find "Panchatantra" you walk straight to shelf P instead of scanning every shelf. "First letter" is a (weak) hash function and the shelves are buckets. Many books starting with S crowd one shelf; that crowding is a collision.
python
class TinyHashMap:
    def __init__(self, size=8):
        self.buckets = [[] for _ in range(size)]

    def _bucket(self, key):
        return self.buckets[hash(key) % len(self.buckets)]

    def put(self, key, value):
        bucket = self._bucket(key)
        for pair in bucket:
            if pair[0] == key:
                pair[1] = value        # key already there: update
                return
        bucket.append([key, value])

    def get(self, key):
        for k, v in self._bucket(key):
            if k == key:
                return v
        return None

m = TinyHashMap()
m.put(3, "three")
m.put(11, "eleven")     # 11 % 8 == 3: same bucket as 3, a collision
print(m.buckets[3])     # Output: [[3, 'three'], [11, 'eleven']]
print(m.get(11))        # Output: eleven
A toy hash map with chaining. In Python, hash() of a small integer is the integer itself, so 3 and 11 both land in bucket 3 of 8.

Equal keys always have equal hashes. String hashes in Python are randomised each time the program starts (a protection against attacks), so never store hash("abc") and expect the same number in the next run.

Collisions: chaining and open addressing

A collision is two different keys landing in the same bucket. It cannot be avoided, because there are far more possible keys than buckets. There are two main fixes:

  • Chaining: each bucket holds a small list of (key, value) pairs, as in the toy above; a lookup scans only that short list. Java's HashMap works this way (and turns very long chains into small trees).
  • Open addressing: each slot holds at most one pair. If the slot is taken, the table probes other slots in a fixed order until it finds the key or an empty slot. Python's dict and set use open addressing.

To keep chains and probe sequences short, the table grows when it gets too full; the ratio of items to slots is called the load factor. Growing means making a bigger array and re-inserting every key, O(n) once, but it happens rarely, so inserting is O(1) amortised, like list.append in Module 1.

Average O(1), worst O(n)

OperationPythonJavaScriptAverageWorst
Insert or updated[k] = vm.set(k, v)O(1)O(n)
Lookupd[k], d.get(k)m.get(k)O(1)O(n)
Membershipk in d, x in sm.has(k), st.has(x)O(1)O(n)
Deletedel d[k], s.discard(x)m.delete(k)O(1)O(n)
Visit every keyfor k in dfor (const [k, v] of m)O(n)O(n)

The worst case happens when many keys collide, for example with a deliberately bad hash function. With built-in types it is very rare, and interviewers expect you to say "O(1) on average". The space cost of a map or set with n keys is O(n).

Which keys are allowed

A key must be hashable: its hash must never change while it sits in the table. Immutable built-ins are hashable: int, float, str, bool, and tuples whose items are all hashable. Lists, dicts and sets can change, so they are not: d[[1, 2]] = "x" raises a TypeError that says unhashable type: 'list'. To use a list or a pair of coordinates as a key, turn it into a tuple.

python
visited = set()
visited.add((2, 3))            # a grid cell (row, col) as a tuple
print((2, 3) in visited)       # Output: True

counts = {}
key = tuple([1, 2])
counts[key] = counts.get(key, 0) + 1
print(counts)                  # Output: {(1, 2): 1}

In JavaScript use Map and Set. A plain object turns every key into a string, so o[1] and o["1"] are the same key. Map keeps keys as they are, but arrays and objects are compared by identity: two separate [1, 2] arrays are two different keys. Use a string such as "1,2" as the key instead.

javascript
const seen = new Map();
seen.set('UPI', 3);
console.log(seen.get('UPI'), seen.has('card'), seen.size);  // 3 false 1

const o = {};
o[1] = 'a';
o['1'] = 'b';
console.log(Object.keys(o));   // [ '1' ]  (one key: 1 became "1")
Map keeps key types; a plain object turns keys into strings.

Two-sum with a map

Given nums and a target, find two different positions whose values add up to the target. Brute force tries every pair, O(n²). With a map from value to index, walk once: for each x the partner you need is target − x. If the partner is already in the map, you are done; otherwise store x with its index. Check BEFORE storing, so an item is never paired with itself. O(n) time and O(n) extra space.

python
def two_sum(nums, target):
    seen = {}                       # value -> index
    for i, x in enumerate(nums):
        need = target - x
        if need in seen:
            return [seen[need], i]
        seen[x] = i
    return []

print(two_sum([2, 7, 11, 15], 9))   # Output: [0, 1]
print(two_sum([3, 3], 6))           # Output: [0, 1]

Common mistakes

  • Using a list for "seen" checks: x in a_list is O(n), so the loop becomes O(n²). Fix: use a set or a dict.
  • Storing x before looking for target − x, so [3] with target 6 pairs the 3 with itself. Fix: check first, then store.
  • Using a list or a set as a key. Fix: convert it to a tuple (or a frozenset when order does not matter).
  • Reading d[key] for a missing key, which raises KeyError. Fix: d.get(key, default), check key in d first, or use collections.defaultdict.
  • Adding or deleting keys while looping over the same dict (RuntimeError: dictionary changed size during iteration). Fix: loop over list(d) or build a new dict.
  • Saying "O(1) always". Fix: say "O(1) on average, O(n) in the worst case".

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Hash maps and hash sets · 1/8

Hash maps and hash sets

This lesson is about hash maps and hash sets, the tools behind most fast interview solutions.

Prefer a video by a teacher? Find free videos on “hash table hash map explained collisions chaining open addressing two sum” on YouTube ↗

In real life: Stopping a double payment

When a payment request arrives twice (a slow network, a double tap on "Pay"), a payments service must not charge the customer twice. A common design gives every request a unique ID, often called an idempotency key, and remembers recently seen IDs in a hash set or a key-value store such as Redis. Before charging, it asks "have I seen this ID?", an O(1) average lookup even with millions of IDs. This is a simplified picture of how such systems typically work.

python
processed = set()

def charge(request_id, amount):
    if request_id in processed:
        return "duplicate ignored"
    processed.add(request_id)
    return f"charged Rs {amount}"

print(charge("TXN1001", 250))
print(charge("TXN1001", 250))
print(charge("TXN1002", 99))
Shows charged Rs 250, then duplicate ignored, then charged Rs 99.

Practice (0/3 done)

Task 1 · Code · Python

Write two_sum(nums, target) that returns a list [i, j] of two DIFFERENT indexes with nums[i] + nums[j] == target and i < j. Exactly one such pair exists. Use a dict so it runs in O(n) time (O(n) extra space). Example: two_sum([2, 7, 11, 15], 9) returns [0, 1].

Task 2 · Code · Python

Write contains_nearby_duplicate(nums, k) that returns True if there are two DIFFERENT indexes i and j with nums[i] == nums[j] and abs(i - j) <= k, else False. O(n) time. Example: contains_nearby_duplicate([1, 2, 3, 1], 3) returns True (indexes 0 and 3).

Task 3 · Code · Python

Write intersection(a, b) that returns the values that appear in BOTH lists, each value once, sorted in increasing order. Use sets: about O(n + m) to find the common values (plus sorting the result). Example: intersection([4, 9, 5], [9, 4, 9, 8, 4]) returns [4, 9].

Quick quiz

Question 1

1.What is the average cost of checking key in d for a Python dict with n keys?

Question 2

2.Which of these can be used as a key in a Python dict?

Question 3

3.In a hash table, what is a collision?

Question 4

4.In two-sum with a map, why look for target − x BEFORE storing x?

Question 5

5.In JavaScript: const o = {}; o[1] = "a"; o["1"] = "b". How many keys does o have?

Question 6

6.When can a hash map lookup become O(n)?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is the average cost of checking key in d for a Python dict with n keys?

Answer: O(1)

Hashing jumps almost straight to the right slot, so the average cost does not grow with n.

Which of these can be used as a key in a Python dict?

Answer: (1, 2)

Tuples of hashable items are hashable. Lists, sets and dicts can change, so they are unhashable.

In a hash table, what is a collision?

Answer: Two different keys that land in the same bucket

More possible keys than buckets means some keys must share a bucket; chaining or open addressing handles it.

In two-sum with a map, why look for target − x BEFORE storing x?

Answer: So an item is never paired with itself, for example nums = [3] with target 6

If 3 were stored first, the lookup for 6 − 3 = 3 would find the same element.

In JavaScript: const o = {}; o[1] = "a"; o["1"] = "b". How many keys does o have?

Answer: 1

Object keys are turned into strings, so 1 and "1" are the same key. A Map would keep them apart.

When can a hash map lookup become O(n)?

Answer: When many keys collide into the same bucket or probe sequence

If most keys share a bucket, a lookup has to check them one by one.

What is the average lookup time of a well-sized hash table?Related

Answer: O(1)

With a good hash and low load factor.

From Hash tables

What is the average cost of key in d for a Python dict d?Related

Answer: O(1)

Dicts use hashing, so membership is O(1) on average.

From The real cost of Python and JavaScript built-ins

What is the worst-case lookup time of std::unordered_map?Related

Answer: O(n)

If many keys collide into one bucket, a lookup checks them one by one.

From std::map and std::unordered_map

Which key puts exactly the anagrams of "listen" into one group AND can be used as a dict key?Related

Answer: "".join(sorted("listen"))

The sorted string is the same for all anagrams and is hashable. A set loses counts and is unhashable, a list is unhashable, and length groups unrelated words.

From Counting and grouping with hashing

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 4 more topics in “Hashing, two pointers and sliding window” before its exam: Counting and grouping with hashing, Two pointers, Sliding window: fixed size, Sliding window: variable size.
  2. Module 3 exam: Hashing, two pointers and sliding window · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Stacks, queues and linked lists. 61 topics left in the course before the final exam.

← Previous: 2-D arrays (matrices)