On This Page

What Is a Hash Map?

A hash map (also called a hash table or dictionary) stores key-value pairs โ€” you associate a key (like a username) with a value (like their profile data). You give it a key, it gives you the value โ€” in O(1) average time. That's the magic.

Imagine a library where instead of searching every shelf, you have a formula that tells you exactly which shelf a book is on based on its title. You skip straight there. That formula is the hash function.

A hash set is the same idea, but it only stores keys โ€” no values. It answers one question: "Is this element in the set?" Python's set and JavaScript's Set are hash sets.

A Brief History

The hash table was invented in 1953 by Hans Peter Luhn at IBM, who described it in an internal memo. Around the same time (1953), Gene Amdahl, Elaine McGraw, and Arthur Samuel independently implemented hash tables for an IBM 701 assembler. The technique was published more broadly by Arnold Dumey in 1956.

The reason this was a breakthrough: before hash tables, the fastest way to look up a value by key was a balanced binary search tree โ€” O(log n). Hash tables offered O(1) average-case lookup. For a million entries, that's the difference between ~20 comparisons and 1. The trade-off? You need a good hash function, and worst-case performance is O(n). But the average case is so good that hash tables became the most widely used data structure in all of software engineering.

Key Concepts

Hash Function โ€” takes a key (string, number, whatever) and converts it to an integer. That integer, taken modulo the array size, becomes the index (also called a bucket) where the value is stored. A good hash function distributes keys evenly across indices, minimizing collisions.

Collision โ€” when two different keys hash to the same index. This will happen (the pigeonhole principle guarantees it: if you have more possible keys than array slots, some must share). How you handle collisions defines the hash map's behavior.

