DSA Roadmap/Arrays & Hashing

Hash Maps & Hash Sets

The highest-leverage trick in interviews: trade O(n) space for O(1) average lookups, counts, and complement checks to collapse brute-force loops into linear scans.

!!!2/5Theory: 2h10 problems

Why this is the highest-leverage tool in the roadmap

If you could only bring one data structure into an interview, it should be a hash map. The reason is almost mechanical: any time a brute-force solution has an inner loop whose only job is "search for something" (a duplicate, a complement, a group, a previously-seen state), that inner O(n) search collapses to O(1) average with a hash map, turning an O(n^2) algorithm into O(n). You already leaned on this in the Foundations topic's complexity cheat sheet (x in list vs x in set) and in the previous subtopic (prefix sum + hash map). This subtopic makes the tool itself — not just its use as a sidekick — the object of study.

What's actually happening under the hood

A hash table is an array of buckets. A hash function maps a key to an integer, which is reduced modulo the bucket-array size to pick a bucket index. Two different keys can hash to the same bucket — a collision — and are typically resolved one of two ways:

  • Separate chaining: each bucket holds a small list (or tree, in some JVM implementations for very full buckets) of all keys that hashed there. Lookup walks that list.
  • Open addressing: on collision, probe forward (linearly, quadratically, or via double hashing) to the next free slot instead of using a secondary structure.

The load factor α = n / m (entries / buckets) governs performance: as long as α is kept below a constant (typically resizing — doubling the bucket array and rehashing everything — around α ≈ 0.7), the expected chain length is O(1), which is why average-case lookup, insert, and delete are all O(1). This resize-and-rehash is itself O(n), but amortized over all the inserts that triggered it, it costs O(1) per insert on average — the same amortized-analysis idea from the Foundations topic applied to a different structure.

Worst case is not O(1). If enough keys collide into the same bucket (adversarially crafted input, or a pathological hash function), lookup degrades to O(n) per operation. This is a real, sometimes-asked follow-up ("what's the worst-case complexity of a hash map lookup, and when does it happen?") — know the honest answer, not just the average-case headline.

Hash map vs. hash set vs. the alternative

StructureStoresLookupOrdered?Use when
Array / list (unsorted)ValuesO(n)Insertion orderSmall n, or order matters more than speed
Hash SetUnique keys, no valuesO(1) avgNo guarantee (language-dependent)"Have I seen this?" / dedup
Hash MapKey → valueO(1) avgNo guarantee (language-dependent)Counting, grouping, complement lookup
Balanced BST / sorted mapKey → valueO(log n)SortedNeed min/max/range queries or sorted iteration

The recurring interview decision is row 2 vs. row 3: if you only need to know whether something exists, use a set (less memory, clearer intent than a map with dummy values). If you need to associate data with the key — a count, an index, a list of items — use a map.

A Python dict (3.7+) happens to preserve insertion order, but don't rely on hash-based ordering being consistent across languages or across a resize in other runtimes (Java's HashMap, for instance, makes no ordering guarantee at all). If a problem needs order, say so explicitly and reach for a structure that guarantees it (an OrderedDict/LinkedHashMap, or a list alongside the map).

The recognition patterns

Nearly every hashing problem in interviews is one of these five shapes. Learning to name the shape out loud is more valuable than memorizing any single solution:

  1. Seen-tracker (hash set). "Has this value/state appeared before?" Walk once, check membership before inserting.
  2. Frequency counter (hash map, often via a Counter/defaultdict(int)). "How many times does each element occur?" Almost always the first step in anagram, majority-element, and top-K problems.
  3. Complement lookup (hash map). "Does some other element combine with this one to satisfy a condition (sum, difference, ratio)?" Store what you've seen so far, keyed by value, and check for the complement of the current element before inserting it.
  4. Grouping by a canonical key (hash map of lists). "Bucket items that share some derived property." Compute a signature for each item (e.g., a sorted tuple, a rounded value, a normalized form) and append to groups[signature].
  5. Index-as-hash-map (in-place, O(1) space). When values are known to be bounded (e.g., constrained to [1, n]), you can sometimes use the array itself as a hash set by encoding "seen" as a sign flip or a swap into the value's own index — trading a real hash map for zero extra space. This shows up in exactly one problem below; it's a favorite "can you do it with O(1) space?" follow-up.

