core idea
A hashmap turns “have I seen this before?” and “where did I see it?” into O(1) operations. You trade O(n) space for the lookup, and in exchange you get to process unsorted data in one pass — no sort step, no lost original indices. The whole skill is picking the right key. Four shapes cover almost every hashmap problem you’ll meet:
| shape | key → value | answers |
|---|---|---|
| value → index | Dictionary<int, int> |
“where did I see the number I need?” |
| value → count | Dictionary<T, int> |
“how many times has this appeared?” |
| prefixSum → count | Dictionary<int, int> |
“how many earlier running totals make a valid range?” |
| membership | HashSet<T> |
“have I seen this at all?” (no value needed, just yes/no) |
when to reach for it
- The brute force is a nested loop checking every pair or every prior element — and the input is not sorted, or sorting would destroy something you need (original indices, order).
- The question is phrased as “have I seen X before” or “how many times does X occur” —
HashSet<T>membership answers the first in one line; that one line, on its own, is the entire trick behind Contains Duplicate (LC 217). - You need a running total (sum, count, balance) and the question is “how many sub-ranges hit a target” — that’s prefixSum → count.
- Grouping or bucketing items by some derived property (“same letters”, “same remainder”) — the derived property becomes the key.
key = what I need
Before reaching for a dictionary, ask: “what do I wish I already knew about the elements I’ve passed?” Whatever that is becomes the key. Two Sum: “is my complement already here?” → key is the value. Subarray Sum: “has this running total shown up before?” → key is the prefix sum.
universal templates
value → index — the lookup that turns O(n²) pair-checking into O(n):
public int[] ValueToIndex(int[] nums, int target)
{
var seen = new Dictionary<int, int>(); // value -> index already seen
for (int i = 0; i < nums.Length; i++)
{
int need = target - nums[i]; // "key = what I need"
if (seen.TryGetValue(need, out int j)) return [j, i];
seen[nums[i]] = i; // record AFTER checking — can't pair with itself
}
return [];
}
value → count — the frequency table underneath anagrams, majority elements, and top-K:
public Dictionary<int, int> Frequencies(int[] nums)
{
var counts = new Dictionary<int, int>();
foreach (int n in nums)
counts[n] = counts.GetValueOrDefault(n) + 1; // 0 the first time, then increments
return counts;
}
prefixSum → count — every “how many subarrays sum to k” question is this loop:
public int PrefixSumToCount(int[] nums, int k)
{
var prefixCount = new Dictionary<int, int> { [0] = 1 }; // empty prefix seen once
int sum = 0, count = 0;
foreach (int n in nums)
{
sum += n;
count += prefixCount.GetValueOrDefault(sum - k); // earlier prefixes that complete a sum-k run
prefixCount[sum] = prefixCount.GetValueOrDefault(sum) + 1;
}
return count;
}
membership — the whole idea in three lines:
public bool HasDuplicate(int[] nums)
{
var seen = new HashSet<int>();
foreach (int n in nums)
{
if (!seen.Add(n)) return true; // Add returns false when the value was already present
}
return false;
}
Every problem below is one of these four skeletons with a different key.
problems
The dictionary key answers: "who is waiting for this number?"
value→count frequency table; int[26] as the specialized dictionary.
Canonical form as the dictionary key — group by signature.
HashSet membership + only start counting at sequence starts.
prefixSum→count: the hashmap remembers every running total seen so far.
The dict+list swap-remove idiom — a design question in disguise.
cheat sheet — hashmap
recognize it
- "have I seen this before?" / "how many times has X occurred?" ->
HashSet<T>membership orDictionary<T,int>frequency count - unsorted array + pair/sum question, or sorting would destroy something you need (original indices) -> value->index dictionary
- "how many subarrays / sub-ranges sum to k" -> prefixSum->count dictionary
- grouping or bucketing items by a derived property ("same letters", "same remainder") -> canonical-key grouping
key tricks
- "key = what I need": ask what you wish you already knew about the elements you've passed - that's the dictionary key
- seed
prefixCount[0] = 1before scanning subarray-sum problems - covers a run that starts at index 0 int[26]beatsDictionary<char, int>when the key space is small and fixed (lowercase letters, digits)- check the complement/target BEFORE inserting the current element into the map, so nothing ever pairs with itself
- swap-with-last (
values[idx] = values[^1], then drop the tail) turns O(n) arbitrary removal into O(1)
common bugs
- inserting into the dictionary before checking for the complement - lets an element pair with itself
- forgetting the
prefixCount[0] = 1seed - silently undercounts subarrays that start at index0 - sorting an array to "make it easier" when the problem needs original indices - sorting is exactly what destroys them
dict[key]throwsKeyNotFoundExceptionon a miss - reach forTryGetValue/GetValueOrDefaultinstead of guarding manually