Evanalysis
1.2Estimated reading time: 19 min

1.2 Hash tables and collision strategies

Use dictionary operations, hash functions, collisions, and chaining to see why hash tables trade ordered structure for fast average-case access.

Course contents

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

KeysBucket index
cat4
dog6
cow0
cod2

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.

Hash-table collisions and correctness

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:

  1. hash the key to choose a bucket,
  2. 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:

  1. computes bucket 3,
  2. moves along the linked list in bucket 3,
  3. compares keys until "cat" is found,
  4. 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

α=nm.\alpha = \frac{n}{m}.

For chaining, n may exceed m, so α\alpha may exceed 1. For open addressing, each slot contains at most one live entry, so 0≤α≤10 \le \alpha \le 1; inserting a new key requires α<1\alpha < 1 and a probe sequence that can actually reach an empty slot.

Theorem/Proposition

Theorem

Counting identity for separate chaining

Let ℓj\ell_j be the length of the chain stored at bucket jj, for j=0,…,m−1j=0,\ldots,m-1. Then

∑j=0m−1ℓj=nand1m∑j=0m−1ℓj=nm=α.\sum_{j=0}^{m-1}\ell_j=n \qquad\text{and}\qquad \frac{1}{m}\sum_{j=0}^{m-1}\ell_j=\frac{n}{m}=\alpha.

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 ∑jℓj=n\sum_j \ell_j=n; dividing by m gives the average occupancy α\alpha.

This is a deterministic counting identity, not by itself a constant-time lookup guarantee. An expected bound such as O(1+α)O(1+\alpha) 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 α\alpha, although individual chain lengths depend on how keys are distributed;
  • open-addressing probe sequences tend to become longer as α\alpha 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.

  1. Insert "ape".
  2. Insert "ant".
  3. Insert "apple".
  4. 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 hi=(h(k)+i2) mod 10h_i=(h(k)+i^2)\bmod 10 for i=0,1,2,…i=0,1,2,\ldots.

The probe sequence works as follows:

  • 89 goes to bucket 9
  • 18 goes to bucket 8
  • 49 hashes to 9, collides, then probes bucket 0
  • 58 hashes to 8, collides, then probes 9, then 2
  • 69 hashes to 9, collides, then probes 0, then 3

So the occupied buckets end up as:

  • 0 -> 49
  • 2 -> 58
  • 3 -> 69
  • 8 -> 18
  • 9 -> 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 mm, double hashing uses a second hash function to choose a step s(k)s(k) and probes

hi=(h0+i s(k)) mod m.h_i=(h_0+i\,s(k))\bmod m.

The probe sequence is guaranteed to cover all mm slots only when gcd⁡(s(k),m)=1\gcd(s(k),m)=1. Requiring only a nonzero step, or merely 1≤s(k)≤m−11\le s(k)\le m-1, is not sufficient.

For a table of size 10 with h(k) = k % 10 and second hash s(k)=7−(k mod 7)s(k)=7-(k\bmod 7), key 23 has step s(23)=7−(23 mod 7)=5s(23)=7-(23\bmod 7)=5. Starting at bucket 3, the probes alternate between 3 and 8 because gcd⁡(5,10)=5\gcd(5,10)=5; 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:

  1. What counts as the key, and what counts as the stored value?
  2. What does Enter do if the key already exists?
  3. Does lookup compare the stored key after hashing?
  4. Where do collision results live: in a chain, in a probe sequence, or in both?
  5. 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 hi=(k+i2) mod 10h_i=(k+i^2)\bmod 10 for i=0,1,2,…i=0,1,2,\ldots. 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.

Practice

Work out your answer, then check it. You can revise and try again.

Loading…