{}DSA Atlas

Hash Maps & Sets

The O(1) lookup that turns quadratic brute force into linear time. Counting, complements, grouping by key, and designing good keys.

Beginner12 practice problems4 easy7 medium1 hard

Why hashing is everywhere

A hash table stores key → value pairs and supports insert, delete and lookup in O(1) average time. Most brute-force solutions spend their time searching ("is there an element that…?"). Replacing that search with a hash lookup removes a whole factor of n.

How it works (enough to reason about it)

  1. A hash function turns a key into an integer.
  2. That integer mod the table size picks a bucket.
  3. Collisions (two keys, same bucket) are handled by chaining (a list per bucket) or open addressing (probe the next slot).
  4. When the table gets too full (load factor ≈ 0.75), it resizes (doubles and rehashes) — amortized O(1).

Worst case is O(n) per operation if everything collides, which is why adversarial inputs can break unordered_map in competitive programming (see the C++ note below).

The toolbox

from collections import Counter, defaultdict

seen = set()
seen.add(3); 3 in seen; seen.discard(3)

freq = Counter("mississippi")         # {'i': 4, 's': 4, 'p': 2, 'm': 1}
freq.most_common(2)                    # [('i', 4), ('s', 4)]

groups = defaultdict(list)             # missing keys start as []
groups["key"].append(1)

last_index = {}
last_index.get("x", -1)                # default when missing

# iterate
for key, val in freq.items(): ...
#include <unordered_map>
#include <unordered_set>

unordered_set<int> seen;
seen.insert(3); seen.count(3); seen.erase(3);

unordered_map<char, int> freq;
for (char c : s) freq[c]++;            // operator[] default-inits to 0

auto it = freq.find('x');
if (it != freq.end()) { /* it->second */ }

// ordered alternatives (O(log n), sorted iteration)
map<int, int> m;  set<int> st;

Pattern 1: Complement lookup

“Find two items that combine into a target.” For each item, compute what its partner would have to be and look it up.

def two_sum(nums, target):
    index_of = {}
    for i, x in enumerate(nums):
        if target - x in index_of:          # check BEFORE inserting x
            return [index_of[target - x], i]
        index_of[x] = i
    return []
vector<int> twoSum(vector<int>& nums, int target) {
    unordered_map<int, int> indexOf;
    for (int i = 0; i < (int)nums.size(); i++) {
        auto it = indexOf.find(target - nums[i]);
        if (it != indexOf.end()) return {it->second, i};
        indexOf[nums[i]] = i;
    }
    return {};
}

Checking before inserting avoids pairing an element with itself.

Pattern 2: Frequency counting

Python
def is_anagram(s, t):
    if len(s) != len(t):
        return False
    count = [0] * 26
    for a, b in zip(s, t):
        count[ord(a) - 97] += 1
        count[ord(b) - 97] -= 1
    return all(c == 0 for c in count)

Frequency maps also power sliding windows (count characters inside the window) — see Sliding Window.

Pattern 3: Grouping by a canonical key

Items that are “equivalent” should map to the same key. Designing that key is the whole problem.

Python
from collections import defaultdict

def group_anagrams(words):
    groups = defaultdict(list)
    for w in words:
        key = [0] * 26
        for ch in w:
            key[ord(ch) - 97] += 1
        groups[tuple(key)].append(w)       # O(L) key instead of O(L log L) sort
    return list(groups.values())

Other canonical keys you'll see:

