01 Β· DSA β File 02
Arrays, Strings & Hashing
Roughly 40% of interview problems are "here's an array or string; the fast solution uses a hashmap, two pointers, a sliding window, or prefix sums." Master these four moves and a huge slice of the interview surface becomes routine.
01Arrays: the physics of contiguous memory
An array is a promise: n elements, side by side, in one contiguous block of memory. Everything good and bad about arrays follows from that single fact.
address: 1000 1008 1016 1024 1032
ββββββββ¬βββββββ¬βββββββ¬βββββββ¬βββββββ
β a[0] β a[1] β a[2] β a[3] β a[4] β (8 bytes each)
ββββββββ΄βββββββ΄βββββββ΄βββββββ΄βββββββ
a[i] lives at: base_address + i Γ element_size β one multiply + one add = O(1)
Reading a[i] is arithmetic, not search β compute an address, jump there.
Contiguity must be preserved, so everything after the hole shifts by one.
Sequential access pulls whole cache lines β arrays beat linked lists in practice even at the same Big-O (file 03 has the full grudge match).
Dynamic arrays (Python list)
A Python list is a dynamic array of pointers to objects. It keeps spare capacity: append fills a slot in O(1), and when full it reallocates bigger and copies β amortized O(1) (the full doubling argument lives in file 01 Β§5). Consequences you should recite on demand:
| Op | Cost | Why |
|---|---|---|
a[i], a[i] = x | O(1) | address arithmetic |
a.append(x), a.pop() | O(1) amortized | tail has spare room |
a.insert(0, x), a.pop(0) | O(n) | shift everything |
a[i:j] | O(jβi) | copies into a new list |
x in a | O(n) | linear scan |
02Strings: immutable arrays of characters
A Python string behaves like a read-only array. Immutability is the interview-relevant property: every "modification" builds a brand-new string. That's how you fall into the classic accidental quadratic:
O(nΒ²) β the trap
s = ""
for ch in chunks:
s += ch # each += copies the WHOLE
# accumulated string β O(nΒ²)
O(n) β the fix
parts = []
for ch in chunks:
parts.append(ch) # O(1) amortized
s = "".join(parts) # one O(n) pass
Other string moves that come up constantly:
s[::-1] # reverse (O(n), makes a copy)
s.split(), s.strip() # tokenize / trim
ord('a'), chr(97) # char β codepoint, for count arrays
sorted(s) # list of chars, O(n log n) β canonical form for anagrams
For "compare letter counts" problems, a 26-slot count array is the zero-overhead hashmap:
counts = [0] * 26
for ch in s:
counts[ord(ch) - ord('a')] += 1
03Hashmaps: how the magic actually works
This is the data structure interviewers most love to ask you to explain, because it separates "I use dicts" from "I know what a dict is."
The problem it solves
Arrays give O(1) access by integer index. But we want O(1) access by arbitrary key β a user ID string, a tuple, an email. The trick: convert the key into an index.
The mechanism, step by step
- Hash the key β a deterministic, well-scattered integer. Same key β same hash, always.
- Modulo by capacity β a bucket index.
- Store (key, value) in that bucket. We keep the key too β because of the next point.
Collisions: the inevitable complication
Different keys can land in the same bucket (pigeonhole: infinite keys, finite buckets). Two classic strategies:
- Chaining: each bucket holds a small list; on collision, append. Lookup = hash, then scan that short list, comparing stored keys β that's why we keep them.
- Open addressing (what CPython uses): on collision, probe other slots in a deterministic sequence until a free one is found. Lookup follows the same probe sequence.
Chaining: Open addressing:
bucket[3] βββΊ (k1,v1) ββΊ (k9,v9) bucket[3] full? try 4β¦ full? try 6β¦ (probe sequence)
Load factor: when to grow
load factor = entries / buckets. As it rises, buckets get crowded, chains/probes lengthen, and O(1) decays toward O(n). So hashmaps resize when the load factor crosses a threshold (CPython: 2/3): allocate a bigger table and re-insert everything β hashes must be re-moduloed by the new capacity. That resize is O(n), but rare, so inserts are amortized O(1). Same doubling logic as dynamic arrays.
The contract that makes it all work
- Keys must be hashable β effectively immutable. This is why lists can't be dict keys but tuples can β if a key mutated after insertion, its hash would change and you'd never find it again (it's filed under the old hash).
a == bmust implyhash(a) == hash(b).- Worst case is O(n) β adversarial keys all colliding. Real-world relevance: hash-flooding DoS attacks; Python randomizes string hashing per-process partly for this reason.
GET key hashes to a bucket, O(1) average. Redis Cluster's keyβslot mapping (CRC16(key) % 16384) is literally hash-mod-capacity applied to shard routing. Same idea, different altitude: consistent hashing exists because naive mod-N re-shuffles almost every key when N changes β the distributed version of "resizing is expensive."Sets
A set is a hashmap that stores only keys. Same costs, same mechanics. Use it whenever the question is "have I seen this before?"
04Pattern: Two Pointers
When: the array is sorted (or can be), or you're working from both ends, or comparing/merging two sequences.
Why it works: sortedness gives you monotonicity β moving a pointer changes the quantity you're tracking in a predictable direction, so you can discard candidates without checking them. That's how you delete an O(n) inner loop.
Two Sum on a sorted array β the canonical demo:
def two_sum_sorted(a, target):
lo, hi = 0, len(a) - 1
while lo < hi:
s = a[lo] + a[hi]
if s == target: return [lo, hi]
if s < target: lo += 1 # sum too small β only a bigger left element can fix it
else: hi -= 1 # sum too big β only a smaller right element can fix it
return []
[1, 3, 4, 6, 8, 11] target 10
loβ βhi 1+11 = 12 too big β hi--
loβ βhi 1+8 = 9 too small β lo++
loβ βhi 3+8 = 11 too big β hi--
loβ βhi 3+6 = 9 too small β lo++
loββhi 4+6 = 10 β
a[lo] can ever work β everything a[lo] could pair with is β€ a[hi]. So I can discard it."Same-direction variant (reader / writer)
In-place dedup, remove-element, move-zeroes β a slow pointer marks where the next kept element goes, a fast pointer scans:
def move_zeroes(a):
write = 0
for read in range(len(a)):
if a[read] != 0:
a[write], a[read] = a[read], a[write]
write += 1
Other members of the family: valid palindrome (ends inward), container with most water, 3Sum (sort + fix one + two-pointer the rest), merging two sorted lists.
05Pattern: Sliding Window
When: "longest / shortest / count of contiguous subarray or substring satisfying a condition."
Why it works: instead of re-examining every one of the O(nΒ²) windows from scratch, you maintain one window and update its state incrementally as the edges move. The right edge grows the window; the left edge shrinks it when the constraint breaks. Each element enters once and leaves once β O(n) β amortized: the inner while is paid for by prior expansions (file 01 Β§5 energy).
"abcabb": both pointers only ever move right, so each character is absorbed once and evicted once β 2n pointer moves total, O(n) despite the nested loop.Longest substring without repeating characters β the archetype:
def longest_unique(s):
last = {} # char β most recent index
left = best = 0
for right, ch in enumerate(s):
if ch in last and last[ch] >= left:
left = last[ch] + 1 # jump past the previous occurrence
last[ch] = right
best = max(best, right - left + 1)
return best
The general template β worth memorizing as a shape, not code:
left = 0
for right in range(n):
# 1. absorb a[right] into window state
while window_is_invalid:
# 2. evict a[left] from window state
left += 1
# 3. window [left..right] is valid β update answer
- Longest valid window β update the answer after the while (as above).
- Shortest valid window (min-window-substring, min-size subarray sum β₯ target) β shrink while valid, updating the answer inside the shrink loop.
- Fixed-size window β no while; slide both edges together (max-average subarray).
06Pattern: Prefix Sums
When: many range-sum queries, or "count subarrays with sum/property X" β especially when negatives make sliding windows invalid.
The idea: precompute running totals once, then any range sum is a subtraction.
a = [ 2, 4, 1, 3, 5]
prefix = [0, 2, 6, 7, 10, 15] (prefix[i] = sum of first i elements)
sum(a[i..j]) = prefix[j+1] - prefix[i] β O(1) per query after O(n) prep
e.g. sum(a[1..3]) = prefix[4] - prefix[1] = 10 - 2 = 8 (4+1+3 β)
The power move β prefix sums + hashmap (Subarray Sum Equals K)
A subarray (i..j] sums to k βΊ prefix[j] β prefix[i] = k βΊ prefix[i] = prefix[j] β k. So walk left to right, and at each point ask: how many earlier prefixes equal (current prefix β k)? A hashmap counts them:
def subarray_sum(a, k):
from collections import defaultdict
seen = defaultdict(int)
seen[0] = 1 # empty prefix β subarrays starting at index 0
total = ans = 0
for x in a:
total += x
ans += seen[total - k] # each match = one subarray ending here
seen[total] += 1
return ans
O(n), handles negatives, and the seen[0] = 1 line is the detail interviewers probe ("why?" β because a prefix that itself equals k forms a valid subarray from the start).
counter(3pm) β counter(2pm) β Prometheus counters + increase() are prefix sums in production clothing.07Classic problems, worked briskly
Try to state the approach and complexity before revealing each answer.
Two Sum (unsorted) β the hashmap hello-world. Given nums and target, return indices of two numbers summing to target.
Brute force is O(nΒ²): all pairs. Better: for each x, its partner is target β x β a lookup, not a search. One pass, store valueβindex as you go. Checking before inserting handles duplicates and "don't use the same element twice" for free.
def two_sum(nums, target):
seen = {}
for i, x in enumerate(nums):
if target - x in seen:
return [seen[target - x], i]
seen[x] = i
O(n) time, O(n) space. The generalizable lesson: "find a pair satisfying a relation" β store what you've seen, look up the complement.
Valid Anagram β are s and t anagrams of each other?
Same letters, same counts. Either sorted(s) == sorted(t) (O(n log n), one line) or count-and-compare (O(n)):
from collections import Counter
def is_anagram(s, t):
return Counter(s) == Counter(t)
O(n) time. For lowercase aβz only, a 26-slot count array works; for general Unicode you need the Counter/dict β mentioning the assumption is the point.
Group Anagrams β cluster a list of strings so anagrams end up together.
Anagrams share a canonical form β use it as a dict key:
def group_anagrams(strs):
from collections import defaultdict
groups = defaultdict(list)
for s in strs:
groups[tuple(sorted(s))].append(s) # or a 26-count tuple for O(nΒ·k)
return list(groups.values())
O(nΒ·k log k) with sorted-tuple keys, O(nΒ·k) with 26-count-tuple keys. Lesson: "group things by equivalence" β design a canonical key, bucket into a hashmap. Same idea as choosing a partition key in Kafka: pick the key so equivalent things land together.
Longest Substring Without Repeating Characters.
Sliding window + last-seen map (Β§05 above): absorb s[right]; if it was already seen inside the window, jump left past the previous occurrence; track the best width. Each pointer only moves forward.
O(n) time, O(min(n, alphabet)) space.
Subarray Sum Equals K β count subarrays whose elements sum to k (negatives allowed).
Sliding window is unsound here (negatives break monotonicity). Use prefix sums + hashmap (Β§06): running total, and at each step add how many earlier prefixes equal total β k. Seed seen[0] = 1 so subarrays starting at index 0 count.
O(n) time, O(n) space.
Honorable mentions to have in your pocket
len(set(a)) < len(a) β a set collapses duplicates.
Prefix products from the left Γ suffix products from the right; no division.
Dump into a set; only start counting from numbers where xβ1 is absent β O(n).
Counter + heap (file 06), or bucket-by-frequency.
08Choosing the pattern β a mini decision tree
Contiguous subarray/substring, longest/shortest/count?
ββ all values non-negative / constraint monotone β SLIDING WINDOW
ββ negatives involved, sum-based β PREFIX SUM (+ hashmap)
Pairs/triples with a target relation?
ββ sorted (or sorting is free) β TWO POINTERS
ββ unsorted, need indices β HASHMAP complement lookup
"Seen before?" / dedupe / grouping β SET / HASHMAP with canonical key
Many range-sum queries β PREFIX SUM
In-place compaction / partition β READER-WRITER POINTERS
09Common interview questions
Q1. Why is dict lookup O(1) and not O(n)?
Hash the key straight to a bucket β arithmetic, not scanning. Collisions add a short chain/probe, kept short by resizing at a load-factor threshold. Average O(1), worst O(n).
Q2. Why can't a list be a dict key?
Mutable β its hash could change after insertion β the entry becomes unfindable β it's filed under the stale hash. Hashability requires stable equality + hash.
Q3. What happens when a hashmap resizes?
New, larger table; every entry re-inserted, because bucket = hash % new_capacity changes. O(n) once, amortized away over inserts.
Q4. Two Sum with duplicates like [3, 3], target 6?
One-pass check-then-insert handles it: the second 3 finds the first in the map before overwriting it.
Q5. Sliding window is O(n) despite nested loops β why?
left only moves forward; each element is added once and removed once. Total pointer movement β€ 2n. (An amortized argument.)
Q6. When does sliding window fail for subarray sums?
Negative numbers: growing the window no longer monotonically grows the sum, so "shrink when too big" is unsound. Use prefix sums + hashmap.
Q7. Anagram check for Unicode vs aβz?
A 26-array works only for lowercase ASCII; use Counter/dict for general alphabets. Stating the assumption out loud is the point.
Q8. Why keep the key inside the hash bucket?
To disambiguate collisions: multiple keys can share a bucket, so lookup must compare actual keys, not just hashes.
10Self-test
Answer from memory, then open the spot-checks.
- Walk through what
d["user:42"] = vdoes, mechanically, in β€ 4 steps (hash β ? β ? β ?). - What is load factor, and what does the map do when it gets too high? What's the cost and why is it acceptable?
- Chaining vs open addressing β one sentence each.
- Write two-pointer Two Sum (sorted) from memory. Then explain why discarding
a[lo]when the sum is too small is safe. - Write the general sliding-window template (3 comment lines are enough). How does it change for "shortest valid window"?
sum(a[i..j])via prefix array β the exact formula, with the off-by-one right.- In Subarray Sum Equals K, why initialize
seen[0] = 1? - Design a canonical key for grouping anagrams. Give two options and their costs.
s += chin a loop over n chars β total complexity and the fix?- Longest Consecutive Sequence in O(n): what's the trick that avoids sorting?
Spot-checks
- hash key β mod capacity β find bucket (probe/chain) β store (key, value).
- entries/buckets; resize + re-insert all, O(n), amortized O(1) per insert.
- Chaining: list per bucket. Open addressing: probe alternative slots in-table.
- Everything
a[lo]could still pair with is β€a[hi], so all its sums are < target. - absorb right / while invalid evict left / record; shortest: shrink while valid and record inside the shrink loop.
prefix[j+1] β prefix[i].- A prefix equal to k means the subarray from index 0 counts.
- sorted-tuple key O(k log k); 26-count tuple O(k).
- O(nΒ²); build a list + join.
- Set membership; only start runs at x where xβ1 is absent, so each run is walked once.