// STUHUB · C++ DATA STRUCTURES

Hash Tables: Hashing, Collisions, Load Factor, and Open Addressing vs Chaining

How hash tables turn a key into an array index, what happens when two keys collide, why the load factor triggers a rehash, and the exam-answer comparison of chaining versus open addressing.

Introduction

A hash table stores key–value pairs and answers find, insert, and erase in O(1) average time by mapping each key to an array slot with a hash function. The whole subject is really about one problem: two different keys can map to the same slot (a collision), and every design decision — chaining, open addressing, load factor, rehashing — is a strategy for surviving that fact. This note walks the pipeline from key to slot, then the two collision strategies exams compare.

From key to slot: the pipeline

The path from a key to a bucket has three stages:

  1. Hash: the key is converted to an integer. For strings, a polynomial rolling hash (h = h·31 + c) is the classic exam example; for integers, the identity is common but poor for structured keys.
  2. Compress: the integer is squeezed into the table's index range, almost always with modulo: index = h(key) % capacity.
  3. Resolve: if that slot is already occupied by a different key, apply the collision strategy.

A good hash function has two properties: it distributes keys uniformly (no slot systematically favored), and it is deterministic (same key, same slot, every time — otherwise lookups break immediately). C++'s std::hash provides stage 1 for built-in types; the container handles 2 and 3.

cpp
// The classic exam-sized version: string key, int value, chaining.
struct Node {
    std::string key;
    int value;
    Node* next;
};

class SimpleHashMap {
    static const int CAPACITY = 101;      // prime reduces clustering
    Node* buckets[CAPACITY] = {};

    static size_t hash(const std::string& s) {
        size_t h = 0;                     // polynomial rolling hash
        for (char c : s) h = h * 31 + static_cast<unsigned char>(c);
        return h;
    }
public:
    void put(const std::string& k, int v) {
        size_t i = hash(k) % CAPACITY;
        for (Node* p = buckets[i]; p; p = p->next)
            if (p->key == k) { p->value = v; return; }   // update
        buckets[i] = new Node{k, v, buckets[i]};          // prepend
    }
    bool get(const std::string& k, int& out) const {
        for (Node* p = buckets[hash(k) % CAPACITY]; p; p = p->next)
            if (p->key == k) { out = p->value; return true; }
        return false;
    }
};

Collision strategy 1: separate chaining

Each slot holds a pointer to a linked list of all keys that hashed there. Insertion prepends to the list (O(1)); lookup walks the list comparing keys with ==.

Why it works: collisions stop being a problem and become ordinary list entries. The cost: average lookup is O(1 + α) where α is the load factor (n / capacity) — with a good hash, lists stay short and this is effectively constant. The worst case: all keys collide into one slot, degrading to O(n) — the pathological case every exam asks you to name (usually caused by a constant hash function or adversarial keys).

Collision strategy 2: open addressing

No lists: every entry lives in the table itself. On collision, probe for another slot. The three probe sequences to know:

  • Linear probing: index = (h + i) % capacity for i = 0, 1, 2, ... — cache-friendly but suffers primary clustering (long runs of occupied slots grow longer).
  • Quadratic probing: index = (h + i²) % capacity — breaks up runs, but suffers secondary clustering among keys with the same base hash.
  • Double hashing: index = (h1 + i·h2(k)) % capacity — the second hash gives each key its own stride; fewest clustering problems.

Deletion is the classic trap: emptying a slot breaks the probe chain of later keys, so you mark it with a tombstone instead of clearing it. That is open addressing's signature complexity in practice.

Load factor and rehashing

The load factor α = n / capacity measures how full the table is. For chaining, average cost degrades gracefully with α; for open addressing, performance falls off a cliff as α → 1 because probe sequences lengthen fast. Both strategies therefore pick a threshold (commonly 0.75) and rehash when it is exceeded: allocate a bigger array (typically double), then re-insert every element — its index changes because h(key) % capacity changed.

Rehashing is O(n), but amortized over the n insertions that caused it, insertion remains O(1) average — the same accounting trick as a dynamic array's growth.

Chaining vs open addressing: the exam table

CriterionSeparate chainingOpen addressing
Extra memoryPointer per entry + list nodesNone beyond the table
Worst-case lookupO(n) (all collide)O(n) (table nearly full)
Performance at α = 0.75Fine (short lists)Degrading (long probes)
DeletionTrivial (unlink)Needs tombstones
Cache behaviorPoor (pointer chasing)Good (contiguous)

std::unordered_map is specified as a hash table with chaining and O(1) average operations — which is why the standard answer to "which container for O(1) lookup by key" is unordered_map, while "sorted order iteration" points back to std::map (a balanced BST, O(log n)).

Exercises with solutions

Work through each question before opening the solution below it.

Exercise 1

Insert the keys 4, 17, 30 into a hash table of capacity 7 using h(k) = k % 7 with separate chaining. Show the final table. Then insert 24 — what happens?

Solution

4 % 7 = 4 → bucket 4. 17 % 7 = 3 → bucket 3. 30 % 7 = 2 → bucket 2.

Buckets: 0:— 1:— 2:[30] 3:[17] 4:[4] 5:— 6:—

24 % 7 = 3 → collision with 17; chaining appends to bucket 3, which becomes [17 → 24] (or [24 → 17] if prepending). Nothing is overwritten — the two keys coexist in the list, which is exactly what chaining guarantees.

Exercise 2

Same keys (4, 17, 30, then 24) with linear probing, capacity 7. Show the probe sequences.

Solution

4 → slot 4 free → placed at 4. 17 → slot 3 free → placed at 3. 30 → slot 2 free → placed at 2. 24 → slot 3 occupied (17) → probe (3+1)%7 = 4 occupied (4) → probe 5 free → placed at 5.

Note how 24 probed past two different keys — under linear probing a collision cluster absorbs unrelated keys, which is primary clustering in miniature.

Exercise 3

A table using open addressing currently holds a key at slot 9 that came from a probe starting at 7. You erase slot 8 (a different key) by setting it to empty. Why might a later lookup for the slot-9 key now fail, and what is the standard fix?

Solution

The slot-9 key is only reachable by the probe 7 → 8 → 9. Making slot 8 truly empty cuts the chain: a lookup probes 7 (wrong key), then 8 (now empty) and concludes absent — a false negative. The standard fix is a tombstone: mark slot 8 as deleted rather than empty, so probes continue past it while insertions may reuse it. Periodically, a rehash clears accumulated tombstones.

Exercise 4

Your hash table with chaining has capacity 10 and currently holds 90 keys. What is the load factor, what does it imply for performance, and what action does the design call for?

Solution

α = 90/10 = 9. Average successful lookup costs O(1 + α) ≈ 10 key comparisons — no longer constant in practice. The design calls for a rehash: allocate a larger table (commonly 2× or the next prime near that), recompute each key's index with the new capacity, and re-insert; α drops to ≈4.5 immediately and to ≈2.25 after doubling twice. The O(n) rehash is amortized against the insertions that filled the table.

Published and updated 2026-08-09 · DUOCODE TECHNOLOGY