EquivalenceKey
anagramssorted string or letter-count tuple
shifted strings (abc ~ bcd)tuple of differences between consecutive letters mod 26
same row/col/box in Sudoku(r, c // 3 ...) tuples
isomorphic shapes on a gridsorted tuple of cell offsets relative to the first cell
lines through pointsreduced slope (dy/g, dx/g) with normalized sign

Pattern 4: Prefix sum + hash map

Counting subarrays with a given sum (even with negatives): if prefix[j] - prefix[i] = k then subarray (i, j] sums to k. As you scan, count how many earlier prefixes equal prefix − k.

Python
def subarray_sum(nums, k):
    count = {0: 1}          # empty prefix
    prefix = ans = 0
    for x in nums:
        prefix += x
        ans += count.get(prefix - k, 0)
        count[prefix] = count.get(prefix, 0) + 1
    return ans

Covered in depth in Prefix Sums.

Pattern 5: Set for O(1) membership → linear algorithms

Python
def longest_consecutive(nums):
    s = set(nums)
    best = 0
    for x in s:
        if x - 1 not in s:                # x starts a run
            y = x
            while y + 1 in s:
                y += 1
            best = max(best, y - x + 1)
    return best

The inner loop looks quadratic but each number is visited by the inner while at most once overall (only from the start of its run) → O(n).

Pattern 6: Index as hash (O(1) extra space)

When values are in the range [1, n], the array itself can serve as a hash table: mark presence by negating nums[v-1], or place each value at its "home" index with cyclic swaps.

Python
def first_missing_positive(nums):
    n = len(nums)
    for i in range(n):
        # keep swapping nums[i] into its home slot nums[i]-1
        while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
            j = nums[i] - 1
            nums[i], nums[j] = nums[j], nums[i]
    for i in range(n):
        if nums[i] != i + 1:
            return i + 1
    return n + 1

Hash map vs sorted map

NeedUse
Just lookups/countshash map (dict, unordered_map)
Keys in sorted order, predecessor/successor, range queriesbalanced BST (std::map, TreeMap, Python sortedcontainers.SortedList)
Small integer keysplain array

Common mistakes

  • Pairing an element with itself in complement problems (insert after checking).
  • Forgetting to seed count[0] = 1 in prefix-sum counting.
  • Using a mutable list as a Python dict key (TypeError) — convert to tuple.
  • C++: m[key] inserts a default value when the key is missing; use find/count to check existence without inserting.
  • C++ competitive programming: anti-hash tests can make unordered_map O(n) per op. Use a custom hash (e.g. splitmix64) or map.
C++
struct SplitMix {
    static uint64_t splitmix64(uint64_t x) {
        x += 0x9e3779b97f4a7c15; x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
        x = (x ^ (x >> 27)) * 0x94d049bb133111eb; return x ^ (x >> 31);
    }
    size_t operator()(uint64_t x) const {
        static const uint64_t R = chrono::steady_clock::now().time_since_epoch().count();
        return splitmix64(x + R);
    }
};
unordered_map<long long, int, SplitMix> safeMap;

Complexity summary

OperationHash map (avg)Hash map (worst)Balanced BST
insert / delete / findO(1)O(n)O(log n)
iterate in key order✗✗O(n)
min / max keyO(n)O(n)O(log n)

Practice problems

Ordered roughly from warm-up to hard. Try each one for 25–40 minutes before peeking at the key idea; if you needed the hint, mark it “review” and redo it in a few days.

0 / 12 solved
Map value → index. For each x, check whether target − x is already in the map before inserting x.
Easy
Counter(s) == Counter(t), or a 26-slot count array: +1 for s, −1 for t, all zeros at the end.
Easy
Store the last index of each value; duplicate within k if i − last[x] <= k.
Easy
Count magazine letters; decrement for each note letter; fail on a negative count.
Easy
Canonical key = sorted word (or tuple of 26 counts). defaultdict(list) keyed by it.
Medium
Count, then bucket sort by frequency (buckets 0..n) for O(n), or a size-k heap.
Medium
Put all in a set. Only start counting from x if x−1 is not in the set, so each run is walked once: O(n).
Medium
Prefix sum + hashmap of counts of previous prefixes; add count[prefix − k] at each step. Seed count[0] = 1.
Medium
Sets per row, column and 3x3 box (index r//3*3 + c//3). Any repeat → invalid.
Medium
Length-prefix each string, e.g. '5#hello'. Decoding reads digits up to '#', then that many chars.
Medium
Meet in the middle: count all a+b sums in a map, then for each c+d add count[−(c+d)]. O(n^2).
Medium
O(1) space: use the array itself as a hash — place value v at index v−1 by swapping, then find the first mismatch.
Hard

Click the box to cycle: solved ✓ → needs review ↺ → not started. Links open LeetCode.