Data Structures & Algorithms Cheatsheet
Hash Maps and Sets
Use this Data Structures & Algorithms reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Complexity
A hash table trades memory for O(1) average lookup. Worst case degrades to O(n) when every key lands in the same bucket.
| Operation | Average | Worst | Notes |
|---|---|---|---|
| insert | O(1) | O(n) | amortized, resizes double capacity |
| lookup by key | O(1) | O(n) | worst is full collision |
| delete | O(1) | O(n) | |
| iterate all | O(n) | O(n) | order is insertion order in Python 3.7+ |
| min or max key | O(n) | O(n) | use a heap or sorted structure |
| range query | O(n) | O(n) | hash maps cannot do ranges |
Space is O(n) plus load-factor slack, roughly 1.5–3× the raw key and value bytes.
When To Reach For A Hash Map
- You are about to write a nested loop that asks "does this other element exist" for each element. That inner loop becomes a set lookup, turning O(n²) into O(n).
- You need counts, groupings, or last-seen indices.
- You need to dedupe or check membership.
- You need a complement lookup: for target − x, previous prefix sum, or a canonical key like the sorted characters of a word.
Do not reach for it when you need order, ranges, or the k smallest. Those want a sorted array, a heap, or a balanced tree.
Python Dict And Set
d = {}
d["a"] = 1
d.setdefault("b", []).append(2) # init-on-first-use
d.get("z", 0) # default instead of KeyError
d.pop("a", None) # delete with default
"a" in d # key membership, O(1)
d.keys(), d.values(), d.items()
for k, v in d.items(): ...
{k: v for k, v in d.items() if v > 1}
max(d, key=d.get) # key with the largest value
from collections import defaultdict, Counter
groups = defaultdict(list)
groups[key].append(x) # no setdefault needed
cnt = defaultdict(int)
cnt[x] += 1
c = Counter("mississippi")
c.most_common(2) # [('i', 4), ('s', 4)]
c["z"] # 0, does not insert
c1 - c2 # subtract, drops non-positive
c1 & c2 # per-key minimum (intersection)
sum(c.values()) # total count
s = set()
s.add(1); s.discard(9) # discard never raises
s.remove(1) # KeyError if absent
a | b, a & b, a - b, a ^ b # union, intersection, difference, symmetric
a <= b # subset
frozenset([1, 2]) # hashable, usable as a dict keyKey Design
Keys must be hashable and immutable. That constraint drives most of the trick.
| Grouping goal | Key expression |
|---|---|
| anagrams | "".join(sorted(word)) or a 26-length count tuple |
| points | (x, y) tuple, never a list |
| grid cell | (r, c) or r * cols + c |
| sets as keys | frozenset(items) |
| shifted string patterns | tuple of consecutive character gaps |
| board state | tuple(map(tuple, grid)) |
| case-insensitive | s.casefold() |
# group anagrams in O(n * k log k) from collections import defaultdict groups = defaultdict(list) for w in words: groups[tuple(sorted(w))].append(w) result = list(groups.values()) # O(n * k) variant with a count signature def sig(w): c = [0] * 26 for ch in w: c[ord(ch) - 97] += 1 return tuple(c)
Canonical Patterns
def two_sum(nums, target): """Complement lookup. O(n) time, O(n) space.""" seen = {} # value -> index for i, x in enumerate(nums): if target - x in seen: return [seen[target - x], i] seen[x] = i return [] def subarray_sum_equals_k(nums, k): """Prefix sum counts. Handles negatives, unlike sliding window.""" from collections import defaultdict count = defaultdict(int) count[0] = 1 # empty prefix total = ans = 0 for x in nums: total += x ans += count[total - k] count[total] += 1 return ans def longest_consecutive(nums): """O(n): only start counting from a sequence start.""" s = set(nums) best = 0 for x in s: if x - 1 in s: continue y = x while y + 1 in s: y += 1 best = max(best, y - x + 1) return best def first_duplicate(a): seen = set() for x in a: if x in seen: return x seen.add(x) return None
Java And C++ Equivalents
Map<String, Integer> m = new HashMap<>(); m.put("a", 1); m.getOrDefault("z", 0); m.merge("a", 1, Integer::sum); // counting idiom m.computeIfAbsent("k", k -> new ArrayList<>()).add(2); m.containsKey("a"); for (Map.Entry<String, Integer> e : m.entrySet()) e.getKey(); Set<Integer> s = new HashSet<>(); s.add(1); s.contains(1); s.remove(1); // ordered variants: O(log n) but give you ranges and min/max TreeMap<Integer, Integer> t = new TreeMap<>(); t.floorKey(5); t.ceilingKey(5); t.firstKey(); t.lastKey(); t.headMap(5); t.tailMap(5); t.subMap(1, 9); LinkedHashMap<String, Integer> lru = new LinkedHashMap<>(); // insertion order
unordered_map<string, int> m; m["a"]++; // default-constructs to 0 first if (m.count("a")) ... if (auto it = m.find("a"); it != m.end()) it->second; unordered_set<int> s{1, 2, 3}; map<int, int> t; // red-black tree, O(log n), sorted t.lower_bound(5); // first key >= 5 t.upper_bound(5); // first key > 5
C++
m["missing"]inserts a default-constructed value. Use.count()or.find()when you only mean to read, especially inside a loop over a const map.
Gotchas
Counter[missing]returns 0 without inserting, butdefaultdict[missing]inserts. That silently grows the dict and breakslen(d)checks.- Mutable keys are a bug. A list or a set cannot be a key. A tuple containing a list cannot either.
floatkeys.0.1 + 0.2is not0.3, andhash(1) == hash(1.0), so{1: "a", 1.0: "b"}has one entry.Trueand1collide.hash(True) == hash(1), so booleans and ints share dict slots.- Iteration order. Python dicts and sets: dicts preserve insertion order, sets do not. Java
HashMapand C++unordered_mapguarantee nothing. - Adversarial hashing. C++
unordered_map<int,int>on competitive judges can be forced to O(n) collisions. Use a custom hash with a random seed, ormap. - Deleting while iterating raises
RuntimeErrorin Python. Collect keys first:for k in list(d).