SwadhyAI Notes

All DSA notes

DSA Hashing

Hash Tables

Expected O(1) is correct under the model. The adversarial key set is not. Dual conflict: the API promises fast exact-key indexing, while collisions, load, and rebuilds decide whether that promise holds.

A hash table converts a key into a candidate location, applies , and confirms equality. bounds are not worst-case guarantees.

What happens first?

1

Lookup pipeline

Hash → reduce to [0, m) → resolve → equality.

2

Keep α in the regime your strategy tolerates.

3

Chaining, probing, or displacement?

4

Migrate live entries when capacity changes.

What actually happens

Naive idea: store entries in an array and scan. Correct, Θ(n) per lookup. Or hope hashing alone gives unique slots: impossible once the key domain exceeds capacity.

Better idea: hash, place into m slots, resolve collisions, grow when threatens long probes. Under a good hash and controlled α, operations are expected Θ(1); resizes amortize.

The design still fails on mutable keys, cleared probe slots without , copied slots during resize, hash flooding, and treating expected O(1) as a hard latency SLO.

Invariant

Every live key is reachable by replaying hash + collision policy + equality. Deleted open-address slots leave markers so later keys remain findable. Equal keys share one logical entry; unequal colliding keys never overwrite each other.

How to choose a hash table

1. Exact-key lookup dominates? Hash table. Sorted/range queries? Balanced tree. Dense small integers? Array or bitset.

2. Deletion-heavy or cache-sensitive? Chaining simplifies delete; linear probing is compact and cache-friendly but clusters and needs careful deletion.

3. Attacker-controlled keys? Prefer keyed/randomized hashing and collision defenses. Expected O(1) is not a security claim.

Quantitative check

Idealized chaining with α = 0.75: expected bucket length ~0.75, so a few equality checks per lookup. Same n with m fixed at 16 while n grows to 10⁴: α ≈ 625, expected scan length hundreds of entries. Birthday bound: with m = 365, n ≈ 23 already yields ~50% chance of at least one collision.

Hash

key → integer

Compress an arbitrarily rich key into a repeatable, well-mixed code.

Index

code → [0, m)

Map the code into the current table capacity; changing m usually changes placement.

Resolve

hash + equality

A collision narrows the search. It never proves that two keys are equal.

The lookup pipeline

1

Key semantics

Use the key type's documented equality; normalize only when the API defines it.

2

Hash

Compute a code consistent with that equality relation.

3

Reduce

Choose a home bucket or slot from capacity m.

4

Resolve

Follow a bucket or deterministic probe sequence.

5

Confirm

Use key equality before returning or updating a value.

Hash codes are filters, not identities. Equal keys must produce the same hash while stored. Unequal keys may produce the same hash, so skipping the final equality check is incorrect.

Interactive collision-strategy lab

Insert the same home indices using chaining, linear probing, and Robin Hood displacement. This isolates how collision policy changes layout, probe cost, and invariants.

Collision-strategy laboratory

Colliding entries share a bucket-owned collection.

Empty table

Step 1 / 9

home(oak) = 1home(elm) = 1home(ash) = 2home(fir) = 1

Keys committed

0 / 4

Load factor α = n/m

0/7 = 0.00

Bucket / slot probes

0

bucket 0

empty

bucket 1

empty

bucket 2

empty

bucket 3

empty

bucket 4

empty

bucket 5

empty

bucket 6

empty

Current action

Each array slot owns a bucket that can hold zero or more entries.

Invariant

Every stored key appears in exactly the bucket selected by its home index; collisions coexist inside that bucket.

Colliding insertions so far: 0. Chaining counts an insertion whose home bucket was already occupied; probing counts each occupied slot encountered. Home indices stay fixed so the strategy, not the hash function: remains the changing variable.

The equality and hashing contract

Required implication

a == b ⇒ hash(a) == hash(b)

The converse is deliberately false: matching hashes identify a collision candidate, not equality. Equality should also be reflexive, symmetric, and transitive for ordinary key behavior.

Stability while resident

bucket(key) must not drift

If equality-relevant key state changes after insertion, lookup may search a different bucket and fail to find an object that is physically still present. Prefer immutable keys or remove–mutate–reinsert.