Load Factor โ€” the ratio of stored items to array size (n/capacity). When it gets too high (typically > 0.75), performance degrades because collisions increase. The hash map resizes โ€” doubles the array and rehashes every existing key. That single resize is O(n), but it happens rarely enough that insertions are still amortized O(1) (the expensive operation's cost is "amortized" โ€” spread across all the cheap operations).

Where You'll See Them

How It Works

The Hash Function

A hash function takes a key and produces an index. For a string key like "cat":

  1. Convert each character to its ASCII value: c=99, a=97, t=116
  2. Combine them using a polynomial rolling hash: 99 ร— 31ยฒ + 97 ร— 31 + 116 = 98,194 (using prime 31 as multiplier)
  3. Modulo the table size: 98194 % 16 = 2
  4. Store at index 2

Why prime numbers? They distribute hash values more evenly. If you used powers of 2 as multipliers, the lower bits of your keys would dominate, causing clusters. The prime 31 is used by Java's String.hashCode() and has become the de facto standard โ€” it's small enough to be fast (multiplying by 31 is the same as (x << 5) - x, which the CPU optimizes to a shift and subtract) and large enough to spread values well.

Collision Resolution

Chaining (Separate Chaining) โ€” each bucket holds a linked list (or, in modern implementations, a dynamic array). When two keys hash to the same index, both go into that bucket's list. To find a key, hash it, go to that bucket, then walk the list. If the load factor stays low, these lists are short (1-2 items on average).

Open Addressing โ€” instead of lists, when a collision happens, you probe for the next empty slot:

๐Ÿ’ก Chaining vs Open Addressing

Java's HashMap uses chaining (and switches from a linked list to a red-black tree when a bucket exceeds 8 items). Python's dict uses open addressing with a custom probing sequence. Both work well in practice โ€” chaining is simpler to implement and handles high load factors gracefully; open addressing has better cache locality (CPU-friendly memory access patterns) since everything lives in one contiguous array.

Resizing

When the load factor crosses the threshold (0.75 in most implementations), the map allocates a new array โ€” usually double the size โ€” and rehashes every existing key. Every key gets a new index because the modulo value changed (hash % 16 vs hash % 32 gives different results). This single resize is O(n) for that one operation, but since it only happens after n/2 insertions, the cost per insertion averages out to O(1).

This is why hash maps use powers of 2 for capacity โ€” modulo with a power of 2 is just a bitwise AND, which is extremely fast: hash % 16 is the same as hash & 15.

Hash Set Internals

A hash set is literally a hash map where you only care about keys. The value is either absent or a dummy placeholder. Same hash function, same collision handling, same resizing. When you call set.add(x), it's doing map[x] = true under the hood.

Interactive Visualization

Watch the hash function in action. Put a key-value pair, see which bucket it lands in, and observe collisions creating chains.

Hash Map Explorer

Operations & Complexity

Hash Map

OperationAverageWorstNotes
Put (insert/update)O(1)O(n)Worst case: all keys hash to same bucket
Get (lookup)O(1)O(n)Same reason โ€” degenerate chain
DeleteO(1)O(n)Find + remove from chain
Contains KeyO(1)O(n)Same as get
ResizeO(n)O(n)Rehash everything โ€” happens rarely
Iterate allO(n)O(n)Must visit every bucket

Hash Set

OperationAverageWorstNotes
AddO(1)O(n)Same as map put (key only)
ContainsO(1)O(n)Hash + check bucket
RemoveO(1)O(n)Hash + remove from chain
UnionO(n+m)โ€”Iterate both sets
IntersectionO(min(n,m))โ€”Iterate smaller, check in larger
โš ๏ธ Worst Case is Real

The O(n) worst case isn't just theoretical. If an attacker can control the keys (e.g., HTTP request parameters), they can craft inputs where every key hashes to the same bucket, turning your O(1) map into an O(n) linked list. This is called a hash collision attack (also known as HashDoS). Python and Java randomize hash seeds at startup to prevent this. This is why Python's hash() returns different values across runs.

Implementation

Hash Map with Chaining

Python
class HashMap:
    def __init__(self, capacity=16):
        self._capacity = capacity
        self._size = 0
        # Each bucket is a list of (key, value) pairs
        self._buckets = [[] for _ in range(capacity)]
        self._load_threshold = 0.75

    def _hash(self, key):
        # Use Python's built-in hash, then constrain to our array size
        return hash(key) % self._capacity

    def put(self, key, value):
        if self._size / self._capacity >= self._load_threshold:
            self._resize()

        idx = self._hash(key)
        bucket = self._buckets[idx]

        # Check if key already exists โ€” update if so
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)
                return

        # New key โ€” append to chain
        bucket.append((key, value))
        self._size += 1

    def get(self, key, default=None):
        idx = self._hash(key)
        for k, v in self._buckets[idx]:
            if k == key:
                return v
        return default

    def delete(self, key):
        idx = self._hash(key)
        bucket = self._buckets[idx]
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket.pop(i)
                self._size -= 1
                return True
        return False

    def contains(self, key):
        """Check if key exists โ€” O(1) average."""
        idx = self._hash(key)
        return any(k == key for k, v in self._buckets[idx])

    def keys(self):
        """Iterate all keys โ€” O(n)."""
        for bucket in self._buckets:
            for k, v in bucket:
                yield k

    def _resize(self):
        # Double capacity and rehash everything
        old_buckets = self._buckets
        self._capacity *= 2
        self._buckets = [[] for _ in range(self._capacity)]
        self._size = 0
        for bucket in old_buckets:
            for key, value in bucket:
                self.put(key, value)


# Usage
m = HashMap()
m.put("name", "Alice")
m.put("age", 30)
print(m.get("name"))    # "Alice"
m.delete("age")
print(m.get("age"))     # None
JavaScript
class HashMap {
  constructor(capacity = 16) {
    this._capacity = capacity;
    this._size = 0;
    this._buckets = Array.from({ length: capacity }, () => []);
    this._loadThreshold = 0.75;
  }

  _hash(key) {
    // Simple string hash โ€” multiply-and-add with prime 31
    let h = 0;
    const str = String(key);
    for (let i = 0; i < str.length; i++) {
      // The "| 0" forces 32-bit integer to prevent float overflow
      h = (h * 31 + str.charCodeAt(i)) | 0;
    }
    return Math.abs(h) % this._capacity;
  }

  put(key, value) {
    if (this._size / this._capacity >= this._loadThreshold) {
      this._resize();
    }
    const idx = this._hash(key);
    const bucket = this._buckets[idx];

    // Update existing key if found
    for (let i = 0; i < bucket.length; i++) {
      if (bucket[i][0] === key) {
        bucket[i][1] = value;
        return;
      }
    }
    bucket.push([key, value]);
    this._size++;
  }

  get(key) {
    const idx = this._hash(key);
    for (const [k, v] of this._buckets[idx]) {
      if (k === key) return v;
    }
    return undefined;
  }

  delete(key) {
    const idx = this._hash(key);
    const bucket = this._buckets[idx];
    for (let i = 0; i < bucket.length; i++) {
      if (bucket[i][0] === key) {
        bucket.splice(i, 1);
        this._size--;
        return true;
      }
    }
    return false;
  }

