hashing · study console
Arrays · Strings · Hashmaps & Sets

Pick the right key.
Pay O(1).
Win.

The complete technique — from brute-force motivation to key-design decisions, one-pass vs two-pass tradeoffs, grouping by derived signatures, and why average-case O(1) actually holds.

frequency counting complement lookup grouping by derived key hashset existence collision handling
§ 01 — earn the structure

Derived from brute force, not memorized

The canonical entry point is Two Sum (unsorted): given an array and a target, return indices of two elements that sum to the target. Don't jump to the solution — find the waste in the brute force first.

The O(n²) brute force

For every pair (i, j) with i < j, check if arr[i] + arr[j] == target. This is correct but wasteful: on each outer iteration at index i, you're linearly scanning the rest of the array to find the complement target - arr[i].

O(n²) brute force — Two Sum
for i from 0 to n - 1:
    for j from i + 1 to n - 1:
        if arr[i] + arr[j] == target:
            return [i, j]           # found the pair

# what are we doing on every outer iteration i?
# scanning all j > i to ask: "is (target - arr[i]) anywhere in the rest?"
# that linear scan is the waste. if we already knew the answer in O(1), we'd save it.

Ask: what are we recomputing?

At each step i, we want to know: "Have I already seen the value target - arr[i]?" The inner loop answers this with a linear scan — O(n) work. If instead we had a structure that answers "have I seen X" in O(1), we'd reduce the total work to O(n). That structure is a hashmap: trade memory for the ability to answer membership queries in constant time.

the core hashmap trade

A hashmap stores elements seen so far, keyed so that the question "have I seen X?" costs O(1) instead of O(n). The trade: O(n) extra space buys O(n) total time instead of O(n²). For most interview problems, this trade is obvious — accept it immediately and focus on what the key should be.

O(n²) — inner scan
O(n) — hashmap
Each outer step: scan remaining elements for complement. n elements × n scan = n² operations. No memory of past answers.
Each step: check hashmap for complement in O(1). If found → answer. If not → store current value. n steps × O(1) = O(n) total.
§ 02 — the design question that wins interviews

The key-design question

Every hashmap solution requires answering exactly two questions: (1) what is the key? and (2) what is the value? The value is usually obvious (a count, an index, a list of indices). The key requires a design decision — and getting it wrong is the single most common source of wrong first attempts.

the key doesn't have to be the data itself

The key can be any deterministic function of the data: the raw value, a sorted version of it, a frequency-count tuple, a prefix sum, an index offset, or any other derived signature. Choosing what to hash as the key is the intellectual core of this pattern class.

Where the naive key choice fails: Group Anagrams

Given a list of strings, group them so that all anagrams appear together. The naive approach: use each string itself as the hashmap key.

❌ raw string as key
✓ sorted-character tuple as key
"eat" → bucket A
"tea" → bucket B
"ate" → bucket C

Three separate buckets for three anagrams. The key doesn't capture the anagram relationship at all. Fails completely.
"eat" → sort → "aet" → bucket X
"tea" → sort → "aet" → bucket X
"ate" → sort → "aet" → bucket X

All three land in the same bucket. The sorted form is the canonical representative of any permutation.

The fix is structural: instead of asking "what is this string?", ask "what does this string reduce to when we remove all ordering information?" That reduced form is the key. Two strings are anagrams if and only if they reduce to the same form.

The two-question decision matrix

