E ExamMaster

CS Core & Software Engineering · Data Structures & Algorithms

Hashing and Hash Maps

Hash tables, collision handling and using maps and sets for constant time lookups.

Eight concepts on hash tables — the hash function, collisions, load factor, sets versus maps, worst-case collapse, and the two classic interview patterns (Two Sum and first unique character). Average O(1) is the prize; collisions and adversarial keys are the catch.

  • CS Core & Software Engineering
  • Easy level
  • 8 concepts
  • 5 practice questions

1Hash function to bucket index

A hash table stores key–value pairs in an array of buckets. A hash function maps each key to an integer bucket index, so insert, delete, and lookup avoid a linear scan of every stored key. In the average case those three operations are O(1) — you pay extra space for the bucket array to buy constant-time access by key.

The function does not invent order among keys. Two different keys may land in the same bucket; that collision is handled by the next concept. What hashing buys is a direct jump to a small neighbourhood of the table, not a guarantee that every key has a private cell forever.

Figure. Five equal bucket cells. The hash reduces a key to one index in 0\ldots4; work then stays inside that cell's chain or probe sequence.

Where a key lands

  1. Hash the keyCompute an integer from the key (language runtime or a chosen hash).
  2. Reduce to a bucketMap that integer into 0 \ldots m-1 for an m-bucket table — typically a modulo or bit mask.
  3. Act in that bucketInsert, find, or delete inside the bucket's collision structure. Average work stays O(1) when buckets stay short.
Average-case lookup by key in a well-sized hash table is
  1. O(1), because the hash jumps to a bucket instead of scanning all n keys
  2. O(n), because every lookup must compare against every stored key
  3. O(\log n), because buckets are always searched with binary search

The hash selects a bucket in constant work. A full scan is the unsorted-array baseline hashing avoids; binary search inside buckets is an implementation choice, not the average-case bound.

2Collisions: chaining vs open addressing

When two keys hash to the same index, the table must still store both. Separate chaining hangs a list (or tree) off each bucket and walks only that bucket's chain. Open addressing keeps one key per slot and, on a collision, probes onward — linear probing steps one cell at a time, quadratic probing uses a quadratic stride, and double hashing uses a second hash for the step.

Chaining grows local lists; open addressing spreads keys across the array and is sensitive to clustering. Neither removes the need for a good hash and a controlled load factor — they only decide where the colliding key lives.

Figure. Bucket 2 holds a chain apple → apricot → avocado. Open addressing would instead scatter those keys into later empty slots of the array.

Two ways to place a colliding key

  1. ChainingAppend the key to the list at the hashed bucket. Lookup walks that list only.
  2. Open addressingIf the hashed slot is full, probe the next candidate slot until an empty one appears.
  3. Same contractBoth still aim for short searches per operation; the difference is list-at-bucket versus probe-through-array.
Collision strategies
StrategyWhere the key livesFailure mode when crowded
Separate chainingPer-bucket list (or tree)Long chains → slow bucket walks
Linear probingNext open slot in the arrayPrimary clustering of filled runs
Quadratic / double hashOpen slot via a non-unit strideStill degrades if \alpha is too high
Under separate chaining, two keys with the same hash
  1. Both live in the same bucket's list; lookup walks that list
  2. The second key overwrites the first; only one key per bucket is allowed
  3. The table must immediately rehash into a larger array before storing the second key

Chaining stores every colliding key in the bucket structure. Overwrite would lose data; resize is driven by load factor, not by the first collision alone.

3Load factor and rehash

Load factor \alpha = n / m is the ratio of stored entries n to buckets m. As \alpha grows, chains lengthen or probe runs stretch, and average operation time drifts above the O(1) target. Implementations typically resize (often double m) and rehash every key when \alpha crosses a threshold such as 0.75.

A single resize costs O(n) to place every key into the new table. Amortized over many insertions that cost spreads to O(1) per insert — the same doubling argument as a dynamic array — but any one insert that triggers growth still pays the hitch.