  _resize() {
    const oldBuckets = this._buckets;
    this._capacity *= 2;
    this._buckets = Array.from({ length: this._capacity }, () => []);
    this._size = 0;
    for (const bucket of oldBuckets) {
      for (const [k, v] of bucket) {
        this.put(k, v);
      }
    }
  }

  get size() { return this._size; }
}

Variation: Hash Map with Open Addressing (Linear Probing)

Python
class OpenAddressHashMap:
    """Hash map using open addressing with linear probing.
    
    Instead of chains, collisions are resolved by probing
    for the next empty slot in the array. Deletions use a
    sentinel value (DELETED) to avoid breaking probe chains.
    
    This is closer to how Python's built-in dict actually works
    (though Python uses a more sophisticated probing sequence).
    """
    EMPTY = object()    # Sentinel for never-used slots
    DELETED = object()  # Sentinel for deleted slots (tombstone)

    def __init__(self, capacity=16):
        self._capacity = capacity
        self._size = 0
        self._keys = [self.EMPTY] * capacity
        self._values = [None] * capacity

    def _hash(self, key):
        return hash(key) % self._capacity

    def put(self, key, value):
        if self._size >= self._capacity * 0.5:  # lower threshold for open addressing
            self._resize()

        idx = self._hash(key)
        while True:
            if self._keys[idx] is self.EMPTY or self._keys[idx] is self.DELETED:
                self._keys[idx] = key
                self._values[idx] = value
                self._size += 1
                return
            if self._keys[idx] == key:
                self._values[idx] = value  # update existing
                return
            idx = (idx + 1) % self._capacity  # linear probe

    def get(self, key, default=None):
        idx = self._hash(key)
        while self._keys[idx] is not self.EMPTY:
            if self._keys[idx] == key:
                return self._values[idx]
            idx = (idx + 1) % self._capacity
        return default

    def delete(self, key):
        idx = self._hash(key)
        while self._keys[idx] is not self.EMPTY:
            if self._keys[idx] == key:
                # Mark as deleted (tombstone) โ€” don't use EMPTY
                # because that would break probe chains for other keys
                self._keys[idx] = self.DELETED
                self._values[idx] = None
                self._size -= 1
                return True
            idx = (idx + 1) % self._capacity
        return False

    def _resize(self):
        old_keys, old_values = self._keys, self._values
        self._capacity *= 2
        self._keys = [self.EMPTY] * self._capacity
        self._values = [None] * self._capacity
        self._size = 0
        for i, k in enumerate(old_keys):
            if k is not self.EMPTY and k is not self.DELETED:
                self.put(k, old_values[i])

Common Mistakes

โš ๏ธ Mistakes That Trip Up Beginners
  1. Using mutable objects as hash map keys. Lists and dicts can't be hashed reliably because their contents can change after insertion, which would change their hash value and make them unfindable. Use tuples or frozensets instead. In Python: dict[[1,2]] = "x" crashes, but dict[(1,2)] = "x" works.
  2. Assuming iteration order is guaranteed. Python 3.7+ guarantees insertion order for dicts, but most other languages don't. Java's HashMap, C++'s unordered_map, and Go's map all iterate in unpredictable order. If you need ordered keys, use a TreeMap (sorted) or LinkedHashMap (insertion order) in Java.
  3. Not using defaultdict or Counter in Python. Writing if key not in dict: dict[key] = 0 everywhere is verbose and error-prone. defaultdict(int) auto-initializes missing keys to 0. Counter(iterable) counts elements in one call. These save time and prevent KeyError bugs.
  4. Confusing hash map and sorted map. If you need ordered keys, range queries, or finding the min/max key efficiently, you want a TreeMap (red-black tree, O(log n) per operation), not a hash map. Hash maps can't answer "what's the smallest key greater than X?" without scanning everything.
  5. Checking if x in list instead of if x in set. Membership testing in a list is O(n). In a set it's O(1). For any problem where you check "have I seen this before?" more than a few times, convert to a set first.

Real-World Example: Word Frequency Counter

Counting word frequency is one of the most common hash map tasks in production code. Search engines rank pages partly by word frequency. Spam filters count suspicious words. Analytics dashboards count events. Here's a practical implementation:

Python
from collections import Counter
import re

