SwadhyAI Notes

All DSA notes

DSA Hashing

Hash Maps

At n = 10⁵ line items per checkout, each “have we priced this SKU?” scan is Θ(n). Nested scans become ~10¹⁰ comparisons. A map from SKU → price turns each lookup into under a healthy .

A hash map stores a partial function from unique keys to values. Hashing narrows the search; equality finishes it. are normal. Worst case remains Θ(n) when the table degenerates.

What happens first?

1

Association contract

One current value per equality-distinct key.

2

Stable keys; equal ⇒ same hash.

3

Separate missing from stored null.

4

Average vs worst

Expected O(1); Θ(n) under collisions.

What actually happens

Naive idea: keep a list of (key, value) pairs and scan for each query. Correct. At n = 10⁵ with n queries, that is Θ(n²) work.

Better idea: hash the key to a bucket, resolve , confirm with equality, then read or replace the value. Keep bounded so expected chain/probe length stays constant. Resizes amortize into expected O(1) inserts.

The map still fails when keys mutate after insert, when you treat get-null as absence, when adversarial keys force Θ(n) probes, or when you need sorted ranges (use a tree map instead).

Invariant

Every live entry is reachable by hashing its key and following the collision policy; equal keys share at most one association. The holds for every resident key. Load stays in the regime the implementation promises for expected O(1).

How to choose a hash map

1. Exact key → value, order secondary? Hash map. Need sorted keys or ranges? Balanced tree map.

2. Many membership-only checks? A hash set may suffice. Need counts, indices, or payloads? Stay with a map.

3. Can you keep keys hash-stable? Prefer immutable keys. Concurrent compound updates need atomic compute/merge, not contains-then-put.

Quantitative check

Build a map of n = 10⁵ SKUs, then answer 10⁵ lookups. Expected work is roughly a few hash+equality steps per op (~10⁶), versus ~10¹⁰ for nested list scans. If all keys collide into one bucket, each lookup becomes Θ(n) and total work collapses back toward Θ(n²). Load factor and hash quality are the difference.

Logical model

key → value, with one current value per equality-distinct key

Typical API

put · get · containsKey · remove · compute

Expected update

O(1), amortized across occasional table resizes

Worst case

O(n) when collision handling degenerates

Interactive hash map demo

Add key-value pairs and watch how they are stored in buckets.

ℹ️ Ready to use Dictionary!
'Name'
'Zara'
Hash: 1
'Age'
'7'
Hash: 5
'Class'
'First'
Hash: 6

Association semantics, step by step

The bucket demo shows physical storage. This trace focuses on observable behavior: insert versus update, present-null versus missing, compute-style updates, and removal.

Collection semantics laboratory

What does each API operation promise?

Follow logical state changes independently of buckets, capacity, and collision strategy.

start with {}

Step 1 / 8

Logical map state

size = 0

Rows show associations, not a promised iteration order.

Return / effect

size = 0

Contract

A map stores at most one value for each equality-distinct key.

Presence and value are separate questions
QuerycontainsKeygetMeaning
Linusfalsenull / defaultmissing
Gracefalsenull / defaultmissing

Key → value association

Directory lookup

Given a name (the key), retrieve a phone number (the value) without scanning every entry. The structure promises expected constant-time access when load and hashing stay healthy.

Key"John Doe"
Value"555-1234"

Why use it?

  • Fast Lookups: O(1) average time to find a value.
  • Flexible Keys: Keys can be strings, numbers, or any hashable type whose equality and hash remain stable while stored.
  • Data Association: Perfect for representing real-world relationships (User ID → User Profile).

Limitations

  • Order varies: the abstract structure is unordered; concrete implementations may document insertion order.
  • Memory Overhead: Uses more memory than arrays to store the hash table structure.
  • Key Uniqueness: Keys must be unique; duplicates overwrite the previous value.

Map API: similar names, different contracts

OperationIf key is missingIf key is presentUse it when…
get(key)error, null, or None: API-specificreturns the associated valueyou only need to read
put(key, value)inserts; size growsreplaces the old valuethe new value is already known
containsKey(key)falsetrue, even for stored nullpresence and value must be separated
remove(key)no-op, false, or error: API-specificdeletes and may return old valuethe association must disappear
compute / mergederives an initial valueatomically derives an update in supporting mapscounting or grouping depends on old state

Keys obey the same equality–hash contract

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

A hash chooses where to search; equality decides whether a candidate is the same key. Equal keys must hash equally, but unequal keys may share a hash. Collisions are normal and must not overwrite one another.

Why mutable keys break lookup

If a key's hash-relevant fields change after insertion, it remains physically stored under its old hash. A lookup using its new hash visits a different location. Use immutable value objects, stable IDs, or defensive copies as keys.

Implementation

Python

python
from collections import defaultdict

words = ["to", "be", "or", "not", "to", "be"]

# Frequency map: missing keys start at zero.
counts = {}
for word in words:
    counts[word] = counts.get(word, 0) + 1

# Index/grouping map: missing keys start with a list.
positions = defaultdict(list)
for index, word in enumerate(words):
    positions[word].append(index)

# Presence is separate from the stored value.
cache = {"report": None}
assert cache.get("report") is None
assert cache.get("missing") is None
assert "report" in cache and "missing" not in cache

# Python dict preserves insertion order.
for key, value in counts.items():
    print(key, value)

