Module F-4·18 min read

How hash tables resolve collisions, Map vs Object, and the lookup patterns behind caching, deduplication, and memoization.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module F-4 — Hash Maps: Hashing, Collisions, and O(1) Lookups

What this module covers: Three modules in a row have hit the same wall — a loop inside a loop, checking every element against every other element, O(n²). This module is the general-purpose fix. Hash maps trade a bit of memory for the ability to ask "have I seen this before?" in O(1), and that single trick is behind more real-world performance fixes — and more of your production stack — than almost any other structure in this course.


The Problem: Checking Membership Shouldn't Cost O(n)

Every version of the O(n²) bug you've seen so far — .includes() inside a loop (Module F-1), the naive rotation check (Module F-3) — has the same shape: "is this value already somewhere in this collection?" On an array, answering that question means scanning, because arrays only know how to find things by position, not by value.

A hash map answers a different question: instead of "where in memory is this array?", it computes "which bucket should this specific key live in?" — directly, without looking at anything else first.

typescript

Whether seen holds 10 entries or 10 million, .has() costs roughly the same. That's the entire value proposition of a hash map, and it's why "swap the array for a Map/Set" is often the single change that turns a timing-out endpoint into an instant one.


How Hashing Actually Works

Underneath, a hash map is still just an array — the same contiguous, O(1)-indexed structure from Module F-2. The trick is a hash function, which takes a key of any type and any size and deterministically converts it into a number (a hash code), which is then reduced (typically with modulo) into a valid index into that backing array.

index = hash(key) % array.length
typescript

Given the same key, simpleHash always returns the same index — that determinism is what makes .get(key) work: compute the hash once, jump straight to that bucket, and the value is there (or it isn't). No scanning the whole structure — you go directly to where the answer has to be, if it exists at all.

Analogy: A hash map is a library that shelves books by the first letter of the author's last name, computed the instant the book arrives — not alphabetically re-sorted on every new arrival. Looking up "an author starting with M" means walking straight to the M shelf, not scanning every shelf in the building.


Collisions: When Two Keys Hash to the Same Bucket

Different keys can, and eventually will, hash to the same bucket index — this is called a collision, and it's unavoidable once you have more possible keys than buckets (the pigeonhole principle guarantees it eventually). How a hash map handles this determines whether it stays fast or quietly degrades.

Chaining (the more common approach): each bucket holds a small list of entries instead of a single one. A collision just means two entries share a bucket, checked with a short linear scan within that one bucket — not the whole table.

bucket[7] → [("user_42", {...}), ("user_58", {...})] // both hashed to index 7

Open addressing (the alternative): on a collision, the map probes forward to the next open slot according to some rule, rather than growing a list at the colliding bucket.

Both approaches degrade toward O(n) in the worst case — if every key happened to hash to the same bucket, you'd be scanning a list as long as the whole map, no better than an array. In practice, a well-designed hash function spreads keys evenly enough that this worst case is rare, which is why hash maps are described as O(1) average case, not O(1) worst case — a distinction Module F-1 covered directly, and one that matters here more than almost anywhere else in this course.


Load Factor and Resizing

Load factor is the ratio of stored entries to bucket count. As it climbs, collisions become more frequent and lookups slow down. Hash maps manage this the same way dynamic arrays manage running out of space (Module F-2): when the load factor crosses a threshold (commonly around 0.75), the map rehashes — allocates a larger backing array and re-inserts every existing entry, because the same key can land in a different bucket once the bucket count changes.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.