Arrays and hashing
Trade memory for time: a hash map answers "have I seen this?" in O(1), and most array problems reduce to asking it the right question.
When it applies
Reach for a hash map or set when the work you are repeating is a lookup — "have I seen this value", "how many times", "what index was it at". Each of those is O(n) by scanning and O(1) by hashing, and that single change is what turns a quadratic solution linear.
The harder skill is choosing what to key on. Keying on the value solves membership; keying on a running total solves range sums; keying on a sorted signature groups anagrams. Get the key right and the problem usually collapses to one pass.
Signals: "count the number of", "find duplicates", "group by", "subarray summing to", "seen before", "in O(n) time". If the input is already sorted, check whether two pointers does the same job in O(1) space before you spend O(n) memory.
The template
Write this from memory. Every problem in this section is a specialisation of it, and most bugs come from deviating without a reason.
seen = {} # key -> whatever you need back
for i, x in enumerate(a):
key = derive(x) # the whole problem is choosing this
if key in seen:
use(seen[key], i) # look up BEFORE inserting, or you match x with itself
seen[key] = iunordered_map<long long, int> seen; // key -> whatever you need back
for (int i = 0; i < (int)a.size(); i++) {
long long key = derive(a[i]); // the whole problem is choosing this
auto it = seen.find(key);
if (it != seen.end()) use(it->second, i); // look up before inserting
seen[key] = i;
}Map<Long, Integer> seen = new HashMap<>(); // key -> whatever you need back
for (int i = 0; i < a.length; i++) {
long key = derive(a[i]); // the whole problem is choosing this
Integer prev = seen.get(key);
if (prev != null) use(prev, i); // look up before inserting
seen.put(key, i);
}const seen = new Map(); // key -> whatever you need back
for (let i = 0; i < a.length; i++) {
const key = derive(a[i]); // the whole problem is choosing this
if (seen.has(key)) use(seen.get(key), i); // look up before inserting
seen.set(key, i);
}let mut seen: HashMap<i64, usize> = HashMap::new(); // key -> what you need back
for (i, &x) in a.iter().enumerate() {
let key = derive(x); // the whole problem is choosing this
if let Some(&prev) = seen.get(&key) { use_it(prev, i); } // look up before inserting
seen.insert(key, i);
}import qualified Data.Map.Strict as M
-- fold the array, carrying the map and the answer together
solve :: [Int] -> Int
solve = snd . foldl step (M.empty, 0) . zip [0 ..]
where
step (seen, acc) (i, x) =
let key = derive x -- the whole problem is choosing this
acc' = case M.lookup key seen of -- look up before inserting
Just prev -> use prev i acc
Nothing -> acc
in (M.insert key i seen, acc')
Problems
Pair with a given sum in an unsorted array
Every pair is n² work. Remembering what you have already passed makes it n, and the order of two lines decides whether it is right.
Open the problem → Medium15 minLongest run of consecutive integers
Sorting gives the answer in O(n log n). A set gives it in O(n), and the reason it is not O(n²) is the interesting part.
Open the problem → Medium15 minThe k most frequent values
A hash map does the counting in one pass. The interesting part is ordering what you counted, and saying precisely how ties break.
Open the problem → Medium20 minCount subarrays with a given sum
Negative numbers break the sliding window. Prefix sums in a hash map fix it, and the counting detail is where people slip.
Open the problem → Medium15 minProduct of every element except itself
Dividing the total by each element is everyone's first answer, and one zero breaks it. Two passes fix it without dividing at all.
Open the problem → Hard25 minSmallest positive integer that is missing
A hash set solves it in one line and is not allowed. The array can be its own hash table, once you see that only n + 1 answers are possible.
Open the problem →Or sit a timed interview: a fixed window, limited submissions, and a report at the end.