Figure. When load factor \alpha grows, rehash into a larger table so average chain length stays near constant.

When the table grows

  1. Measure \alphaAfter an insert, \alpha = n/m. Compare to the threshold (e.g. 0.75).
  2. Allocate larger mTypically double the bucket count so \alpha halves.
  3. Rehash all keysRecompute each key's bucket in the new array — O(n) work once, amortized O(1) per insert across a growth sequence.

Cross the resize line

A table has m = 8 buckets and currently holds n = 5 entries. The implementation resizes when \alpha > 0.75. After one more successful insert (still before any resize), what is \alpha, and does that insert trigger a resize?

  • After insert, n = 5 + 1n = 6
  • \alpha = n / m = 6 / 80.75
  • Resize rule: trigger when \alpha > 0.75no resize yet (\alpha = 0.75 is not > 0.75)

Pro tip. Thresholds differ by library — some resize at \geq 0.75. Read the inequality; off-by-one on the boundary is a common exam trap.

Rehashing all n keys when the table doubles costs
  1. O(n) for that resize, amortized O(1) per insert across a doubling sequence
  2. O(1) wall-clock for the resize itself, because hashing is constant time
  3. O(n^2), because every key is compared to every other key while moving

Each of the n keys is placed once into the new table. Doubling spreads those linear hits so the average insert stays O(1), but the resize call is still linear.

4Hash set vs hash map

A hash set stores unique keys and answers membership: is x present? A hash map stores key–value pairs and answers retrieval: what value is bound to k? Both sit on the same hash-table engine, so average insert, delete, and lookup stay O(1) for each.

Reach for a set when the value is only "present or not" (seen characters, visited nodes). Reach for a map when you must remember an associated payload (value → index, character → count). Swapping them usually means you are encoding the payload into a side structure you did not need.

Figure. A set answers membership; a map stores a payload per key — pick by whether you need the associated value.

Pick the structure

  1. Only membership?Use a hash set — the key is the whole record.
  2. Need a payload?Use a hash map — key looks up the associated value.
  3. Same costsAverage O(1) access either way; space still O(n) for n stored keys (plus values in a map).
Set vs map
StructureStoresTypical question
Hash setUnique keysHave I seen x?
Hash mapKey → valueWhat is bound to k?
Recording each number's index while scanning for Two Sum needs
  1. A hash map from value → index
  2. A hash set of values only, because indices are never required
  3. A sorted array alone, because hashing cannot store indices

The algorithm returns indices, so the payload is the index. A set remembers presence but forgets where the value sat; sorting is a different Two Sum variant.

5Worst case: when hashing falls to linear

Average O(1) assumes keys spread across buckets. With a bad hash or adversarial keys that all collide, every operation walks a structure of size n and degrades to O(n). That is the same cost as a linked list or unsorted scan — the table shape no longer helps.

Some runtimes mitigate long chains by treeifying a bucket past a length threshold, bringing a pathological bucket down to O(\log n) rather than O(n). The average-case slogan still needs the disclaimer: constant time is not a worst-case guarantee unless the hash and load factor cooperate.

Reuse the chaining picture under Collisions: chaining vs open addressing — a long single-bucket chain is exactly the O(n) degeneration; treeified buckets cut that bucket to O(\log n).

How the bound collapses

  1. All keys share a bucketHash collisions pile every entry into one chain or one probe cluster.
  2. Lookup walks themFinding or rejecting a key compares against \Theta(n) entries.
  3. MitigationTreeified buckets (e.g. in Java) cut that walk to O(\log n); they do not restore true O(1) worst case.
Cost under collision load
SituationLookup / insert / delete
Well-spread keys, moderate \alphaAverage O(1)
All keys collide, plain chainsWorst case O(n)
All keys collide, treeified bucketWorst case O(\log n) per op in that bucket
If every key lands in one plain linked-list bucket, hash-table lookup is
  1. O(n) in the worst case — the chain holds every entry
  2. Still O(1) worst case, because the hash function ran in constant time
  3. O(\log n) automatically, even without treeifying the bucket