Process randomization

Some runtimes deliberately vary hashes between processes. Never persist an ordinary runtime hash as a durable identifier or wire format.

Composite keys

Hash every field used by equality, with type-appropriate mixing. Omitting a field may be valid but creates avoidable collisions.

Floating and textual keys

Define NaN, signed zero, case folding, Unicode normalization, and locale behavior before choosing equality and hashing.

Collisions are normal

A finite table receives keys from a much larger domain, so collisions are unavoidable. Under an idealized independent uniform model, the chance of at least one collision after n insertions into m buckets is approximately:

1 − exp(−n(n−1) / 2m)

Birthday approximation; a model, not a guarantee for a particular hash function or key set.

Design for collisions instead of hoping to eliminate them.

A good policy preserves reachability after insert and delete, bounds load before probes become pathological, and still uses equality to distinguish colliding keys.

Collision-resolution families

Separate chaining

Each array entry owns a bucket collection. A lookup hashes once, selects one bucket, then compares keys inside it. With uniform spreading, expected bucket length is the load factor α = n/m; α may be greater than one.

Terminal
0: []
1: [(oak, 8), (elm, 3), (fir, 5)]
2: [(ash, 2)]
3: []

Buckets can be arrays, linked nodes, compact blocks, or: under implementation-specific conditions: another search structure. “Chaining” does not require a linked list.

Open addressing

Every live entry occupies the slot array itself. Lookup must replay the same probe sequence as insertion. A never-used empty slot proves absence; a tombstone does not, because the desired key may lie beyond it.

Linear

Great locality, but contiguous clusters accumulate.

Quadratic

Offsets reduce primary clustering; coverage depends on the formula and m.

Double hash

A key-specific stride reduces shared paths; the stride must traverse the table adequately.

Displacement schemes

Robin Hood insertion evens out displacement by allowing farther-traveled entries to take slots from nearer-home occupants. Cuckoo-style designs give each key a small candidate set, making lookup short but allowing insertion evictions, cycles, stashes, and rehashes.

No strategy dominates universally. Payload size, cache layout, deletion frequency, maximum latency, attack model, and implementation complexity all matter.

Hash-table collision strategies and trade-offs
StrategyPlacement ruleStrengthEngineering cost
Separate chainingEach slot owns a collection of entriesEasy deletion; α may exceed 1Extra indirection and per-bucket/node overhead
Linear probingTry h, h+1, h+2, … modulo mCompact and cache-friendlyPrimary clustering; tombstones or careful backshift deletion
Quadratic probingUse a growing nonlinear offsetReduces primary clusteringCapacity and probe formula must guarantee adequate coverage
Double hashingA second hash chooses the step sizeBetter probe dispersionStep must be compatible with capacity to visit enough slots
Robin HoodSwap when the incoming probe distance is largerReduces variance in displacementInsertion/deletion logic and metadata are more involved
Cuckoo familyEach key has a small set of candidate locationsVery short lookup pathInsertion can evict repeatedly, cycle, use a stash, or trigger rehash

Load factor and why resize thresholds differ

Chaining

α = n / buckets

Expected bucket work grows with α, but the representation can remain valid above 1.

Open addressing

α = live / slots < 1

Insertion needs a reusable slot. An unsuccessful lookup stops at NEVER_USED or after examining at most m slots.

Tombstones

effective load ≥ live load

Deleted markers lengthen probes even though they hold no live key; rebuilding may be triggered by occupancy plus tombstones.

There is no universal 0.75 threshold. The strategy, probe rule, memory budget, key distribution, and latency target determine when to grow, clean tombstones, or shrink.

Resizing is relocation, not array copying

Because the index depends on capacity, entries must be reinserted or otherwise migrated when m changes. Copying old slots to the same numeric positions can break lookup.

1

Allocate

Choose a new capacity compatible with the reduction and probe rules.

2

Rehash / migrate

Place every live entry according to the new capacity; omit tombstones.

3

Publish

Switch the table root and retire the old storage only when readers can no longer use it.

Geometric growth makes insertion expected amortized Θ(1), but one conventional rebuild is still Θ(n). Incremental migration can spread that work across operations, trading simpler pauses for two-table lookup state and more complex correctness.