Java

java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("to", "be", "or", "not", "to", "be");

        // LinkedHashMap matches Python dict's insertion-order iteration.
        Map<String, Integer> counts = new LinkedHashMap<>();
        for (String word : words) {
            counts.merge(word, 1, Integer::sum);
        }

        Map<String, List<Integer>> positions = new LinkedHashMap<>();
        for (int i = 0; i < words.size(); i++) {
            positions.computeIfAbsent(words.get(i), key -> new ArrayList<>())
                     .add(i);
        }

        Map<String, String> cache = new LinkedHashMap<>();
        cache.put("report", null);
        assert cache.get("report") == null;
        assert cache.get("missing") == null;
        assert cache.containsKey("report");
        assert !cache.containsKey("missing");

        for (Map.Entry<String, Integer> entry : counts.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

Missing is not the same as stored null

{"job": null}

Present, value is null

The key participates in size and iteration. It may mean “loaded, and no result.”

{}

Key is absent

There is no association. In a cache this might mean “not loaded yet.”

containsKey(key)

Ask the right question

Use a presence test or found flag. An Option/Optional works when null is not a legal payload; otherwise use a distinct sentinel or a result that carries presence separately.

API details matter: Java HashMap.get returns null in both cases, while Python dict[key] raises for absence. Java's computeIfAbsent also treats a null mapping as absent.

Patterns that naturally produce a map

Frequency

item → count

Anagrams, mode, sliding-window validity, multiset comparison

Index

value → index / indices

Two-sum complements, inverted indexes, last-seen positions

Grouping

group key → list

Anagram buckets, adjacency lists, records partitioned by owner

Memoization

state → answer

Top-down DP and expensive pure-function caching

Prefix-state lookup

prefix sum → earliest index / count

Subarray sums, especially when negative values prevent two pointers

Entity cache

stable ID → object

Identity maps, request deduplication, database hydration

Iteration order is a separate feature

Map familyOrder guaranteeCore costChoose it for
Hash mapImplementation-specific or unspecifiedExpected O(1)Fast general lookup
Insertion-ordered hash mapFirst insertion order; update rules varyExpected O(1)Stable serialization and predictable iteration
Access-ordered hash mapMost/least recently accessedExpected O(1)LRU bookkeeping
Balanced-tree mapSorted by key/comparatorO(log n) worst caseRange queries, floor/ceiling, ordered traversal

Python dictionaries preserve insertion order. Java HashMap does not; LinkedHashMap adds insertion/access order, while TreeMap provides sorted keys. Rely only on guarantees documented by the exact implementation.

Complexity ledger

OperationExpected / amortizedWorst caseNotes
get / containsKeyO(1)O(n)Hash plus probe/chain equality checks
put / updateO(1) amortizedO(n)May trigger an O(n) resize/rebuild
removeO(1)O(n)May repair a probe cluster or unlink a chain node
iterate entriesΘ(n)Θ(n + capacity)Open-addressed tables may scan empty slots
build n mappingsO(n)O(n²)Assumes expected-constant hashing per insertion

These bounds treat key hashing/equality as constant-time. Hashing a long uncached string or comparing large composite keys adds their own cost.

Caching and concurrency require stronger contracts

A map is storage, not a complete cache

  • Define eviction: capacity, LRU/LFU, TTL, or explicit invalidation.
  • Define whether null means a cached negative result or absence.
  • Prevent cache stampedes when many callers miss the same key.
  • Account for mutable cached values and memory retention.

Compound operations must be atomic

if missing → compute → put is a race even if each individual operation is thread-safe. Use the concurrent map's atomic compute, merge, or putIfAbsent contract, or protect the whole transition with a lock. Iteration may be snapshot, fail-fast, or weakly consistent; verify the API. Java's ConcurrentHashMap, for example, rejects null keys and values, which removes missing-versus-null ambiguity.

Common failure modes

  • Mutable keys: changing a hash-relevant field makes the mapping unreachable by normal lookup.
  • Accidental overwrite: put on an existing key updates; use put-if-absent when replacement is a bug.
  • Shared mutable defaults: create a fresh list/set per key when grouping.
  • Order assumptions: deterministic-looking output is not a guarantee.
  • Adversarial collisions: expected O(1) can become O(n); runtime defenses and key design matter.
  • Oversized keys: composite/string hashing and equality may dominate the table operation.

Exercises

  1. A cache stores report → null meaning “loaded, empty.” How do you distinguish that from a missing key in Java HashMap vs Python dict?
  2. n = 10⁵ inserts into a table that never resizes (tiny fixed capacity). What happens to average probe length as load factor grows past 1 for chaining? For open addressing?
  3. Construct a failure timeline: insert a mutable key, change a hash field, then lookup. Where is the entry physically, and what does containsKey return?

Primary sources

  • Cormen et al., Introduction to Algorithms (CLRS): hash tables, chaining, open addressing, load factor.
  • Java Map / HashMap documentation: null keys/values, compute, and iteration contracts.
  • Python data model: __hash__ / __eq__ and dict insertion-order guarantees.

Takeaway

Use a hash map for fast key → value association when order and ranges are secondary. Expected O(1) assumes healthy load and hashing; worst case is Θ(n). Define absence explicitly, keep keys hash-stable, and choose an ordered or concurrent variant when those guarantees are required.