Hashing only picks the bucket; a single giant chain is a linear search. Constant-time hashing does not bound the walk; O(\log n) needs an ordered tree in the bucket.

6Two Sum in one pass

Given an array and a target, return indices of two values that sum to the target. Brute force tries every pair in O(n^2). The hash-map pattern stores each value's index as you scan: for the current number, ask whether \mathrm{target} - \mathrm{num} was already seen; if yes, return those indices; if not, record the current value → index.

One pass yields O(n) time and O(n) extra space. The map is essential — a set of values alone would lose the indices the problem asks for.

Figure. One pass: for each x look up t-x in the map of prior values; store x after the probe.

One-pass map

  1. Empty mapvalue → index, initially empty.
  2. ComplementFor nums[i], compute need = target − nums[i].
  3. Hit or storeIf need is in the map, return [map[need], i]; else store nums[i] → i.

Two Sum, one pass

def two_sum(nums, target):
    seen = {}
    for i, num in enumerate(nums):
        need = target - num
        if need in seen:
            return [seen[need], i]
        seen[num] = i

Two Sum

Given nums = [2, 7, 11, 15] and target = 9, return indices of the two numbers that add up to the target.

  • i = 0, num = 2, need = 9 − 2 = 7; 7 not in map → store 2 → 0seen = {2: 0}
  • i = 1, num = 7, need = 9 − 7 = 2; 2 is in map at index 0return [0, 1]
  • Time / space versus nested loopsO(n) time, O(n) space (not O(n^2))

Pro tip. The one-pass hash-map solution is O(n) time and O(n) space, far better than the O(n^2) nested loop.

Coding lab. Two Sum in one pass runs in the app, with checks on your output.

At nums = [2, 7, 11, 15], target = 9, after processing index 0 the map must contain
  1. 2 → 0, so index 1 can find complement 2
  2. 7 → 0, anticipating the next value
  3. Nothing — the map is filled only after the pair is found

You store the value you just saw, not the complement. Index 1 looks up need = 2 and hits the entry written at index 0.

7First non-repeating character

To find the first character that appears exactly once, count frequencies in a hash map on a first pass, then scan the string again in order and return the first character whose count is 1. Two linear passes beat comparing every pair in O(n^2).

Space is O(k) for alphabet size k (or O(n) in the worst case over arbitrary symbols). Order of first appearance comes from the second scan of the string — the map alone does not know which unique character appeared first.

Figure. "leetcode" after the frequency pass. Second scan returns the first letter whose count is 1 — here l — later uniques stay unused.

Count, then scan

  1. Frequency passFor each character, increment its count in a hash map.
  2. Order passWalk the string from the left; return the first character with count 1.
  3. ComplexityO(n) time, O(k) space for alphabet size k.

First Non-Repeating Character

Given a string, find the first character that appears exactly once, e.g. "leetcode" → "l".

  • Frequency map after one pass over "leetcode"l:1, e:3, t:1, c:1, o:1, d:1
  • Second scan: check l (count 1)return "l"
  • Later uniques t, c, o, d are ignored once l winsfirst-in-order, not any unique

Pro tip. Two linear passes with a frequency map avoid the O(n^2) compare-every-pair approach.

In "leetcode", after building frequencies, the first non-repeating character is
  1. "l", the leftmost character with count 1
  2. "t", because e repeats and l is skipped for being first
  3. "e", the most frequent character

The second pass returns the first count-1 character in string order. Frequency alone would not prefer l over t; most-frequent is a different problem.

8When hashing is the right hammer

Reach for a hash set or map when the bottleneck is "have I seen this key" or "what did I store under this key" and you can afford O(n) extra memory for average O(1) probes. Two Sum, anagram grouping, frequency counting, and graph visited-sets are the usual shapes.

Skip hashing when you need ordered traversal of keys, when worst-case O(1) is mandatory without a trusted hash, or when O(1) extra memory is required and a two-pointer or in-place pass exists. The structure is a trade: space and hash quality for speed.

