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
Are two words anagrams?
Two words are anagrams when every letter occurs equally often in both. One table of counts, added for one word and taken away for the other.
Open the problem → Easy10 minPair 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 → Easy10 minA repeated value within k positions
Two equal values at most k apart. Keep the last index of every value and the check costs one lookup per element.
Open the problem → Easy15 minFind every number from 1 to n that is missing
It's the same sign marking you'd use to find duplicates, only this time you read the answer from the slots that never got marked.
Open the problem → Easy10 minCut a note out of a magazine
You count the letters the magazine gives you and then spend them on the note one at a time, and the first count that would go below zero tells you the answer is no.
Open the problem → Easy15 minCan one word be renamed into another?
You keep a map from the letters of s to the letters of t, and the part people miss is that you need one going back the other way as well.
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 → 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 minFind every value that appears twice
With a set this is a few lines, and the space limit takes the set away. Since every value is between 1 and n, the sign of nums[v − 1] can remember whether you've met v already.
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 → Medium20 minGroup the words that are anagrams
Two words are anagrams when they have the same letter counts, so the whole trick is to turn each word into a key that depends on nothing else and then let a hash map do the grouping for you.
Open the problem → Medium20 minLongest subarray with a given sum
I'd call this the counting problem's twin, with the same prefix map and one change, which is that you keep the first index of every total.
Open the problem → Medium20 minCount subarrays with a sum divisible by k
You count ranges the way you did for a sum of k, except that the key in the map is each prefix's remainder mod k, and negative remainders need fixing.
Open the problem → Medium15 minIs this Sudoku board valid so far?
Every filled cell has to be new in its row, its column and its 3 by 3 box, so you keep a seen set for each of the 27 units and the only real thinking is which box a cell is in.
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.