Frequency countingKey = the element itself (a character, number, word). Value = its count. Both choices are "obvious." The design work is knowing that you're producing a frequency map for downstream reasoning — do you need it fully built before you query it (two-pass) or can you build and query simultaneously (one-pass)?
Complement / lookupKey = the complement value you'd need (e.g., target - arr[i]), or the element itself if you store elements and look up complements. Value = the index where that element was seen. Neither is "obvious" — you must decide whether to store elements-seen or complements-needed, and the one-pass variant stores elements and asks "is my complement already here?"
Grouping by derived keyKey = a computed signature (sorted characters, frequency tuple, normalized form). Value = a list of elements sharing that signature. The key is definitionally non-obvious: choosing the signature requires identifying what invariant is shared by all members of the group.
Prefix sums as keysKey = a running cumulative quantity (a prefix sum, a running XOR, a running balance). Value = the index where that cumulative quantity first occurred. Non-obvious: the key is not an element at all — it's a derived aggregate. Recognizing this is the leap required for Subarray Sum Equals K and variants.
HashSet for existenceKey = the element. Value = nothing — set membership is Boolean. The design question is simply whether you need counts (HashMap) or just presence (HashSet). A HashSet is a HashMap where the value type is discarded. Prefer HashSet when you only ever ask "is X in here?" and never need "how many times was X seen?"
§ 03 — four concrete patterns

Four canonical patterns

These four patterns cover virtually all hashmap interview problems. A given problem may layer two of them together (e.g., prefix sums + frequency counting in Subarray Sum Equals K), but the individual patterns are the building blocks.

Pattern 1 — Frequency counting

Build a map from element → count, then reason over the count structure. The canonical problem is Valid Anagram: two strings are anagrams iff their character-frequency maps are equal. Frequency counting is often a sub-step inside a larger algorithm rather than the final answer by itself.

frequency count then query
# pass 1: build frequency map
freq = {}
for x in arr:
    freq[x] = freq.get(x, 0) + 1      # increment or initialize to 1

# pass 2: reason over the count map
for key, count in freq.items():
    # e.g., check if count > n//2 (majority element)
    # or compare against another freq map (anagram)
    # or find elements with count == 1 (first unique)

Pattern 2 — Complement / lookup (one-pass)

Process each element and ask: "have I already seen the thing I need to pair with this?" The map is built incrementally — store an element at step i, then check it at some later step j. This is one-pass because the map is populated and queried in the same scan.

one-pass validity condition

One-pass works when "have I seen the complement among elements before this one" is the correct question. If the problem requires global information (like whether an element appears in the array at all, regardless of position relative to the current element), pre-build the full map first (two-pass).

Pattern 3 — Grouping by derived key

Compute a signature for each element and bucket elements sharing the same signature. The signature must be: (1) identical for all elements in the same group, and (2) different for elements in different groups. The design work is entirely in choosing the right signature function.

Common signature choices: sorted characters of a string (anagram grouping), a tuple of (char, count) pairs in canonical order (more collision-resistant than sorted string for Unicode), a prefix/suffix normalization, a hash of the element itself (content-addressing), or a composite tuple of multiple fields.

Pattern 4 — HashSet for existence / dedup

When you only need to ask "have I seen X" and never "how many times have I seen X", a HashSet is cleaner than a HashMap with dummy values. Two important sub-cases: (1) dedup while preserving order — iterate and add to an output list only if the element isn't in the set yet; (2) cycle detection — if a value recurs, a cycle exists (e.g., Happy Number problem). The set tracks visited states, not counts.

hashset vs hashmap: when to use which

If your query is only "is X present?" → HashSet. If your query is "how many times has X appeared?" or "where was X first seen?" → HashMap. A HashSet is not a degenerate HashMap — it has a distinct semantic: membership, not counting. Using a HashMap where a HashSet suffices is a minor code smell that signals you haven't thought clearly about what information you actually need.

§ 04 — the scanning discipline

One-pass vs two-pass

Many candidates default to two-pass out of habit — build the full map, then query it. Two-pass is sometimes necessary (when you need global frequency information before you can answer any query), but one-pass is possible — and slightly more elegant — in a surprising number of cases.

When one-pass is valid

One-pass is valid when the question at each step is: "does the thing I need already exist among the elements I've processed before this point?" In Two Sum, at index i, you check if target - arr[i] is already in the map. If yes, the answer pair uses the element at i and some earlier element. The map at step i contains exactly arr[0..i-1]. This is correct because the pair consists of two distinct positions and we process them in order.