Use the need → structure table: membership → hash set; key→value or count → hash map; unsorted pair sum → one-pass map; sorted pair sum with O(1) extra memory → two pointers instead.

Prompt shape → structure
You need…Reach for…
Membership / seen-beforeHash set
Value → index or countHash map
Pair sum without sortingOne-pass map (Two Sum)
Sorted pair sum, O(1) extra memoryTwo pointers (not hashing)
On a sorted array, finding two elements that sum to a target with O(1) extra memory is best done with
  1. Opposite-end two pointers, not a hash map
  2. A hash map only — sorting makes hashing mandatory
  3. Nested loops only — neither pointers nor hashing apply

Sortedness lets two pointers move inward in O(n) time and O(1) space. A hash map still works but spends O(n) memory the sorted variant can avoid.

Notes

  • Hash Table Basics: A hash function maps keys to bucket indices, giving average O(1) insert, delete, and lookup by trading space for speed.
  • Collision Handling: Separate chaining stores colliding keys in a per-bucket list; open addressing (linear/quadratic probing, double hashing) finds the next open slot.
  • Load Factor: The ratio of entries to buckets; when it exceeds a threshold (e.g., 0.75) the table resizes and rehashes to keep operations near O(1).
  • Sets vs Maps: A hash set stores unique keys for membership tests; a hash map stores key-value pairs - both give average constant-time access.
  • Worst Case: With many collisions (adversarial keys or a bad hash), operations degrade to O(n); Java mitigates this by treeifying long buckets to O(\log n).

Formulas

  • Average lookup/insert/delete: O(1); worst case: O(n).
  • Load factor: \alpha = n / m (entries n over buckets m); resize typically when \alpha > 0.75.
  • Space complexity: O(n) for n stored keys.
  • Rehash cost: O(n) when the table doubles, amortized to O(1) per insertion.
  • Two Sum via hash map: O(n) time, O(n) space versus O(n^2) brute force.

Exam traps & shortcuts

  • Whenever a problem asks 'have I seen this before?' or needs frequency counts, a hash map/set turns O(n^2) into O(n).
  • For Two Sum, store each value's complement in a map and check membership in one pass.
  • Use a hash set to detect duplicates or find intersections/unions of collections in linear time.
  • Remember hash maps do not preserve order; if order matters use an ordered/linked map or sort afterward.

Reference tables

Restated from the concepts above — scan sheet, not a second argument.

Complexity cheatsheet
Operation / situationTimeExtra space
Average insert / lookup / deleteO(1)O(n) for the table
Worst case, plain colliding chainO(n)O(n)
Worst case, treeified bucketO(\log n)O(n)
Resize / rehash of n keysO(n) once; amortized O(1) per insertO(n) new buckets
Two Sum via hash mapO(n)O(n)
First unique character (two passes)O(n)O(k) alphabet

The identities the ledgers keep using.

Formula anchors
IdentityForm
Load factor\alpha = n / m
Typical resize thresholdgrow when \alpha > 0.75 (library-dependent)
Two Sum complementneed = target − nums[i]
Space for n keysO(n)

Recap

Read only this the night before a hashing round.

Core
Hash → bucket; average insert/lookup/delete O(1) by spending space.
Collisions
Chaining lists per bucket; open addressing probes for the next slot.
Load
\alpha = n/m; past the threshold, resize and rehash (O(n) once, amortized O(1)).
Set / map
Set = membership; map = key → value. Same engine, different payload.
Worst case
All-collide → O(n); treeified buckets soften to O(\log n), not true O(1).
Two Sum
One-pass map of value → index; complement hit returns the pair.
Unique char
Count in a map, then scan in order for the first count-1 character.

Practise Hashing and Hash Maps

Reading is free and needs no account. Practice, mocks and progress live in the app.

  • 5 exam-style questions on this topic, with explanations
  • A 6-question practice set that ends the chapter
  • Timed mocks scored with the real marking scheme
  • Readiness tracked per topic, kept on your device
Continue with Google — freeNo card, no trial. Works offline once installed.