Hash Table
A data structure that spreads keys into buckets with a hash function for average O(1) access.
Worth knowing first
#How does it work?
A hash table stores keys by turning them into an array index with a hash function. To find a key you don't scan all the data: you compute the hash and go straight to the right bucket.
Different keys can land in the same bucket; this is a collision. With separate chaining, used in this lesson, each bucket keeps a small list of the keys that land in it.
A good hash function spreads keys evenly across buckets. Chains stay short, so insert, search and remove take constant time on average.
#Try it yourself
Type a key and insert it. The hash function (key mod 7) picks its bucket; keys in the same bucket form a chain.
#Basic operations
- insert(key)
- Finds the bucket with hash(key); if the key isn't there, appends it to the chain.
- search(key)
- Scans only the chain in bucket hash(key).
- remove(key)
- Finds the key in its bucket's chain and unlinks it.
- load factor
- Number of elements / number of buckets. Past a limit (e.g. 0.75) the table grows and every key is rehashed.
#Example code
The same algorithm in 8 languages. Every program is compiled, run and checked to print the same output.
class HashSet:
"""Separate chaining: each bucket is a list of the keys that hash to it."""
def __init__(self, bucket_count: int) -> None:
self.buckets: list[list[int]] = [[] for _ in range(bucket_count)]
def _hash(self, key: int) -> int:
return key % len(self.buckets)
def contains(self, key: int) -> bool:
return key in self.buckets[self._hash(key)]
def add(self, key: int) -> None:
if not self.contains(key):
self.buckets[self._hash(key)].append(key)
def remove(self, key: int) -> None:
chain = self.buckets[self._hash(key)]
if key in chain:
chain.remove(key)
s = HashSet(7)
for key in (15, 8, 22, 31): # 15, 8 and 22 all hash to bucket 1
s.add(key)
print(str(s.contains(22)).lower())
s.remove(8)
print(str(s.contains(8)).lower())
print(*s.buckets[1])#Complexity
| Operation | Time |
|---|---|
| Insert (average) | O(1) |
| Search (average) | O(1) |
| Remove (average) | O(1) |
| Worst case (all in one bucket) | O(n) |
| Rehashing | O(n), rarely |
| Memory | O(n) |
Average O(1) holds as long as the load factor stays low and the hash function spreads keys well. If every key lands in the same bucket, the table becomes a linked list and operations are O(n). Growing takes O(n) at once but happens rarely, so the cost per element is constant (amortised O(1)).
#When to use it?
Anywhere you need fast access by key: dictionaries and sets (Python dict/set, Java HashMap, JavaScript Map/Set), caches, counters, duplicate checks. If you need to walk elements in order or run range queries, balanced trees fit better.
#Practice
Log in to answer questions and save your progress.
Easy
What is the average complexity of a hash table lookup?
- 1O(1)
- 2O(log n)
- 3O(n)
- 4O(n log n)
What is it called when two different keys land in the same bucket?
- 1Collision
- 2Overflow
- 3Rehashing
- 4Balancing
With separate chaining, where are colliding keys kept?
- 1In a list inside the bucket
- 2In the next empty bucket
- 3In a separate hash table
- 4They aren't stored
Medium
With 7 buckets and hash(k) = k mod 7, which bucket does 23 go to?
- 12
- 23
- 31
- 46
Hard
When do hash table operations slow down to O(n)?
- 1When most keys land in the same bucket
- 2When the table is empty
- 3When keys are numbers
- 4When the bucket count is prime
#Up next
Lessons that build on this one: