0/23in Data Structures & Algorithms
Data Structures & Algorithms

Hashing: Maps & Sets

Trade memory for time. How hash tables turn O(n) scans into O(1) lookups, and the counting/grouping patterns they unlock.

~15 minLesson 14 of 60
Your progressNot started

Most brute-force solutions are slow for one reason: they search. “Have I seen this value before?” answered by scanning the array is O(n), and doing it inside a loop is where O(n²) comes from. A hash table answers that same question in O(1), and that single upgrade collapses a huge family of problems from quadratic to linear.

The trade is memory for time. You keep a side structure — a dict (map) or a set — and consult it as you go. Learning to reach for it reflexively is one of the highest-leverage habits in all of DSA.

The mental model

A hash table stores keys in an array of “buckets.” A hash function turns a key into a bucket index, so instead of looking through every slot, you compute where the key would be and go straight there. Two keys landing in the same bucket is a collision, resolved by chaining or probing — but amortized, the lookup, insert, and delete are all O(1).

Two Python types cover almost everything:

  • set — membership. “Have I seen x?” Order-free, duplicate-free.
  • dict — association. Map a key to a count, an index, a list, anything.
seen = set()
seen.add(5)
3 in seen          # False, O(1)

count = {}
count["a"] = count.get("a", 0) + 1   # the counting idiom
Hash table · chainingload factor 0.60
hash("bee") = sum of char codes = 300 mod 5 = 0
0
bee
1
2
cat
3
4
dog
keys: 3buckets with collisions: 0
just insertedChaining stores collisions as a linked list inside one bucket — lookups stay O(1) average while the load factor is small.

Pattern recognition

Variations you will meet

Worked problems

Try each yourself before revealing the solution. Notice how every one of them is really “replace a search with a lookup.”

LeetCode 1Easy
  • Hash map
  • Complement

Two Sum

Given an array of integers and a target, return the indices of the two numbers that add up to the target. Exactly one valid pair exists, and you may not reuse the same element.

Approach. The brute force checks every pair — O(n²). Instead, as you scan, ask: “for the current number x, have I already seen its complement target - x?” A map from value to index answers that in O(1). Check before inserting so you never pair an element with itself.

Show solution
def two_sum(nums, target):
    seen = {}                      # value -> index
    for i, x in enumerate(nums):
        need = target - x
        if need in seen:
            return [seen[need], i]
        seen[x] = i
    return []

Complexity. O(n) time, O(n) space — one pass, one map. This “store what you have, look up what you need” move is the single most common hashing pattern in interviews.

LeetCode 49Medium
  • Hash map
  • Grouping

Group Anagrams

Given a list of words, group together the ones that are anagrams of each other (same letters, any order). Return the groups in any order.

Approach. Two words are anagrams exactly when they share a canonical form. Sorting a word’s letters produces a key that all its anagrams also produce, so bucket words into a map from that key to the list of words that match.

Show solution
from collections import defaultdict

def group_anagrams(words):
    groups = defaultdict(list)
    for w in words:
        key = tuple(sorted(w))     # canonical form; tuple is hashable
        groups[key].append(w)
    return list(groups.values())

Complexity. O(n · k log k) for n words of length up to k, from sorting each word. If letters are limited to lowercase, a 26-length count tuple as the key drops it to O(n · k). The lesson: turn “are these equivalent?” into “do they produce the same key?”

LeetCode 128Medium
  • Hash set
  • O(n)

Longest Consecutive Sequence

Given an unsorted array, find the length of the longest run of consecutive integers (e.g. [100, 4, 200, 1, 3, 2]1,2,3,4 → length 4). Do it in O(n) — no sorting.

Approach. Put every number in a set for O(1) membership. A number x can only be the start of a run if x - 1 is not in the set — so only start counting from those, then walk x+1, x+2, … while they exist. Each number is visited at most twice, so the whole thing is linear despite the nested-looking loop.

Show solution
def longest_consecutive(nums):
    s = set(nums)
    best = 0
    for x in s:
        if x - 1 in s:            # not a run start; skip
            continue
        length = 1
        while x + length in s:    # walk the run upward
            length += 1
        best = max(best, length)
    return best

Complexity. O(n) time, O(n) space. The x - 1 in s guard is what keeps it linear — without it you would re-walk the same run from every element. Skipping non-starts is a pattern worth remembering.

When not to reach for it

Hashing costs memory and destroys order. If the input is already sorted, two pointers or binary search often solve the same problem in O(1) extra space — no map needed. And if you need keys kept in order (range queries, “next larger key”), a hash map cannot help; that is what balanced trees and heaps are for, which are coming up next.

Your progressNot started