When two-pass is necessary

Two-pass is necessary when the query requires global information. Example: "find the first element whose frequency in the entire array is exactly 1." You can't answer this without knowing the final frequency of every element, which requires processing the entire array first. Similarly, "group anagrams" requires seeing all strings before you can assemble groups — though you can accumulate into buckets in one pass if you process one string at a time.

two-pass — pre-populate then query
one-pass — build and check simultaneously
Pass 1: scan entire array, build frequency or lookup map.
Pass 2: scan again, answer queries against the complete map.

Use when: the query requires knowing something about elements you haven't processed yet. Correct always. Slightly more memory (same asymptotically).
Single scan: for each element, check the map (query against past elements), then insert the element (for future elements to query against).

Use when: "have I seen the thing I need among earlier elements" is the complete question. More elegant, same asymptotic complexity.

In practice: try one-pass first. If you find yourself needing to know about future elements to answer the current query, switch to two-pass.

§ 05 — the honest engineering aside

Why O(1) holds (and when it doesn't)

Hash tables provide amortized O(1) average-case insertion and lookup. The word "amortized" and the qualifier "average-case" both carry real weight — interviewers at research-lab-tier companies occasionally probe this, and being caught flat-footed is avoidable.

How it works

A hash function maps a key to a bucket index: bucket = hash(key) % num_buckets. A perfect hash function spreads keys uniformly across all buckets — each lookup goes directly to the right bucket, O(1). Real hash functions are not perfect: two distinct keys can hash to the same bucket. This is a collision.

Chaining (most common)Each bucket holds a linked list (or dynamic array) of entries. On collision, the new entry is appended to the list at that bucket. Lookup scans the bucket's list linearly. In the best case (no collisions), each list has exactly one entry and lookup is O(1). In the worst case (all keys collide to one bucket), the list has n entries and lookup is O(n).
Open addressingAll entries live in the array itself. On collision, probe to the next available slot (linear probing, quadratic probing, or double hashing). Lookup follows the same probe sequence until it finds the key or an empty slot. More cache-friendly than chaining since no pointer chasing. Degrades badly at high load factors — the probe sequences get long.
Load factor and resizingLoad factor α = (number of entries) / (number of buckets). As α approaches 1, collisions become frequent and average chain/probe length grows. The standard mitigation: when α exceeds a threshold (typically 0.7–0.75), double the number of buckets and rehash all entries. Each entry is rehashed O(1) amortized across all insertions.
amortized O(1): what it actually means

Amortized O(1) means the average cost per operation over a sequence of n operations is O(1), even though any individual operation might occasionally be O(n) (during a resize). Across n insertions: most are O(1), one resize costs O(n) — amortized cost per insertion = O(n)/n = O(1). Under adversarial inputs (all keys collide) with a predictable hash function, worst case degrades to O(n) per operation. Language implementations mitigate this with randomized hash seeds (Python's hash randomization, Java's HashMap tree-ification at bucket depth > 8).

What to say in an interview

If an interviewer asks "what if the hash function is bad?", the complete answer is: average case O(1) holds under a uniform hash function and reasonable load factor. Worst case is O(n) per operation if all keys collide (adversarial input, birthday-paradox clustering). Real implementations defend with randomized hash seeds, so adversarial worst-case requires the attacker to know the seed. Java additionally converts degenerate buckets (8+ entries) to balanced BSTs, giving O(log n) worst case instead of O(n).

§ 06 — operationalize the design decision

Widget A — "What's the key?" interactive trainer

Five problems. For each, choose what the correct key and value should be in the hashmap solution. The wrong choices are plausible — they'll compile and run, just give the wrong answer or wrong complexity. On selection, you'll see exactly why the wrong choice fails and why the right one succeeds.

Widget A · key-design exercise

5 problems
§ 07 — watch the map build

Widget B — live hashmap build animator

Step through an array element by element. The hashmap panel updates live — watch insertions, hit-vs-miss lookups, and how the map's state changes at each step. Toggle between Two Sum (one-pass: build and check simultaneously) and frequency count (two-pass: populate first, then query) to see the structural difference in scanning order.

Widget B · hashmap animator

live build
array
phase
map size
0
answer
press Step to begin
current element lookup hit lookup miss answer found
hashmap { value → index }
empty
§ 08 — see where keys land

Widget C — hash table & collision visualizer

An 8-bucket hash table displayed as physical slots. Insert keys and watch them hash into buckets. Collisions are shown as chained entries in the same bucket — making the O(n) worst case visually obvious. Toggle between a good hash function (uniform distribution) and a deliberately bad one (everything lands in bucket 0) to see how quickly performance degrades.

Widget C · collision & bucket visualizer

8 buckets
bucket = sum(charCodes) % 8
entries
0
max chain length
0
load factor
0.00
lookup: O(1) average
newly inserted key collision in bucket
try this

Insert "eat", "tea", "ate" with the good hash function — they'll land in different buckets. Switch to bad hash function and insert anything — watch every key pile into bucket 0, turning lookups from O(1) into O(n) chain scans.

§ 09 — before you write a single line

Pattern recognition: the pre-code checklist

At Tier-1 interviews, the problem statement won't say "use a hashmap." The pattern has to be recognized from structural cues in the problem description. Internalize these triggers:

reach for a hashmap / hashset when you see
  • "Have I seen this before?" — any question about membership or prior occurrence. HashSet first, HashMap if you need to know where or how many times.
  • "Find the pair / triple that sums to..." (unsorted input, no sortedness given) — complement lookup. The sorted version belongs to two-pointers; the unsorted version belongs here.
  • "Count occurrences of..." or "frequency of..." — frequency map. Sub-question: do you need the full map before any answer (two-pass), or can you answer while building (one-pass)?
  • "Group / cluster elements that share some property" — grouping by derived key. The property defines the key: find the invariant shared by all members of the same group.
  • "Are these two strings / arrays equivalent under rearrangement?" — anagram / permutation check. Key design: sorted form or frequency-tuple as canonical signature.
  • Brute force is naturally O(n²) due to a repeated linear scan inside an outer loop — the inner scan is doing membership testing. Replace it with O(1) lookup via a map or set.
  • Prefix sums, running XOR, or any cumulative quantity — the running total is likely the key; you're looking for a prior occurrence of a related value (e.g., sum - k for subarray sums).
  • "Longest / shortest subarray satisfying..." where the constraint is about element values (not counts or distinct-ness) — may need prefix sum + HashMap. Distinguish from sliding window, which handles constraints on the window's contents.
§ 10 — beyond the interview room

Hashmap patterns in real systems

The four interview patterns are not toy abstractions — each has a direct, non-trivial application in production systems. Recognizing them bridges the gap between interview performance and engineering judgment.

GROUP BY as bucketing by derived key. A tabular query tool's GROUP BY implementation is a direct, undisguised application of the grouping pattern: each row is hashed by its group-key column value, and rows sharing a key accumulate into the same bucket for aggregation — identical in structure to grouping anagrams by sorted characters, except the key is an existing column value rather than a computed signature. The aggregation function (SUM, COUNT, AVG) is then applied per bucket, exactly as frequency counting reduces a bucket's entries to a scalar.

UTXO set as O(1) existence check with a composite key. A ledger-style system that must answer "does this referenced output exist and is it still unspent?" on every incoming transaction is using a hashmap purely for O(1) existence and membership checking — structurally identical to a "have I seen this value before?" interview pattern, except the key is a composite of two fields (a transaction identifier and an output position index) rather than a single primitive value. This is itself a good illustration of the "key can be any deterministic derived value, including a tuple" principle from the grouping pattern.

Database hash indexes and hash joins. A hash index stores hash(column_value) → row_pointer, enabling O(1) equality lookups on that column — the database equivalent of a complement-lookup map. Hash join algorithms (used in query planning when two large tables need to be joined on a key) build a hash map from the smaller table's join key to its rows, then probe it for each row of the larger table — a textbook one-pass build-and-query pattern.

Content-addressed storage. Systems like Git, IPFS, and content delivery networks use the data's own hash as its key — the "key is a derived signature, not the raw value" principle applied at the storage level. Two files with identical content produce identical hashes, and identical hashes guarantee identical content (with cryptographic certainty). This is the grouping-by-derived-key pattern elevated to a storage model: the signature is the address.

prefix sum + hashmap: the pattern most often missed

Many subarray problems reduce to: "find two indices i and j such that some function of the subarray arr[i..j] equals a target." If that function is a prefix-sum difference, the hashmap key is the prefix sum value, and you're checking whether prefix_sum - target has appeared before. This pattern spans subarray sum equals k, number of subarrays with product less than k, and many variants. Recognizing "I need to query over prefix sums" is the key transfer from interview to production analytics queries over time-series data.

§ 11 — calibrated easy → hard

Problem set — 10

Ordered easy to hard. All four sub-patterns are covered. Four problems explicitly reuse Widget A or Widget B (same visual vocabulary, different parameters). Problem 10 is a design/explain problem — the kind that appears at research-lab tier as a follow-up to a coding problem, not as the primary coding task.

§ 12 — language-agnostic

Pseudocode templates

Four templates. Every annotation names a decision point you will encounter, not a formality. The fourth template (hash table insertion with chaining) makes Widget C's behavior legible as executable pseudocode.

template 1 — one-pass lookup-complement (Two Sum)
function one_pass_complement(arr, target):
    seen = {}                           # maps: value → index it was seen at

    for i from 0 to len(arr) - 1:
        complement = target - arr[i]

        if complement in seen:           # O(1) lookup: have I seen what I need?
            return [seen[complement], i]  # yes — pair found

        seen[arr[i]] = i                # no — record this element for future queries
                                        # note: insert AFTER the lookup check
                                        # prevents pairing arr[i] with itself

    return None                         # no valid pair exists

# key: target - arr[i] (or arr[i] stored for complement lookup)
# value: index i
# one-pass valid because: we only need "seen before this position"
template 2 — frequency count then query (two-pass)
function freq_count_two_pass(arr):
    # pass 1: build frequency map over the ENTIRE input
    freq = {}
    for x in arr:
        freq[x] = freq.get(x, 0) + 1

    # pass 2: query the complete map
    result = []
    for x in arr:                       # or iterate over freq.items() if you want pairs
        if freq[x] == 1:               # example: find elements with frequency == 1
            result.append(x)
        # other queries: freq[x] > n // 2 (majority), max(freq.values()) (top K), etc.

    return result

# two-pass necessary when: the query requires knowing the FINAL count of every element
# (e.g., "first element with frequency 1" can only be answered after seeing all elements)
template 3 — grouping by derived key
function group_by_signature(items):
    groups = {}                              # maps: signature → [list of items]

    for item in items:
        sig = compute_signature(item)        # THE design decision: what is the key?
                                             # anagrams: sorted(item)
                                             # numeric: item % k, item // k, abs(item)
                                             # strings: tuple(freq_map(item))
                                             # general: any deterministic fn of item

        if sig not in groups:
            groups[sig] = []
        groups[sig].append(item)             # bucket this item by its signature

    return list(groups.values())

# compute_signature must be:
#   (1) deterministic — same input always gives same signature
#   (2) same for all items in the same group
#   (3) different for items in different groups (no false groupings)
template 4 — hash table insertion with chaining (Widget C in pseudocode)
class HashTable:
    buckets = [[] for _ in range(num_buckets)]   # each bucket is a chain (list)
    size = 0
    LOAD_THRESHOLD = 0.75

    function insert(key, value):
        idx = hash(key) % len(buckets)              # map key to bucket index
        chain = buckets[idx]

        for entry in chain:                         # check for existing key (update)
            if entry.key == key:
                entry.value = value
                return

        chain.append({key: key, value: value})      # collision: append to chain
        size += 1

        if size / len(buckets) > LOAD_THRESHOLD:    # O(n) rehash amortized to O(1)
            rehash()

    function lookup(key):
        idx = hash(key) % len(buckets)
        for entry in buckets[idx]:                  # scan chain at this bucket
            if entry.key == key: return entry.value
        return None                                 # key not found

    # lookup cost: O(1) average (chain length ~1), O(n) worst case (all keys collide)
    # amortized O(1) insert: most inserts are O(1); rehash is O(n) but rare
§ 13 — go deeper

Further reading

Hash functions and load factor — CLRS Chapter 11 Introduction to Algorithms (Cormen, Leiserson, Rivest, Stein), Chapter 11: "Hash Tables." Covers hash function design (division method, multiplication method, universal hashing), load factor analysis, chaining vs open addressing, and the formal proof of expected O(1) lookup under uniform hashing. This is the definitive reference for "what if everything collides" follow-up depth — universal hashing (choosing a hash function randomly from a family) is the principled defense against adversarial inputs, and the chapter proves its guarantees rigorously. Skip to §11.3–11.4 for the analysis most likely to appear in interviews.
HashMap vs HashSet vs TreeMap — when O(1) is worth losing ordering HashMap and HashSet provide O(1) average-case insert, lookup, and delete, but offer no ordering guarantees — iteration order is undefined (or implementation-defined). TreeMap (a balanced BST under the hood — Java's is a red-black tree) provides O(log n) operations but guarantees sorted iteration order and supports range queries (floorKey, ceilingKey, subMap). Use HashMap/HashSet when you only need membership or frequency and the keys have no natural ordering you need to exploit. Use TreeMap/OrderedDict when you need to answer "what's the nearest key to X?" or "give me all keys between A and B" — these queries are O(log n) on a tree and undefined (linear scan) on a hash table.
Hash indexes and hash joins in databases CMU 15-445/645 Database Systems, Lecture 7 (Hash Tables) and Lecture 11 (Join Algorithms). Database hash indexes are the production version of the complement-lookup pattern: the index maps hash(column_value) to a list of row pointers, enabling O(1) equality lookups on that column. Hash join (Grace hash join, Classic hash join) builds a hash table on the smaller relation's join key, then probes it for each tuple in the larger relation — a large-scale, multi-phase version of the one-pass build-and-check template. These lectures are freely available on CMU's course website and are unusually clear on the engineering tradeoffs versus B-tree indexes.
Content-addressed storage — the key-as-derived-signature principle at scale Git's object model (git-scm.com/book, Chapter 10: Git Internals) is the most accessible worked example of content-addressed storage: every object (blob, tree, commit) is stored under its SHA-1 (now SHA-256) hash — the hash of the content is the key. This makes the "key can be any deterministic function of the data" insight from grouping-by-derived-key concrete at the filesystem level. IPFS extends this to a distributed content-addressable store: identical content has the same address on every node worldwide, making deduplication a structural property rather than a policy. Reading Git Internals before a systems design interview on distributed storage is worth 30 minutes.
Where this topic sits in the sequence: Hashing is the prerequisite for several downstream patterns. Sliding window problems that track "distinct character count" or "frequency of each character in the window" use a HashMap as their window-state structure. Top K Frequent Elements (Problem 5 in this set) uses a frequency HashMap as input to a min-heap — a direct bridge to the Heaps topic. Prefix sum + HashMap (Problem 7) is itself a prerequisite for understanding difference arrays and range-update patterns. The grouping-by-key pattern recurs in graph problems (grouping nodes by component, degree, or neighbor-signature) and in DP where the "state" is a derived quantity stored as a map key rather than an array index.
hashing · study console · part of the DSA interview series