A minimal, generic illustration of pattern 4 — grouping by a derived key, applied to something unrelated to any specific interview problem so you can see the shape clearly:

from collections import defaultdict def group_by_signature(items, signature_fn): groups = defaultdict(list) for item in items: key = signature_fn(item) groups[key].append(item) return list(groups.values()) # e.g. group points by which quadrant they fall in quadrant = lambda p: (p[0] >= 0, p[1] >= 0) group_by_signature([(1, 2), (-1, 3), (2, -4), (-2, -1)], quadrant)

The whole pattern is: derive a key that's invariant across everything that should end up in the same bucket, then let the hash map do the grouping. Applying this to strings, counts, or coordinates is a matter of choosing the right signature_fn — that choice is the actual interview problem.

Complexity analysis

OperationHash Map / Set (average)Hash Map / Set (worst case)
InsertO(1)O(n)
Lookup / membershipO(1)O(n)
DeleteO(1)O(n)
SpaceO(n)O(n)

State both the average and the caveat when asked in an interview: "O(1) average per operation, assuming a reasonable hash function and load factor kept constant via resizing; O(n) worst case under pathological collisions." Space is always O(n) in the number of distinct keys stored — this is the "trade memory for speed" tradeoff, and it's worth naming explicitly when you introduce a hash map as your optimization.

Common pitfalls

  • Using a list where a set/map would do. x in some_list inside a loop silently reintroduces the O(n^2) you were trying to avoid — this is the single most common "how do we optimize this?" answer interviewers are fishing for.
  • Mutable keys. Lists and (in Python) other dicts can't be hash keys because their hash would change if mutated, breaking the bucket invariant. Use tuples, frozensets, or strings as canonical/immutable keys instead.
  • Assuming iteration order. As noted above — don't depend on it unless the language/structure explicitly guarantees it.
  • Off-by-one in the "seen before or after" direction. In a complement-lookup problem (pattern 3), decide carefully whether you check for the complement before or after inserting the current value — for example, doing both in the wrong order can cause you to match an element with itself when only one occurrence exists.
  • Forgetting that a Counter/defaultdict auto-vivifies keys on read, not just on write — accidentally checking if my_dict[k]: (instead of if k in my_dict:) on a defaultdict silently inserts k with a default value as a side effect.
  • Reaching for a hash map when you don't need one. If the value range is small and fixed (e.g., lowercase letters, digits 0–9), a fixed-size array/list is faster in practice than a hash map and avoids hashing overhead entirely — this is effectively a "perfect hash function" (identity mapping) and is worth mentioning as a micro-optimization.
  • Not clarifying what counts as "equal" for custom objects/floats. Hashing floating-point keys is fragile (rounding errors); hashing custom objects requires consistent __hash__/__eq__ (or equals/hashCode in Java) — call this out if it's relevant to the problem's data.

When hashing is the wrong tool

Hash maps and sets don't preserve order and don't support range queries (give me all keys between X and Y) or ordered iteration efficiently — for those, you want a balanced BST/ordered map (covered in the Trees topic) or a sorted structure. If a problem needs a running min/max alongside O(1) lookups, that's usually a signal to combine a hash map with a heap (Heaps & Priority Queues topic) rather than expecting the hash map alone to do the job.

How to talk about it in an interview

Name the tradeoff out loud the moment you introduce a hash map: "I'll trade O(n) space for a hash map so that this lookup drops from O(n) to O(1) average, which brings the whole algorithm down from O(n^2) to O(n)." Then, if pressed, be ready with the honest worst-case caveat from the complexity table above — that precision is exactly what separates a Senior-level answer from a memorized one.

Further Resources (Optional)

Practice Problems

Interview relevancy:!!!Critical — must-have!!Important — highly recommended!Good to have~Niche — rarely asked

Optional Practice (Extra Reps)

For once you've cleared the main set above and want more reps on this pattern. These don't count toward the roadmap's progress stats — solve them purely for your own benefit.