Didactic implementations

Production tables add layout specialization, cached hashes, optimized probing, randomized seeds, concurrency policies, and rigorous exception handling. These examples isolate the core contracts.

Chaining map

Python

python
class HashTable:
    def __init__(self, capacity=8):
        if capacity <= 0:
            raise ValueError("capacity must be positive")
        self.buckets = [[] for _ in range(capacity)]
        self.size = 0

    def _bucket(self, key):
        return self.buckets[hash(key) % len(self.buckets)]

    def get(self, key):
        for stored_key, value in self._bucket(key):
            if stored_key == key:
                return value
        raise KeyError(key)

    def put(self, key, value):
        bucket = self._bucket(key)
        for index, (stored_key, _) in enumerate(bucket):
            if stored_key == key:
                bucket[index] = (key, value)  # update, do not duplicate
                return

        if (self.size + 1) / len(self.buckets) > 0.75:
            self._resize(2 * len(self.buckets))
            bucket = self._bucket(key)
        bucket.append((key, value))
        self.size += 1

    def delete(self, key):
        bucket = self._bucket(key)
        for index, (stored_key, value) in enumerate(bucket):
            if stored_key == key:
                bucket.pop(index)
                self.size -= 1
                return value
        raise KeyError(key)

    def _resize(self, capacity):
        entries = [entry for bucket in self.buckets for entry in bucket]
        self.buckets = [[] for _ in range(capacity)]
        self.size = 0
        for key, value in entries:
            self.put(key, value)

Java

java
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;

final class ChainedHashTable<K, V> {
    record Entry<K, V>(K key, V value) {}

    private List<List<Entry<K, V>>> buckets;
    private int size;

    ChainedHashTable(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException("capacity must be positive");
        buckets = emptyBuckets(capacity);
    }

    private List<List<Entry<K, V>>> emptyBuckets(int capacity) {
        List<List<Entry<K, V>>> result = new ArrayList<>(capacity);
        for (int i = 0; i < capacity; i++) result.add(new ArrayList<>());
        return result;
    }

    private int index(K key) {
        return Math.floorMod(Objects.hashCode(key), buckets.size());
    }

    V get(K key) {
        for (Entry<K, V> entry : buckets.get(index(key))) {
            if (Objects.equals(entry.key(), key)) return entry.value();
        }
        throw new NoSuchElementException(String.valueOf(key));
    }

    void put(K key, V value) {
        List<Entry<K, V>> bucket = buckets.get(index(key));
        for (int i = 0; i < bucket.size(); i++) {
            if (Objects.equals(bucket.get(i).key(), key)) {
                bucket.set(i, new Entry<>(key, value));
                return;
            }
        }
        if ((size + 1.0) / buckets.size() > 0.75) {
            resize(2 * buckets.size());
            bucket = buckets.get(index(key));
        }
        bucket.add(new Entry<>(key, value));
        size++;
    }

    V remove(K key) {
        List<Entry<K, V>> bucket = buckets.get(index(key));
        for (int i = 0; i < bucket.size(); i++) {
            if (Objects.equals(bucket.get(i).key(), key)) {
                V value = bucket.remove(i).value();
                size--;
                return value;
            }
        }
        throw new NoSuchElementException(String.valueOf(key));
    }

    private void resize(int capacity) {
        List<List<Entry<K, V>>> old = buckets;
        buckets = emptyBuckets(capacity);
        size = 0;
        for (List<Entry<K, V>> bucket : old) {
            for (Entry<K, V> entry : bucket) put(entry.key(), entry.value());
        }
    }
}

Open-address deletion states

Terminal
NEVER_USED: lookup may stop: no insertion crossed this slot
OCCUPIED(k): compare k using equality
TOMBSTONE: lookup must continue; insertion may reuse carefully
 
delete(k):
replay k's probe sequence
stop with "absent" at NEVER_USED
after m examined slots, report absent / table exhausted
when equality finds k, write TOMBSTONE
 
# Some linear-probing variants use backward-shift deletion instead.
# That proof is strategy-specific; blindly clearing a slot is wrong.

