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
- Hash the keyCompute an integer from the key (language runtime or a chosen hash).
- Reduce to a bucketMap that integer into 0 \ldots m-1 for an m-bucket table — typically a modulo or bit mask.
- 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
- O(1), because the hash jumps to a bucket instead of scanning all n keys
- O(n), because every lookup must compare against every stored key
- 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
- ChainingAppend the key to the list at the hashed bucket. Lookup walks that list only.
- Open addressingIf the hashed slot is full, probe the next candidate slot until an empty one appears.
- Same contractBoth still aim for short searches per operation; the difference is list-at-bucket versus probe-through-array.
| Strategy | Where the key lives | Failure mode when crowded |
|---|---|---|
| Separate chaining | Per-bucket list (or tree) | Long chains → slow bucket walks |
| Linear probing | Next open slot in the array | Primary clustering of filled runs |
| Quadratic / double hash | Open slot via a non-unit stride | Still degrades if \alpha is too high |
Under separate chaining, two keys with the same hash
- Both live in the same bucket's list; lookup walks that list
- The second key overwrites the first; only one key per bucket is allowed
- 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
- Measure \alphaAfter an insert, \alpha = n/m. Compare to the threshold (e.g. 0.75).
- Allocate larger mTypically double the bucket count so \alpha halves.
- 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
- O(n) for that resize, amortized O(1) per insert across a doubling sequence
- O(1) wall-clock for the resize itself, because hashing is constant time
- 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
- Only membership?Use a hash set — the key is the whole record.
- Need a payload?Use a hash map — key looks up the associated value.
- Same costsAverage O(1) access either way; space still O(n) for n stored keys (plus values in a map).
| Structure | Stores | Typical question |
|---|---|---|
| Hash set | Unique keys | Have I seen x? |
| Hash map | Key → value | What is bound to k? |
Recording each number's index while scanning for Two Sum needs
- A hash map from value → index
- A hash set of values only, because indices are never required
- 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
- All keys share a bucketHash collisions pile every entry into one chain or one probe cluster.
- Lookup walks themFinding or rejecting a key compares against \Theta(n) entries.
- MitigationTreeified buckets (e.g. in Java) cut that walk to O(\log n); they do not restore true O(1) worst case.
| Situation | Lookup / insert / delete |
|---|---|
| Well-spread keys, moderate \alpha | Average O(1) |
| All keys collide, plain chains | Worst case O(n) |
| All keys collide, treeified bucket | Worst case O(\log n) per op in that bucket |
If every key lands in one plain linked-list bucket, hash-table lookup is
- O(n) in the worst case — the chain holds every entry
- Still O(1) worst case, because the hash function ran in constant time
- 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
- Empty mapvalue → index, initially empty.
- ComplementFor nums[i], compute need = target − nums[i].
- 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] = iTwo 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
- 2 → 0, so index 1 can find complement 2
- 7 → 0, anticipating the next value
- 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
- Frequency passFor each character, increment its count in a hash map.
- Order passWalk the string from the left; return the first character with count 1.
- 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
- "l", the leftmost character with count 1
- "t", because e repeats and l is skipped for being first
- "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.
| You need… | Reach for… |
|---|---|
| Membership / seen-before | Hash set |
| Value → index or count | Hash map |
| Pair sum without sorting | One-pass map (Two Sum) |
| Sorted pair sum, O(1) extra memory | Two pointers (not hashing) |
On a sorted array, finding two elements that sum to a target with O(1) extra memory is best done with
- Opposite-end two pointers, not a hash map
- A hash map only — sorting makes hashing mandatory
- 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.
| Operation / situation | Time | Extra space |
|---|---|---|
| Average insert / lookup / delete | O(1) | O(n) for the table |
| Worst case, plain colliding chain | O(n) | O(n) |
| Worst case, treeified bucket | O(\log n) | O(n) |
| Resize / rehash of n keys | O(n) once; amortized O(1) per insert | O(n) new buckets |
| Two Sum via hash map | O(n) | O(n) |
| First unique character (two passes) | O(n) | O(k) alphabet |
The identities the ledgers keep using.
| Identity | Form |
|---|---|
| Load factor | \alpha = n / m |
| Typical resize threshold | grow when \alpha > 0.75 (library-dependent) |
| Two Sum complement | need = target − nums[i] |
| Space for n keys | O(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