LearnAbout

Hashing

On this page
Quick reference
Insert / lookup / delete (average case)O(1)
Insert / lookup / delete (worst case)O(n)
The Idea

Recall from Arrays & Strings Fundamentals that array indexing is O(1) because the address is computed directly from the index — base + index × size, pure arithmetic, no searching. Hashing extends that same trick to keys that aren't already small, tidy integers. A hash function takes any key — a word, a tuple, a whole object — and squashes it down to a number, which is then used as a plain array index. Look up "apple" in a hash table and, under the hood, "apple" gets converted to a number, and that number tells you exactly which slot in a backing array to check — no scanning required.

Picture a library that shelves books by a formula computed straight from the title, instead of alphabetically. Given any title, you compute the formula and walk directly to the shelf — no browsing required. That's the whole idea: trade some memory (the backing array has to have room for the formula's possible outputs) for near-instant lookup.

Hashing turns "have I seen this value before?" from an O(n) scan into a ~O(1) lookup — the single most common upgrade in this phase, and the reason so many problems from earlier topics get revisited here with a better time complexity.
Build It
Structures
structure HashMap:
    buckets      // a plain array (Arrays & Strings Fundamentals) — one slot per possible hash value
    size         // how many key/value pairs are currently stored

function hash(key):
    return someFormula(key) % buckets.length   // squashes any key down to a valid array index
collisions — when two keys land in the same bucket
// two different keys can hash to the same bucket index — a collision.
// one common fix: chaining — each bucket holds a small list instead of one slot
function insert(map, key, value):
    bucketIndex = hash(key)
    if map.buckets[bucketIndex] is empty:
        map.buckets[bucketIndex] = new list
    map.buckets[bucketIndex].add((key, value))   // append to the chain, don't overwrite it
A good hash function spreads keys evenly across buckets, keeping each chain short — usually just one entry. A bad one (or an adversarial input) can pile many keys into the same bucket, turning that bucket's lookup into a walk through a list.
frequency count — hash map
function countFrequencies(items):
    counts = new HashMap
    for item in items:
        counts[item] = counts.get(item, default=0) + 1
    return counts
membership check — hash set
function hasDuplicate(items):
    seen = new HashSet
    for item in items:
        if item in seen:          // O(1) average — a hashed lookup, not a scan
            return true
        seen.add(item)
    return false
Know It

The "average" qualifier matters here more than almost anywhere else — recall the average-vs-worst-case distinction from Big-O Notation. A hash table's speed depends on the hash function spreading keys out well; when that assumption holds (the normal case), lookups are effectively instant.

OperationTimeSpaceWhy
Insert / lookup / delete (average case)O(1)O(1) per operation (O(n) total for n entries)A good hash function spreads keys evenly, so each bucket holds close to zero or one entries — checking a bucket is essentially constant work.
Insert / lookup / delete (worst case)O(n)O(1) per operationIf every key collides into the same bucket (a poor hash function, or adversarial input crafted to collide), that bucket's chain grows to length n and searching it is a full scan — rare in practice, but real.
Break It

Using a mutable value as a key

a key's contents change after it's already been inserted

A key's hash is computed from its contents at insert time. If the key is a mutable object and its contents change afterward, its hash would be different if recomputed — but the table doesn't recompute it, so the entry is now sitting in the wrong bucket for what the key currently looks like, and future lookups for it silently fail. Keys should be values that don't change once stored.

Assuming iteration order matches insertion order

code loops over a hash map's entries expecting a particular order

Some languages' hash map implementations happen to preserve insertion order (or expose a variant that does); others make no such guarantee and can return entries in an order that looks arbitrary. Code that depends on order without checking the specific language's guarantee works by accident in one language and breaks in another.

Reaching for hashing when order actually matters

the problem needs sorted order, not just fast membership

Hashing gives fast "is this here / what's paired with this," not order — a hash set or map doesn't keep keys sorted, and iterating one doesn't produce anything meaningful about relative order. Problems that need "the smallest," "in sorted order," or "the next largest" need a different tool even if fast lookup is also part of the problem.

Collisions silently degrading performance

many keys happen to hash to the same or nearby buckets

Code that assumes every hash map operation is O(1) without exception can be quietly O(n) per operation on data that collides heavily — an attacker who knows your hash function can sometimes construct exactly such input on purpose. "Average case O(1)" is a property of typical data, not a guarantee for all data.

Use It
Two Sum
The real tool behind the O(n) approach hinted at back in Big-O Notation: a hash map storing "value seen → index" turns "has the complement of this number already appeared?" into a single O(1) lookup instead of a nested scan.
Easy
Contains Duplicate
A hash set gives O(1) average membership checks — drop each element in as you go, and the moment an element is already present, you're done in one pass.
Easy
Valid Anagram
A hash map of character → count, built up from one string and torn down by the other (or a fixed-size array, if you know it's just lowercase letters, as in Sliding Window's frequency tables). Any mismatched or leftover count means not an anagram.
Easy
Group Anagrams
Two anagrams always produce the same value when their characters are sorted (or counted) — use that canonical form as a hash map key, and bucket every original string under it.
Medium
Top K Frequent Elements
A hash map handles the counting pass in O(n). To read off the top k without a full sort, bucket the counted values by their frequency (an array indexed by count, from 0 up to n) and walk that bucket array from the high-frequency end.
Medium
Longest Consecutive Sequence
Drop every number into a hash set for O(1) membership checks, then only start counting a streak from numbers that are the start of one (no num - 1 in the set) — that skips re-walking the same streak from the middle over and over, keeping the whole scan O(n).
Medium
Ransom Note
A hash map (or fixed-size array, if it's just lowercase letters) counting the magazine's letters, then subtracted as each letter the ransom note needs gets "spent" — if any count goes negative, it's not possible.
Easy
Isomorphic Strings
Two hash maps: one mapping characters from the first string to the second, one mapping back. A character can only ever map to (and be mapped from) exactly one partner, checked in a single pass.
Easy
Word Pattern
The same two-way mapping idea as Isomorphic Strings, just between pattern letters and whole words instead of individual characters.
Easy
Subarray Sum Equals K
Exactly the Prefix Sums approach, now with its missing piece named: the running-sum lookup you were tracking is a hash map from running-sum value to how many times it's occurred — that's what makes each check O(1) instead of rescanning.
Medium