Complexity, with the assumptions visible

Let n be live entries and m be allocated buckets or slots. The simplified linear worst cases below assume a maintained load regime with m = Θ(n). If capacity remains much larger than the live set, or tombstones occupy most non-live slots: an open-addressed scan is Θ(m), and initializing a replacement table also costs Θ(mnew).

Expected, amortized, and worst-case hash-table costs
OperationTypical modelWorst caseWork performed
LookupExpected Θ(1)Θ(n)Hash, inspect the candidate bucket/probe sequence, then test key equality.
Insert or update, no rebuildExpected Θ(1)Θ(n)Search for an equal key, then place or replace one entry; rebuild cost is listed separately.
DeleteExpected Θ(1)Θ(n)Chaining unlinks an entry; open addressing must preserve the probe-search contract.
Resize / rehashExpected Θ(n + m_new)O(n · m_new + m_new)Initialize new storage and place every live entry; degenerate probing can compound.
IterateΘ(n + m) in a basic tableΘ(n + m)Some implementations scan m buckets/slots as well as n live entries.

Expected Θ(1) assumes a suitable hash family or distribution, controlled load, and ordinary equality cost. Hashing a length-L string itself costs Θ(L) unless a stable code is cached, so “constant time” is relative to key representation too.

Security and adversarial keys

Hash flooding

Chosen keys can force long chains or probes when the hash behavior is predictable, turning expected constant work into linear work.

Mitigations

Keyed/randomized hashing, collision-aware fallback structures, input limits, time budgets, and capacity controls reduce exposure; no single technique fits every key type.

Hash choice

Fast non-cryptographic hashes suit trusted in-memory keys. Attacker-controlled keys may need a keyed denial-of-service-resistant function; this is different from password hashing.

Concurrency changes the contract

What must be defined

Are get, put-if-absent, compute, iteration, and resize linearizable? Can iterators be weakly consistent? Which compound actions are atomic?

Implementation families

A global lock is simple; striping or sharding increases independent progress; read-copy-update and CAS-based designs need careful resize publication and memory reclamation. One atomic instruction does not make an entire table lock-free.

Correctness invariants and failure modes

Inconsistent equality and hash

Derive both from the same immutable logical fields.

Clearing a probed slot on delete

Use a tombstone or a proven shift algorithm so later keys remain discoverable.

Copying slots during resize

Recompute placement under the new capacity and probe state.

Counting updates as new keys

Confirm equality and replace the existing value without incrementing size.

Relying on unspecified iteration order

Choose an ordered map or sort keys when order is part of the result.

Using runtime hashes as persistent IDs

Choose a documented stable digest or serialization-specific identifier instead.

Ignoring key hash cost

Include key length and equality work in complexity for strings, tuples, and composite objects.

Assuming thread-safe compound actions

Use an atomic API such as put-if-absent rather than a racy contains-then-put sequence.

How to choose

Choose a hash table when exact-key lookup dominates, ordering is secondary or supplied separately, and you can define stable equality and hashing. Choose a balanced search tree for sorted/range queries and worst-case logarithmic bounds, an array/bitset for a small dense integer universe, or a trie when prefix structure is part of the query.

Exercises

  1. With chaining and α = n/m = 2, what is the expected bucket length under uniform hashing? If every key hashes to the same bucket, what are lookup costs?
  2. Open addressing, α = 0.9, many deletes leaving tombstones. Why can unsuccessful lookups get slower even though live n is small? What restores short probes?
  3. Show why copying slots unchanged into a doubled array breaks lookup for a key that used hash % m. What must resize do instead?

Primary sources

  • Cormen et al., Introduction to Algorithms (CLRS): hashing, chaining, open addressing, universal hashing.
  • Knuth, The Art of Computer Programming, Vol. 3: searching, clustering, and deletion in open-address tables.
  • Pagh & Rodler, cuckoo hashing paper; classic references on Robin Hood hashing for displacement schemes.

Takeaway

Hash tables deliver expected constant-time exact-key indexing when load, hashing, and collision policy cooperate. Average is not worst case: collisions, tombstones, and adversarial keys can force linear work. Resize remaps; it does not memcpy slots.