Once ADTs, pointers, and basic node structures are in place, the next question is how to support dictionary-style operations efficiently. A hash table is one of the standard answers: instead of preserving sorted order, it applies a hash function to jump directly to a bucket.
That jump is powerful, but it is never perfect. The whole chapter exists to explain what the jump can guarantee, where collisions come from, and how the implementation recovers when multiple keys land in the same location.
Motivation
Start with the dictionary operations the table must support:
- insert a value under a key,
- look up the value for a key,
- delete an existing key-value entry.
So the logical object is a dictionary or symbol table. The hash table is only one implementation strategy for that ADT.
Definition
Hashtable as a dictionary ADT
A hashtable stores key-value pairs and supports dictionary operations such as insert, lookup, and delete by mapping each key to a bucket index.
This definition already implies two different design questions:
- how keys are converted into bucket indices,
- what the implementation does when two keys map to the same index.
What a hash function is supposed to do
Definition
Hash function
A hash function maps a key into an integer index in a fixed range, usually
[0, H_SIZE).
A good hash function should have four properties:
- it must always return a legal bucket index,
- it should reduce collisions,
- it should distribute keys reasonably evenly,
- it should be quick to compute.
The last point matters because a beautifully distributed hash rule is not very useful if every lookup spends too long computing the bucket index itself.
Worked example
A simple string hash idea
For a string key, one very simple rule is:
int Hash(char *s, int nBuckets) {
int h = 0;
while (*s != '\0') {
h += *s;
s++;
}
return h % nBuckets;
}
This kind of rule is easy to compute, but it is also easy to fool. Strings whose
character sums are equal, or merely congruent modulo nBuckets, collide;
anagrams are an immediate example. The fragment also assumes nBuckets > 0,
nonnegative input characters such as ASCII bytes, and a sum that fits in int.
Those assumptions make it useful for tracing the calculation, but a production hash
table would use a carefully defined unsigned hash state and a stronger mixing
rule.
Read and try
Test a simple hash-and-bucket model
The lab maps sample keys into buckets with a simple hash rule so you can see collisions and chaining directly.
Collision count: 0
| Keys | Bucket index |
|---|---|
| cat | 4 |
| dog | 6 |
| cow | 0 |
| cod | 2 |
Bucket index 0
cow
Bucket index 1
∅
Bucket index 2
cod
Bucket index 3
∅
Bucket index 4
cat
Bucket index 5
∅
Bucket index 6
dog
Collision is unavoidable, not exceptional
The most important conceptual shift is that collisions are not a rare bug. They are a normal consequence of sending a large key space into a fixed set of buckets.
Definition
Collision
A collision happens when two different keys are mapped to the same bucket index.
If the bucket count is fixed and the key space is large, collisions are inevitable. So the real implementation question is not “how do we avoid every collision?” but “how do we recover from collisions without breaking dictionary correctness?”
Watch collision handling preserve dictionary correctness
The short visual below keeps the ADT contract visible while comparing the two main recovery patterns. Hashing only chooses where to start; chaining and probing explain how the table keeps different keys from being confused after a collision.
Watch how a hash table keeps dictionary operations correct when two different keys collide.
Hashing is only the first step. Correct lookup and update still depend on the collision strategy checking keys along the chain or probe sequence.
Chaining keeps colliding keys in one bucket list
Chaining handles a collision by retaining the entries in a bucket-side list.
Definition
Chaining
With chaining, each bucket stores a linked list of all entries whose keys hash to that bucket.
This design fits the pointer-and-node model from the previous section very naturally.
typedef struct cellT {
char *key;
void *value;
struct cellT *next;
} cellT;
Each bucket head points to the first node in its chain. Lookup therefore has two layers:
- hash the key to choose a bucket,
- scan only that bucket’s chain instead of the whole table.
Worked example
What lookup does under chaining
Suppose Hash("cat") = 3, and bucket 3 stores the chain
("cow", value1) -> ("cat", value2) -> ("cod", value3).
To execute Lookup(table, "cat"), the implementation:
- computes bucket
3, - moves along the linked list in bucket
3, - compares keys until
"cat"is found, - returns
value2.
The collision does not destroy correctness. It only makes that one bucket take longer to search.
Open addressing uses probing instead of linked chains
Open addressing keeps all entries inside the table. Instead of storing a linked list in each bucket, it keeps probing other slots when the preferred one is occupied.
Typical probing styles include:
- linear probing,
- quadratic probing,
- double hashing.
The key difference from chaining is where the extra work lives:
- chaining keeps multiple entries in a bucket-side list,
- open addressing keeps searching for another empty slot in the main table.
This means clustering and probe-sequence design become central implementation issues. Deletion needs an additional rule that preserves probe paths. This section names delete as an ADT operation without developing that separate invariant.
Theorem
Collision handling is part of correctness, not only performance
If the collision strategy is wrong, the table can return the wrong answer or lose entries entirely, even if the hash function itself is valid.
That is why the collision policy belongs in the conceptual explanation, not as an implementation afterthought.
Why load factor matters
Even a good hash function degrades if the table is too full.
For n stored entries and m buckets, the load factor is usually written as
For chaining, n may exceed m, so may exceed 1. For open
addressing, each slot contains at most one live entry, so ;
inserting a new key requires and a probe sequence that can actually
reach an empty slot.
Theorem/Proposition
Theorem
Counting identity for separate chaining
Let be the length of the chain stored at bucket , for . Then
Proof sketch or proof idea
Under separate chaining, the bucket chains partition the n stored entries:
every entry belongs to exactly one chain. Summing all chain lengths therefore
counts every entry once and gives ; dividing by m gives the
average occupancy .
This is a deterministic counting identity, not by itself a constant-time lookup guarantee. An expected bound such as additionally needs a suitable hash-function or key-distribution assumption. Without that assumption, even a small load factor can coexist with one long chain.
As the table becomes more heavily loaded:
- the exact average chain occupancy is , although individual chain lengths depend on how keys are distributed;
- open-addressing probe sequences tend to become longer as approaches
1, and probe-coverage conditions still matter; - average-case lookup moves away from the ideal constant-time picture unless the distribution and load remain controlled.
So resizing is not just a practical optimization. It is part of protecting the performance model promised by the data structure.
Generic pointers make the ADT reusable
The following C interface uses void * values to keep the ADT independent of
the concrete value type.
typedef struct hashtableCDT *hashtableADT;
hashtableADT EmptyHashtable();
void Enter(hashtableADT table, char *key, void *value);
void *Lookup(hashtableADT table, char *key);
The table does not need to know the concrete value type. It only stores the association between a key and an opaque pointer to some client-owned data.
That makes the same hashtable ADT usable for many different applications, as long as the client also knows how to interpret the returned pointer correctly.
Common mistakes
Common mistake
A collision does not mean two keys are equal
If Hash(key1) == Hash(key2), that only means the keys land in the same bucket.
It does not mean the keys are identical.
Common mistake
A valid hash range is necessary but not sufficient
A hash function that always returns a legal bucket index can still be bad if it clusters many unrelated keys into the same few buckets.
Common mistake
Average-case speed depends on keeping the table healthy
Saying “hash table lookup is O(1)” silently assumes a reasonable hash function and a controlled load factor. It is not a license to ignore collisions.
Quick checks
Checkpoint
What is a collision in hashing?
Answer in terms of keys and bucket indices.
Solution · Answer
A collision happens when two different keys are mapped to the same bucket index.
Checkpoint
Under chaining, what extra work happens after computing the bucket index?
Focus on where the implementation looks next.
Solution · Answer
It scans the linked list stored in that bucket until it finds the matching key or reaches the end of the chain.
Exercises
Checkpoint
Why does a hashtable still need key comparison after hashing?
Use collision reasoning, not only a slogan about speed.
Solution · Guided solution
Hashing only narrows the search to a bucket. Because different keys may collide into the same bucket, the implementation must still compare stored keys to the target key to confirm it found the correct entry.
Checkpoint
Explain one tradeoff between chaining and open addressing.
Answer in terms of where the collision-handling work is stored.
Solution · Guided solution
Chaining stores colliding entries in an auxiliary linked structure attached to the bucket, while open addressing keeps probing alternative slots inside the main table. Chaining uses extra pointer structure; open addressing relies more heavily on table occupancy and probe design.
Reading the implementation more closely
Read the ADT contract before examining the bucket layout. It specifies the operations that any storage strategy must implement.
Definition
Dictionary semantics of the table
A hash table is a dictionary structure. For one key, the table should support insertion, lookup, and deletion. If the same key is inserted again, the newer value replaces the older association instead of creating a second copy of the key.
That overwrite behavior is easy to miss, but it is one of the reasons the ADT is a dictionary rather than a multiset.
Worked example
Enter updates an existing key
Suppose a table already stores:
"cat" -> 3"dog" -> 8
If Enter(table, "cat", 9) is called again, the intended result is not two
copies of "cat". The association for "cat" is updated, so a later
Lookup(table, "cat") returns 9.
Enter therefore inserts a value for a specified key and overwrites the old
value if that key already exists.
Why key comparison still matters after hashing
Hashing narrows the search to a bucket, but it does not finish the job. Finding the right bucket does not yet establish that the requested key is present.
If two keys hash to the same bucket, the table still has to compare the stored key with the query key. Otherwise it would not know whether the match is exact or only accidental.
Worked example
A bucket can contain several unrelated keys
Assume bucket 3 contains:
("cow", value1) -> ("cat", value2) -> ("cod", value3)
If the lookup key is "cat", hashing only tells us to inspect bucket 3. The
implementation must still compare "cow", then "cat", before it can return
the correct value.
Chaining is flexible because it stores collision structure separately
The simplest collision strategy here is chaining. It is easy to reason about because each bucket has its own list of entries.
This gives the implementation a useful freedom: it can add new entries at the front of the list, the back of the list, or in another consistent order. The dictionary contract does not care about that local ordering as long as lookup and overwrite behave correctly.
Common mistake
Bucket order is not the dictionary contract
Students sometimes assume the order inside one chain is part of the meaning of the table. It is not. The contract is about finding the correct key-value pair; the chain order is only an implementation detail.
Worked example
Insertion and lookup under chaining
Consider a table with five buckets and a simple hash rule.
- Insert
"ape". - Insert
"ant". - Insert
"apple". - Look up
"ant".
If "ape" and "ant" collide, they will appear in the same bucket chain. The
lookup for "ant" hashes once, follows only that chain, and compares keys until
it finds the exact entry.
This is why chaining can still stay fast on average when the buckets remain reasonably balanced.
Open addressing keeps the search inside the table
Instead of attaching a list to each bucket, open addressing keeps searching for another slot inside the table itself.
The cost of a collision therefore moves into the probing rule.
- linear probing checks the next slot, then the next, and so on;
- quadratic probing jumps by square offsets;
- double hashing uses a second hash function to generate a step size.
Definition
Linear probing
With linear probing, if the preferred slot is full, the table checks the next slot in sequence and wraps around when necessary.
Linear probing tends to create long runs of filled buckets. That effect is called primary clustering, and it degrades the performance of the table because future probes are more likely to collide with the same dense region.
Quadratic probing is one response to that problem.
Definition
Quadratic probing
With quadratic probing, the ith probe uses a square offset such as i^2.
The step size grows faster than in linear probing, which helps reduce primary
clustering.
Worked example
Quadratic probing with a small table
Start with an empty table of size 10 and hash rule h(k) = k % 10. Insert
89, 18, 49, 58, 69 in that order, using the quadratic probes
for .
The probe sequence works as follows:
89goes to bucket918goes to bucket849hashes to9, collides, then probes bucket058hashes to8, collides, then probes9, then269hashes to9, collides, then probes0, then3
So the occupied buckets end up as:
0 -> 492 -> 583 -> 698 -> 189 -> 89
That example is useful because it shows that the probe rule, not just the raw hash value, determines where each entry finally lives.
Common mistake
Quadratic probing does not magically remove all limits
Quadratic probing reduces primary clustering, but it does not make the table
invincible. For example, with Nbuckets = 7 and F(i) = i^2, the probe
sequence repeats before reaching all
slots. Some buckets may never be visited under a given probe rule.
Worked example
Why the probe sequence can miss slots
For Nbuckets = 7, h0 = 0, and probes (h0 + i^2) % 7, taking
i = 0, 1, ..., 6 gives the sequence
0, 1, 4, 2, 2, 4, 1
The important point is not the arithmetic itself. The important point is that the probing rule can fail to cover every bucket. That is why open addressing needs a load-factor discipline and why the exact probing formula matters.
Definition
Double hashing
For a table of size , double hashing uses a second hash function to choose a step and probes
The probe sequence is guaranteed to cover all slots only when . Requiring only a nonzero step, or merely , is not sufficient.
For a table of size 10 with h(k) = k % 10 and second hash
, key 23 has step . Starting at bucket 3, the probes alternate
between 3 and 8 because ; if both are occupied, the search
cycles without reaching other empty slots. A well-chosen second hash can spread
probes better, but the table size and step rule must be designed together.
Rehashing is the cost of changing the table shape
When the bucket count changes, the reduction from a hash code to a bucket index changes too. Even if the underlying hash-code computation stays the same, every stored entry has to be placed again under the new bucket rule. That process is called rehashing.
Rehashing may be rare in a fixed-capacity implementation, but it is conceptually important because it explains why a growing hash table cannot be treated as a fixed pile of memory locations forever.
Worked example
Why resizing forces rehashing
If a table grows from one bucket count to another, the old bucket index is no longer guaranteed to be valid for the new table size. The entries must be visited again, their keys hashed again, and their new positions computed again.
That is why resizing is not just a matter of copying bytes. It changes the meaning of the bucket index itself.
How to choose between chaining and probing
No collision strategy is universally best. The useful comparison is the set of tradeoffs each strategy makes visible.
- Chaining is easier to explain and naturally handles collisions with a linked list.
- Linear probing keeps the table compact but can form primary clustering.
- Quadratic probing reduces that clustering but needs stricter load-factor control.
- Double hashing usually distributes probes better, but it is more expensive because it depends on two hash functions.
If you are reading code, the safest question is not “Which strategy is faster in abstract?” The safer question is “Which strategy matches the table size, the expected load, and the update pattern of this program?”
Summary
When you look at a hashtable implementation in C, use this compact checklist:
- What counts as the key, and what counts as the stored value?
- What does
Enterdo if the key already exists? - Does lookup compare the stored key after hashing?
- Where do collision results live: in a chain, in a probe sequence, or in both?
- What happens when the table gets too full?
If those five answers are clear, the rest of the implementation becomes much less mysterious.
More exercises
Checkpoint
Why does Enter need to overwrite an old value when the key already exists?
Answer from the dictionary contract, not from the code alone.
Solution · Guided solution
The ADT is a dictionary keyed by unique keys. If the same key is inserted again, the stored association should be updated. Otherwise the table would no longer have a single well-defined value for that key.
Checkpoint
What clustering problem does quadratic probing reduce compared with linear probing?
Name the clustering effect.
Solution · Answer
Quadratic probing reduces primary clustering, which is the tendency of linear probing to build long contiguous runs of filled buckets.
Checkpoint
Start with an empty table of size 10. Insert 89, 18, 49, 58, 69 in that order, using probes for . In which buckets do 49 and 69 land?
Use the probe sequence, not just the raw hash values.
Solution · Guided solution
49 hashes to 9, collides, and then lands in bucket 0. 69 hashes to
9, collides, then probes 0, then lands in bucket 3.
Checkpoint
How can double hashing improve probe distribution, and what extra computation does it require?
Focus on what extra ingredient it needs.
Solution · Guided solution
Double hashing usually gives better probe distribution, but it needs a second hash function. That extra function is the price of the improved probe sequence.