Arrays & hashing¶
At a glance
Priority: P0 · Complexity: ⅕ · Est. time: 4 h · Phase: 1 · Prereqs: Python idioms You're done when: you solve every NC150 problem below in ≤ 20 min each, and can explain prefix-sum + hashmap counting (LC 560) and O(n) consecutive-sequence (LC 128) from scratch.
Why it matters¶
Hashing is the most common optimisation in interviews: it turns an O(n^2) "search for a partner" into O(n) "look it up". Almost every other pattern (sliding window, graphs, DP memo) leans on a hash map. At Staff level, follow-ups pivot to hashing's real-world properties: memory overhead, adversarial collisions, distributed partitioning (consistent hashing), and approximate structures (Bloom filters, Count-Min Sketch).
Core concepts¶
Pattern recognition signals¶
| When you see… | Think… |
|---|---|
| "find a pair/complement that sums to X" (unsorted) | Hash map of value → index (Two Sum) |
| "have we seen this before", "duplicates" | Set |
| "group items that are equivalent" | Canonical key → list (sorted string, count tuple) |
| "count subarrays with sum/xor = k" | Prefix sum + hash map of prefix counts |
| "frequency", "top k frequent", "majority" | Counter, bucket sort, Boyer-Moore |
| "O(1) extra space" + values in range [1, n] | Use the array itself as a hash (index marking / cyclic sort) |
| "product/sum of everything except i" | Prefix and suffix passes |
Template 1: complement lookup (O(n) time, O(n) space)¶
def two_sum(nums, target):
seen = {} # value -> index
for i, x in enumerate(nums):
if target - x in seen: # look up BEFORE inserting (handles x + x)
return [seen[target - x], i]
seen[x] = i
return []
Template 2: prefix sum + counts (O(n) time, O(n) space)¶
from collections import defaultdict
def subarray_sum_equals_k(nums, k):
count = defaultdict(int)
count[0] = 1 # empty prefix: subarray starting at index 0
prefix = ans = 0
for x in nums:
prefix += x
ans += count[prefix - k] # earlier prefixes p with prefix - p == k
count[prefix] += 1
return ans
Works with negatives, which is why this, not a sliding window, is the tool when values can be negative.
Template 3: canonical key grouping¶
from collections import defaultdict
def group_by_key(items, key_fn):
groups = defaultdict(list)
for it in items:
groups[key_fn(it)].append(it)
return list(groups.values())
# anagram key: tuple of 26 counts -> O(K) instead of sorted() O(K log K)
Template 4: array as hash (O(1) extra space)¶
def first_missing_positive(nums):
n = len(nums)
for i in range(n):
# place v at index v-1 while in range and not already there
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, v in enumerate(nums):
if v != i + 1:
return i + 1
return n + 1
Each swap places one value permanently, so the total is O(n) despite the nested loop.
Variations & decision flow¶
flowchart TD
A[Array/string problem] --> B{Need a partner / seen-before?}
B -- yes --> C[Hash map / set]
B -- no --> D{Subarray sum/count condition?}
D -- "values can be negative" --> E[Prefix sum + hashmap]
D -- "all non-negative + longest/shortest" --> F[Sliding window]
D -- no --> G{Grouping by equivalence?}
G -- yes --> H[Canonical key -> defaultdict list]
G -- no --> I{O-1 space, values in 1..n?}
I -- yes --> J[Index marking / cyclic sort]
I -- no --> K{Frequency / top-k?}
K -- yes --> L[Counter + bucket sort or heap]
| Variation | Example | Twist |
|---|---|---|
| Frequency buckets | Top K Frequent (347) | Bucket index = frequency, giving O(n) |
| Boyer-Moore voting | Majority Element (169, 229) | O(1) space; verify the candidate if a majority isn't guaranteed |
| Prefix/suffix products | Product Except Self (238) | Output array doubles as the prefix store, giving O(1) extra |
| Set "start of run" | Longest Consecutive (128) | Only expand from x where x-1 is absent |
| Encoding framing | Encode/Decode Strings (271) | Length-prefix is robust to any character |
Common bugs¶
- Inserting into the map before the lookup in Two Sum pairs an element with itself.
- Forgetting
count[0] = 1in prefix-sum counting. - Using
sorted(s)as a key without"".joinortuple(lists are unhashable). - In 128, looping
while x+1 in sfrom every x gives O(n^2). Start only at run heads. - Delimiter-based encoding breaks when the delimiter appears in the data.
Senior-level nuance¶
- Hash map memory in Python is about 50–100+ bytes per entry once object overhead is included. For 10^8 keys, switch to sorting, numpy, or probabilistic structures.
- Collision attacks: CPython randomises
str/byteshashing (PYTHONHASHSEED) against hash-flooding DoS.inthashes are not randomised. - Distributed version: Two Sum over a sharded dataset means partitioning by
xandtarget - xso partners land on the same shard. That is the shuffle in MapReduce joins.
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| NeetCode roadmap: Arrays & Hashing | course/video | Short, clear video per problem in exactly this order | intermediate | freemium |
| Tech Interview Handbook: Array | article | Edge cases and techniques checklist | intermediate | free |
| Tech Interview Handbook: Hash table | article | Concise corner cases and recommended problems | intermediate | free |
| VisuAlgo: Hash table |
interactive | Watch linear probing vs chaining and resizing happen | intermediate | free |
| Python collections docs | docs | Counter/defaultdict semantics | intermediate | free |
| Sean Prashad's LeetCode Patterns |
interactive | Filter problems by pattern and company to extend practice | intermediate | free |
| Coding Interview Patterns (Xu & Gunawardane, ByteByteGo) | book/course | Visual, pattern-first explanations; hashing and prefix-sum chapters | intermediate | paid |
Hands-on lab¶
Problem set (NeetCode-250 order). Time-box each at 25–35 min, log it in the problem tracker, and re-solve on day 1, 3, 7 and 21.
| # | LC | Problem | Difficulty | Set | Key insight |
|---|---|---|---|---|---|
| 1 | 217 | Contains Duplicate | Easy | NC150 | Set membership: first repeat wins, O(n) time/space. |
| 2 | 242 | Valid Anagram | Easy | NC150 | Equal frequency maps (Counter or 26-int array). |
| 3 | 1 | Two Sum | Easy | NC150 | Store value->index; look up target - x before inserting x. |
| 4 | 1929 | Concatenation of Array | Easy | NC250+ | Warm-up: index arithmetic i and i+n. |
| 5 | 14 | Longest Common Prefix | Easy | NC250+ | Vertical scan, or compare only min and max after sorting. |
| 6 | 169 | Majority Element | Easy | NC250+ | Boyer-Moore vote: O(1) space. |
| 7 | 706 | Design HashMap | Easy | NC250+ | Buckets + chaining; discuss load factor and resize. |
| 8 | 49 | Group Anagrams | Medium | NC150 | Canonical key: sorted string or 26-count tuple. |
| 9 | 347 | Top K Frequent Elements | Medium | NC150 | Bucket sort by frequency gives O(n); heap gives O(n log k). |
| 10 | 271 | Encode and Decode Strings | Medium | NC150 | Length-prefix framing ('4#abcd') beats delimiters. (Premium; free on NeetCode) |
| 11 | 238 | Product of Array Except Self | Medium | NC150 | Prefix product pass, then suffix product pass; no division. |
| 12 | 36 | Valid Sudoku | Medium | NC150 | Three set families; box id = (r//3, c//3). |
| 13 | 128 | Longest Consecutive Sequence | Medium | NC150 | Only start counting at x where x-1 not in set -> O(n). |
| 14 | 560 | Subarray Sum Equals K | Medium | NC250+ | Prefix-sum counts: add count[prefix-k]; seed {0:1}. |
| 15 | 75 | Sort Colors | Medium | NC250+ | Dutch national flag: three pointers, one pass. |
| 16 | 912 | Sort an Array | Medium | NC250+ | Implement merge sort / heap sort; know stability trade-offs. |
| 17 | 229 | Majority Element II | Medium | NC250+ | Generalised Boyer-Moore: at most 2 candidates for > n/3. |
| 18 | 41 | First Missing Positive | Hard | NC250+ | Array as hash: cyclic-place value v at index v-1. |
Stretch exercise (30 min): implement MyHashMap (706) with separate chaining and resizing at load factor 0.75. Then benchmark 10^6 random inserts against dict and write down the constant-factor gap.
Questions¶
L1 — Recall¶
Q1. Why does LC 560 need prefix sums + hashmap rather than a sliding window?
Answer
Sliding window relies on monotonicity: extending the window increases the sum, shrinking decreases it. With negative numbers that breaks, so you can't decide when to shrink. Prefix sums turn the subarray sum into P[j] - P[i] = k, a complement lookup, which is order-independent.
Q2. What makes LC 128 O(n) even though there is a nested while loop?
Answer
You only enter the inner loop when x-1 is not in the set (x starts a run). Each element is visited by the inner loop exactly once across all runs, so O(n) total.
Q3. Explain Boyer-Moore majority vote.
Answer
Keep a candidate and a counter. The counter increments on a match and decrements on a mismatch. When it hits 0, take the next element as the candidate. Pairing off different elements never eliminates a true majority (> n/2). If a majority isn't guaranteed, do a second verification pass. For > n/3, keep 2 candidates.
L2 — Apply¶
Q4. Implement Top K Frequent in O(n).
Answer
O(n) time and space. Frequency is bounded by n, so the buckets index it directly.Q5. Trace Product of Array Except Self on [1,2,3,4] with O(1) extra space.
Answer
First pass (prefix): out = [1, 1, 2, 6]. Second pass from the right with suffix:
i=3: out[3]=6·1=6, suffix=4. i=2: out[2]=2·4=8, suffix=12. i=1: out[1]=1·12=12, suffix=24. i=0: out[0]=1·24=24.
Result [24, 12, 8, 6].
Q6. Design an encoding for a list of arbitrary strings (LC 271).
Answer
def encode(strs):
return "".join(f"{len(s)}#{s}" for s in strs)
def decode(s):
out, i = [], 0
while i < len(s):
j = s.index("#", i)
n = int(s[i:j])
out.append(s[j + 1:j + 1 + n])
i = j + 1 + n
return out
Content-Length, Kafka and gRPC.
L3 — Design & trade-offs¶
Q7. Two Sum: hashmap vs sort + two pointers. When do you pick each?
Answer
Hashmap: O(n) time, O(n) space, keeps original indices. Sort + two pointers: O(n log n) time, O(1) extra space if in-place (but you lose indices unless you sort pairs). Pick sort when memory is tight, input is already sorted, or you need all unique pairs / k-sum generalisation (3Sum). Pick hashmap for the single-pass streaming case.
Q8. Group Anagrams on 10^7 strings of length ≤ 100 with a Unicode alphabet. What changes?
Answer
The 26-count tuple no longer applies. Use "".join(sorted(s)) (O(K log K)) or a Counter frozenset key (heavier). Memory is dominated by the strings themselves. Consider hashing the canonical key to a 64-bit digest (accepting a tiny collision risk, verified per bucket) to shrink the key. At this scale, do it as a MapReduce: map emits (canonical_key, s), shuffle, reduce groups.
Q9. Contains Duplicate on a 1 TB stream with 100 MB RAM.
Answer
Exact: external sort, or hash-partition to disk into P files (each fits in RAM), then set-check per partition. Two passes. Approximate: a Bloom filter sized for the expected distinct count (for example 10^9 items at 1% FPR is about 1.2 GB, too big, so partition first or accept a higher FPR), then verify positives exactly. State the false-positive semantics: a Bloom filter says "maybe seen" or "definitely not seen".
L4 — Staff-level ambiguity¶
Q10. 'Top-k frequent elements' is now a real-time dashboard over 1M events/sec across 50 nodes. Design it.
Answer
Per node: a Count-Min Sketch plus a heap of the top-k candidates (or the Space-Saving algorithm, which has deterministic error bounds), windowed per minute. Aggregate: nodes send top-m (m ≫ k, for example 10k) with counts every few seconds, and the aggregator merges them. Error comes from items that are hot globally but not in any node's local top-m, so size m by skew or use mergeable sketches (CMS is mergeable by addition). Exactness trade-off: say so explicitly. Windowing: tumbling vs sliding (ring of per-second sketches). Failure: aggregator restart loses at most one window, and nodes can replay. Kafka + Flink is the off-the-shelf version.
Q11. The hashmap in a hot service path is using 40 GB of RAM. How do you approach it?
Answer
Measure first: key/value sizes, count and growth. Options in rising cost:
- Compact representation: ints instead of strings, interned keys, __slots__/struct arrays, numpy/Arrow columns.
- Off-heap stores (RocksDB, mmap'd sorted arrays with binary search).
- Eviction (LRU/TTL) if it's a cache.
- Sharding across processes or machines (consistent hashing).
- Probabilistic structures if exactness isn't required.
Pair it with an SLO: what latency increase is acceptable for what memory saving. Write the decision up as an ADR.
Real-world use cases¶
- Idempotency keys: a hash set (Redis
SETNXwith TTL) of request ids dedupes retries in payment and booking APIs. - Shipment reconciliation (logistics): matching bookings to invoices is a hash join on (booking_ref, container_no) instead of O(n·m) nested loops.
- Anagram-style canonicalisation: normalising addresses or party names (lower-case, sorted tokens) to group duplicate master-data records.
- Prefix sums: running totals for cumulative TEU per voyage, answering "any window hitting capacity k?" in O(1) per query.
Pitfalls & anti-patterns¶
- Defaulting to a hashmap when the input is sorted (two pointers is O(1) space).
- Forgetting that hashmaps lose order information you might need.
- Assuming O(1) under adversarial keys, or ignoring memory overhead at scale.
- Mutating a list while iterating over it for dedupe.
Checklist¶
- I can write complement-lookup, prefix-sum-count and canonical-key templates from memory
- I solved all NC150 problems in this set within the time-box
- I can explain Boyer-Moore and cyclic sort invariants
- I answered all L3 questions out loud in < 3 min each