def analyze_text(text):
    """Analyze word frequency in text.
    
    Real-world uses:
    - Search engine indexing (TF-IDF scoring)
    - Spam detection (count suspicious words)
    - Plagiarism detection (compare frequency profiles)
    - Natural language processing (bag-of-words model)
    """
    # Normalize: lowercase, extract words only
    words = re.findall(r'[a-z]+', text.lower())

    # Counter is a dict subclass optimized for counting
    freq = Counter(words)

    # Most common words
    print("Top 5 words:")
    for word, count in freq.most_common(5):
        bar = "โ–ˆ" * count
        print(f"  {word:>12}: {bar} ({count})")

    # Unique word count
    print(f"\nTotal words: {len(words)}")
    print(f"Unique words: {len(freq)}")
    print(f"Vocabulary richness: {len(freq) / len(words):.2%}")

    return freq

# Example
sample = """
The quick brown fox jumps over the lazy dog.
The dog barked at the fox. The fox ran away.
The quick fox was too quick for the lazy dog.
"""
freq = analyze_text(sample)

# Check if two texts are similar (cosine similarity)
def text_similarity(freq1, freq2):
    """Compare two frequency maps using cosine similarity.
    
    Returns 0.0 (completely different) to 1.0 (identical).
    This is the basis for document similarity in search engines.
    """
    # Only consider words that appear in both texts
    common = set(freq1.keys()) & set(freq2.keys())
    if not common:
        return 0.0

    dot_product = sum(freq1[w] * freq2[w] for w in common)
    mag1 = sum(v**2 for v in freq1.values()) ** 0.5
    mag2 = sum(v**2 for v in freq2.values()) ** 0.5

    return dot_product / (mag1 * mag2)

This frequency-counting pattern appears everywhere: log analysis (which error codes appear most?), A/B testing (count conversions per variant), network monitoring (which IPs send the most traffic?), and content moderation (flag posts with unusual word patterns).

Interview Patterns

๐Ÿ’ก The Two-Sum Pattern

Given an array, find two numbers that add to a target. The brute force is O(nยฒ) โ€” check every pair. The hash map trick: for each number x, check if target - x is already in the map. If yes, you found your pair. If no, add x to the map. One pass. O(n).

This pattern extends to three-sum (sort + two pointers), four-sum, and subarray-sum problems. The core idea โ€” "compute what I need, check if I've seen it" โ€” is the single most useful hash map technique.

๐Ÿ’ก Frequency Counting

Build a frequency map, then query it. "Most frequent element", "first unique character", "anagram checking" โ€” all frequency maps. The pattern is almost always: one pass to build the map, then one pass (or one lookup) to answer the question.

๐Ÿ’ก Hash Set for O(1) Membership

Whenever you catch yourself doing if x in list (O(n)), ask: could this be a set? Converting the list to a set gives O(1) lookups. Problems like "contains duplicate", "intersection of two arrays", "longest consecutive sequence" all rely on this conversion.

๐Ÿ’ก Subarray Sum Equals K (Prefix Sum + Hash Map)

Store prefix sums (cumulative sums from the start) in a hash map. For each index, compute the running sum and check if sum - k exists in the map. If it does, there's a subarray summing to k between those two positions. This turns an O(nยฒ) problem into O(n).

๐Ÿ’ก Grouping by Key

Many problems ask you to group items โ€” anagrams, items by category, nodes by level. The pattern: compute a "group key" for each item (sorted characters for anagrams, tree depth for level-order), then use a hash map from group key to list of items. defaultdict(list) in Python makes this clean.

Problems: Group Anagrams (#49), Group People by Body Size, Accounts Merge.

๐Ÿ’ก Sliding Window + Hash Map

Combine a sliding window (two pointers defining a range) with a hash map (tracking elements in the current window). The map tells you whether the window is "valid" โ€” has all required characters, has no duplicates, etc. Problems: Longest Substring Without Repeating Characters (#3), Minimum Window Substring (#76).

Practice Problems

Hash map/set problems are everywhere. These cover the main patterns you'll see.

#ProblemDifficultyPattern
1Two SumEasyComplement lookup
217Contains DuplicateEasyHash set membership
242Valid AnagramEasyFrequency counting
706Design HashMapEasyBuild from scratch
49Group AnagramsMediumSorted key grouping
560Subarray Sum Equals KMediumPrefix sum + map
128Longest Consecutive SequenceMediumHash set + smart iteration
146LRU CacheMediumHashMap + doubly linked list