Hashing & Prefix Sums
Trade a little memory for instant lookups, and precompute sums so every range answer is one subtraction.
▶ guided lesson⚡ 1-minute summary↻ 15 recall cards
before this: Arrays & Strings · Big-O · Java Toolkit
01 · why it exists
Why this exists, and the weight it carries
since1953
Hash table
Hans Peter Luhn; Gene Amdahl, Elaine M. McGraw (née Boehme), Nathaniel Rochester, Arthur Samuel (IBM); Arnold Dumey; W. Wesley Peterson
In January 1953 Hans Peter Luhn wrote an internal IBM memo that put keys into numbered buckets and chained the overflow — one of the first uses of linked lists. At about the same time, Gene Amdahl, Elaine McGraw (née Boehme), Nathaniel Rochester and Arthur Samuel used hashing in an assembler for the IBM 701; Amdahl is credited with open addressing with linear probing. Arnold Dumey gave the first published description in 1956, and W. Wesley Peterson's 1957 paper studied open addressing in depth. Knuth's The Art of Computer Programming, Vol. 3 (§6.4), tells the full story.
the problem it solvedFinding a record by its key in about one step, instead of scanning everything or keeping the data sorted.
its jobs today
- Java's
HashMap/HashSet, Python's dict and JavaScript'sMap: the default key → value store in every mainstream language. - Caches and databases: key–value stores like Redis, plus hash indexes and hash joins inside databases.
- Git stores each loose object under
.git/objects/, in a folder picked by the first two hex digits of the object's hash.
the weight it carries
Illustrative: 1 crore (10,000,000) UPI transaction ids, and each new payment must be checked against them for a repeat. Linear scan: up to 1 crore comparisons per check. Sorted array + binary search: ⌈log₂ 10⁷⌉ = 24 comparisons, but every insert shifts data. HashSet: a short chain — fewer than 2 key comparisons on average at load factor 0.75 — for both checks and inserts.
without it
Every "have I seen this?" or "what's stored under this key?" would cost O(n), or O(log n) with sorted structures — caches, compilers and deduplication would all slow down by orders of magnitude.
when · why · how
- when
- You look things up, insert or delete by key, and you don't need the keys in sorted order.
- why
- O(1) average per operation (O(log n) worst case in Java 8+), paid for with O(n) extra memory and no ordering. Need order or nearest-key queries?
TreeMap, O(log n). - how
Map<K, V> map = new HashMap<>();withget,put,getOrDefault,merge,computeIfAbsent;Set<T> seen = new HashSet<>();whereaddreturns false on repeats.
since1998 · 2014
Java's HashMap (resize + treeify)
Doug Lea, Josh Bloch, Arthur van Hoff, Neal Gafter (authors in the source); JEP 180 in JDK 8
java.util.HashMap arrived in JDK 1.2 (December 1998) with the new Collections Framework; its source lists Doug Lea, Josh Bloch, Arthur van Hoff and Neal Gafter as authors.
Its load factor has been 0.75 from the start; today the table starts at 16 buckets and doubles once it gets fuller than that.
JDK 8 (2014) delivered JEP 180: a bucket holding more than 8 entries, in a table of at least 64 buckets, becomes a balanced red-black tree.
So even a flood of colliding keys costs O(log n) per lookup instead of O(n).
the problem it solvedA general-purpose map for any key with a sensible equals/hashCode — fast on average, and protected against keys that all collide.
its jobs today
- Everyday counting and grouping in Java:
mergefor counts,computeIfAbsentfor groups. - Parsing a JSON object into a
Map<String, Object>, caching results by key, deduplicating ids with aHashSet(which is aHashMapinside).
the weight it carries
Illustrative: 10 lakh keys in a default HashMap. The table doubles 16 → 32 → … → 2²¹ = 20,97,152 buckets (2²⁰ × 0.75 = 7,86,432 is below 10 lakh, so one more doubling). Each doubling is O(n), yet all of them together move fewer than 20 lakh entries. new HashMap<>(1_333_334) (10 lakh ÷ 0.75) avoids every resize.
without it
Without resizing, chains would grow without limit and lookups would drift towards O(n); without treeification, many colliding keys would do the same.
when · why · how
- when
- Your default for key → value in Java whenever the order of keys doesn't matter.
- why
- O(1) average. Resizing keeps chains short (amortized O(1) put), and Java 8's trees cap a bad bucket at O(log n). Iteration order is unspecified.
- how
new HashMap<>(), or pre-size withnew HashMap<>(n * 4 / 3 + 1)when you know n. Keys need consistentequals/hashCode— arecordgives you both.
since?
Prefix sums (1D)
Running totals are as old as account books; the technique has no single inventor to credit. Its 2D form does have a clear history — see the summed-area table.
its jobs today
- Dashboards: "revenue between two dates" becomes one subtraction of two cumulative totals.
- Cricket scorecards keep the total after every over: runs between over 10 and over 20 = total after 20 − total after 10.
the weight it carries
Illustrative: 10⁴ daily sales numbers and 10⁴ range questions. Re-adding each range: up to 10⁴ × 10⁴ = 10⁸ additions. Prefix sums: 10⁴ additions once, then 10⁴ subtractions.
without it
Every range question would re-add its range — O(n) per query instead of O(1).
when · why · how
- when
- Many range-sum questions on data that doesn't change — and as the first step of every prefix-sum + HashMap problem.
- why
- O(n) to build, O(1) per query. Any update costs O(n) to rebuild; with frequent updates use a Fenwick tree (O(log n)).
- how
long[] p = new long[n + 1]; for (int i = 0; i < n; i++) p[i + 1] = p[i] + a[i];thensum(l..r) = p[r + 1] - p[l].
since1984
2D prefix sums (summed-area table)
Franklin C. Crow; later Paul Viola & Michael Jones
In 1984 Franklin Crow presented summed-area tables at SIGGRAPH, to filter textures quickly in computer graphics. Each cell stores the sum of everything above and to the left of it, so any rectangle's sum takes four lookups. In 2001 Paul Viola and Michael Jones called it the "integral image" and used it for fast face detection: thousands of rectangle features per image, each in constant time.
the problem it solvedSumming any rectangle of a big image or table in O(1), however large the rectangle is.
its jobs today
- Image processing: box blurs and the Viola–Jones face detector's rectangle features.
- Spreadsheet and heat-map dashboards: the total of any block of rows and columns.
the weight it carries
Illustrative: a 1920 × 1080 image (20,73,600 pixels) and a 100 × 100 box blur. Summing each box directly: 10,000 additions per pixel ≈ 2 × 10¹⁰ in total. With a summed-area table: one pass to build it, then 4 lookups per pixel ≈ 83 lakh.
without it
Every rectangle query would cost its whole area: big blurs and region totals would be far too slow for live video.
when · why · how
- when
- Many rectangle-sum queries on a grid that doesn't change.
- why
- O(r·c) to build, O(1) per query, O(r·c) memory.
- how
p[r+1][c+1] = m[r][c] + p[r][c+1] + p[r+1][c] - p[r][c]; query =p[r2+1][c2+1] - p[r1][c2+1] - p[r2+1][c1] + p[r1][c1].
since?
Prefix sum + HashMap
Pairing running sums with a hash map of earlier sums is a well-known competitive-programming and interview technique; we found no single inventor to credit.
its jobs today
- Ledgers: find stretches of transactions whose credits and debits net to exactly zero.
- Event logs: count time windows whose total hits a target, even when values go up and down (logins +1, logouts −1).
the weight it carries
Illustrative: 2 × 10⁴ numbers (LC 560's limit). Every subarray with a running sum: n(n + 1)/2 ≈ 2 × 10⁸ steps. Prefix sum + HashMap: 2 × 10⁴ lookups and 2 × 10⁴ updates.
without it
"Count subarrays with sum k" with negative numbers would stay O(n²) — a sliding window can't handle negatives.
when · why · how
- when
- Count (or find the longest) subarrays with sum k, equal 0s and 1s, or a sum divisible by k — especially when values can be negative.
- why
- O(n) time and O(n) space versus O(n²). It never assumes the sum grows with the window, so any values work.
- how
seen.put(0, 1);then for each x:sum += x; count += seen.getOrDefault(sum - k, 0); seen.merge(sum, 1, Integer::sum);
02 · plain english
The idea in 30 seconds
A hash function turns a key into an array index, so finding a key is one jump instead of a scan: O(1) on average.
A prefix sum array stores running totals once, so the sum of any range is prefix[r+1] − prefix[l]: O(1) per question.
Together they solve "count subarrays with sum k" in one pass: remember every prefix sum you've seen in a HashMap.
picture this
Hashing is how Git stores your files: it hashes each object's content, and the first two hex digits of that hash pick one of 256 folders under .git/objects/. Finding an object is one jump into one small folder, never a search through all of them. A prefix sum is a cricket scorecard's running total: runs scored between over 10 and over 20 = total after 20 − total after 10.
03 · remember
What you must know
- Hash → bucket:
index = hash(key) mod capacity. Different keys can land in the same bucket (a collision); Java keeps them in a chain (separate chaining) and tells them apart withequals. - Load factor 0.75: when
size > 0.75 × capacity,HashMapdoubles its table and re-hashes every entry. One resize is O(n), but they're rare, soputis O(1) amortized. - Java 8+ treeification: if one bucket grows past 8 entries and the table has at least 64 buckets, that chain becomes a red-black tree, so even a bad bucket costs O(log n), not O(n). With a smaller table, Java resizes instead.
- equals/hashCode contract: equal objects must have equal hash codes. Override both or neither, and never mutate a key while it's inside a map.
- Pick the flavour:
HashMap/HashSet= fastest, no order ·LinkedHashMap= insertion (or access) order ·TreeMap/TreeSet= sorted, O(log n), floor/ceiling queries. - Counting is the #1 hashing pattern:
map.merge(x, 1, Integer::sum). If keys are small integers or letters, anint[]is a faster "perfect" hash table. - One-to-one mappings need two maps: "each letter stands for one word and no two letters share a word" is two rules, so check both directions.
- Prefix sums:
prefix[i+1] = prefix[i] + nums[i]withprefix[0] = 0. The leading zero removes the "range starts at 0" special case. - Prefix sum + HashMap: a subarray
(j..i]sums to k exactly whenprefix[i] − prefix[j] = k. So at each i, ask the map how many earlier prefixes equalprefix[i] − k. This works with negative numbers, where sliding window fails.
04 · see it
Play with it in 3D
Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.
HashMap.put in 3D: hash, bucket, chain
Each key is hashed to a bucket. Collisions chain up; equal keys overwrite instead of duplicating.
Load factor 0.75: resize and rehash
Start small and watch the table double. Every entry moves, because its bucket depends on the capacity.
Build a prefix-sum array, then answer a range in O(1)
Each prefix cell reuses the one before it. A range sum is the difference of two cells.
2D prefix sums as a 3D landscape
Every cell = itself + up + left − up-left. Any rectangle is four lookups.
Prefix sum + HashMap: count subarrays that sum to k
The map remembers every prefix sum seen so far. A hit means a subarray that ends here sums to k.
05 · cost
Time & space, explained
| Average | Worst case | why | |
|---|---|---|---|
| HashMap put / get / containsKey / remove | O(1) | O(log n) | Average: one hash plus a short chain. Worst: everything collides into one bucket, which Java turns into a red-black tree (O(n) before Java 8, or if keys aren't Comparable). |
| HashMap put that triggers a resize | O(1) amortized | O(n) for that one call | Doubling re-hashes all n entries, but it happens only after n/2 cheap puts, so the cost spreads to O(1) each. |
| HashSet add / contains / remove | O(1) | O(log n) | A HashSet is a HashMap with a dummy value, so it inherits the same costs. |
| Iterate a HashMap / HashSet | O(n + capacity) | O(n + capacity) | The iterator visits every bucket, empty or not. This rarely matters, but it explains why huge, mostly empty maps iterate slowly. |
| LinkedHashMap put / get | O(1) | O(log n) | It's a HashMap plus a doubly linked list through the entries, and updating that list is O(1). |
| TreeMap / TreeSet put / get / floorKey / ceilingKey | O(log n) | O(log n) | A red-black tree stays balanced, so its height is always O(log n). |
| int[] counting array (keys in 0..m−1) | O(1) | O(1) | The key is the index, so there's no hashing and no collisions. It costs O(m) memory. |
| Time | Extra space | why | |
|---|---|---|---|
| Build 1D prefix array | O(n) | O(n) | One addition per element, stored in an array of n + 1 slots. |
| Range sum query (after building) | O(1) | O(1) | It's one subtraction: prefix[r+1] − prefix[l]. |
| Range sum without prefix sums | O(n) per query | O(1) | You re-add the range every time, so q queries cost O(n·q). |
| Change one nums[i], then query | O(n) per update | O(n) | Every prefix after i changes. For many updates use a Fenwick or segment tree (Range Queries chapter): O(log n) each. |
| Build 2D prefix (r × c) | O(r·c) | O(r·c) | Each cell is filled once from three neighbours already computed. |
| 2D rectangle query | O(1) | O(1) | Four lookups: whole − above − left + corner. |
| Count subarrays with sum k (prefix + HashMap) | O(n) | O(n) | One pass. Each step does one O(1) map lookup and one O(1) update. |
06 · java
Raw vs Collections Framework
A hash table by hand (separate chaining)
class ChainedHashMap<K, V> { private static final double LOAD_FACTOR = 0.75; private static class Node<K, V> { final K key; V value; Node<K, V> next; Node(K key, V value) { this.key = key; this.value = value; } } private Node<K, V>[] table; private int size = 0; ChainedHashMap(int initialCapacity) { table = newTable(initialCapacity); } /** hashCode → bucket index. The mask drops the sign bit so the index is never negative. */ static int indexFor(Object key, int capacity) { return (key.hashCode() & 0x7fffffff) % capacity; } public void put(K key, V value) { int index = indexFor(key, table.length); for (Node<K, V> node = table[index]; node != null; node = node.next) { if (node.key.equals(key)) { node.value = value; // same key → overwrite, size unchanged return; } } append(index, new Node<>(key, value)); // new key → add to the chain size++; if (size > table.length * LOAD_FACTOR) resize(); // too full? } public V get(K key) { for (Node<K, V> node = table[indexFor(key, table.length)]; node != null; node = node.next) { if (node.key.equals(key)) return node.value; } return null; } /** Double the table and move every entry to its bucket in the new table. */ private void resize() { Node<K, V>[] old = table; table = newTable(old.length * 2); for (Node<K, V> head : old) { for (Node<K, V> node = head; node != null; node = node.next) { append(indexFor(node.key, table.length), new Node<>(node.key, node.value)); } } } private void append(int index, Node<K, V> fresh) { if (table[index] == null) { table[index] = fresh; return; } Node<K, V> tail = table[index]; while (tail.next != null) tail = tail.next; tail.next = fresh; } public int size() { return size; } public int capacity() { return table.length; } @SuppressWarnings("unchecked") private static <K, V> Node<K, V>[] newTable(int capacity) { return (Node<K, V>[]) new Node[capacity]; }}- This is the same idea as
java.util.HashMap, simplified so every line is readable. - The real
HashMapalso mixes the high bits into the low ones (h ^ (h >>> 16)) and useshash & (capacity − 1)instead of%. That's why its capacity is always a power of two (default 16). HashMapallocates its table lazily on the firstput. The default threshold is 16 × 0.75 = 12 entries.- Java 8+ appends new entries to the tail of a chain and turns a chain into a red-black tree past 8 entries (when capacity ≥ 64). If a resize splits it down to 6 or fewer, it turns back into a list.
Counting: int[] vs HashMap
class ArrayCounting { /** Frequency of each lowercase letter. An int[26] is a perfect hash table for 'a'..'z'. */ static int[] letterCounts(String s) { int[] count = new int[26]; for (char c : s.toCharArray()) count[c - 'a']++; return count; } static boolean isAnagram(String a, String b) { return Arrays.equals(letterCounts(a), letterCounts(b)); }}class MapCounting { static Map<String, Integer> countWords(String[] words) { Map<String, Integer> freq = new HashMap<>(); for (String w : words) freq.merge(w, 1, Integer::sum); // insert 1 or add 1 return freq; } static int countOf(Map<String, Integer> freq, String word) { return freq.getOrDefault(word, 0); // no null checks needed } static Map<Integer, List<String>> groupByLength(String[] words) { Map<Integer, List<String>> groups = new HashMap<>(); for (String w : words) groups.computeIfAbsent(w.length(), len -> new ArrayList<>()).add(w); return groups; }}- Use
int[26](orint[128]for ASCII) when keys are small and dense. There's no boxing, no hashing, and it's often several times faster. - Use a
HashMapwhen keys are big, sparse, negative, or not numbers. merge(k, 1, Integer::sum)means "insert 1, or add 1 to what's there".getOrDefaultavoidsnullchecks.computeIfAbsentbuilds a list per key on demand.
Custom keys: the equals/hashCode contract
/** A custom key: equals and hashCode must agree, and both must use only final fields. */final class Point { private final int x; private final int y; Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Point other)) return false; return x == other.x && y == other.y; } @Override public int hashCode() { return 31 * x + y; // equal points → equal hash codes (the contract) }} /** Since Java 16 a record writes equals, hashCode and toString for you. */record Cell(int row, int col) {}- If
a.equals(b)thena.hashCode() == b.hashCode(). The reverse doesn't have to hold: equal hashes can belong to different keys (collisions). - Override
equalswithouthashCodeand two equal points land in different buckets, socontainsreturns false. - Prefer
recordkeys (Java 16+). They are immutable and generate a correctequals/hashCodefor you. - Never use a mutable object as a key and then change it. Its hash changes, and the entry is lost in the wrong bucket.
HashSet vs TreeSet vs LinkedHashSet
class SetFlavours { /** HashSet: O(1) average, but iteration order is unspecified. */ static boolean hasDuplicate(int[] nums) { Set<Integer> seen = new HashSet<>(); for (int x : nums) if (!seen.add(x)) return true; // add returns false if already present return false; } /** TreeSet: O(log n), always sorted, supports floor/ceiling queries. */ static List<Integer> sortedUnique(int[] nums) { TreeSet<Integer> set = new TreeSet<>(); for (int x : nums) set.add(x); return new ArrayList<>(set); } static Integer smallestAtLeast(TreeSet<Integer> set, int x) { return set.ceiling(x); // null if none } /** LinkedHashSet / LinkedHashMap: O(1) average and remembers insertion order. */ static List<Integer> uniqueInFirstSeenOrder(int[] nums) { Set<Integer> set = new LinkedHashSet<>(); for (int x : nums) set.add(x); return new ArrayList<>(set); }}HashSet/HashMapiteration order is unspecified and can change after a resize. Never rely on it.TreeSet/TreeMapkeep keys sorted and addfloor,ceiling,first,lastandheadMap/tailMapin O(log n).LinkedHashMapremembers insertion order. Withnew LinkedHashMap<>(16, 0.75f, true)plusremoveEldestEntry, it becomes an LRU cache (see Linked Lists).
Range sums: re-add every time vs precompute
class LoopEveryQuery { /** Sum of nums[left..right] by walking the range: O(n) per query. */ static long rangeSum(int[] nums, int left, int right) { long sum = 0; for (int i = left; i <= right; i++) sum += nums[i]; return sum; }}class PrefixSum { private final long[] prefix; // prefix[i] = nums[0] + ... + nums[i-1] PrefixSum(int[] nums) { prefix = new long[nums.length + 1]; // prefix[0] = 0: the empty sum for (int i = 0; i < nums.length; i++) { prefix[i + 1] = prefix[i] + nums[i]; } } /** Sum of nums[left..right], inclusive, in O(1). */ long rangeSum(int left, int right) { return prefix[right + 1] - prefix[left]; }}- Precomputing pays off as soon as there is more than a handful of queries: O(n + q) instead of O(n·q).
- Use
longfor prefix sums when values or lengths are large. A running total overflowsintlong before any single element does.
Library prefix sums and 2D prefix sums
class PrefixSum2D { private final long[][] p; // p[r][c] = sum of the rectangle from (0,0) to (r-1,c-1) PrefixSum2D(int[][] grid) { int rows = grid.length, cols = rows == 0 ? 0 : grid[0].length; p = new long[rows + 1][cols + 1]; // extra zero row and column for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { // up + left counts the up-left block twice, so subtract it once p[r + 1][c + 1] = grid[r][c] + p[r][c + 1] + p[r + 1][c] - p[r][c]; } } } /** Sum of the rectangle (r1,c1)..(r2,c2), inclusive, in O(1). */ long sumRegion(int r1, int c1, int r2, int c2) { return p[r2 + 1][c2 + 1] - p[r1][c2 + 1] - p[r2 + 1][c1] + p[r1][c1]; }}class LibraryPrefix { /** Arrays.parallelPrefix turns an array into its running sums in place (inclusive, no leading 0). */ static long[] runningSums(int[] nums) { long[] out = Arrays.stream(nums).asLongStream().toArray(); Arrays.parallelPrefix(out, Long::sum); return out; }}Arrays.parallelPrefixgives inclusive running sums (no leading 0). Interviewers usually expect the hand-written loop.- In 2D, the extra zero row and column play the same role as
prefix[0] = 0in 1D.
| call | does | cost |
|---|---|---|
map.put(k, v) / map.get(k) | Insert/overwrite, or read (null if missing) | O(1) avg |
map.getOrDefault(k, 0) | Read with a fallback, so there's no null check | O(1) avg |
map.merge(k, 1, Integer::sum) | Insert 1, or add 1 to the existing count — returns the new count | O(1) avg |
map.computeIfAbsent(k, x -> new ArrayList<>()).add(v) | Group values under a key | O(1) avg |
map.putIfAbsent(k, v) | Store v only if k is new; returns the old value (or null) — handy for one-to-one checks | O(1) avg |
map.containsKey(k) / map.remove(k) | Check membership / delete | O(1) avg |
map.entrySet() | Iterate key–value pairs together (faster than keySet + get) | O(n + capacity) |
set.add(x) / set.remove(x) | Return false if x was already there / wasn't there: a free membership check | O(1) avg |
treeMap.floorKey(k) / ceilingKey(k) | Largest key ≤ k / smallest key ≥ k | O(log n) |
new HashMap<>(expected * 4 / 3 + 1) | Pre-size to avoid resizes when you know the count | O(capacity) |
Objects.hash(a, b, c) / Objects.equals(a, b) | Quick hashCode for custom keys (boxes its arguments) / null-safe equality for boxed values | O(fields) / O(1) |
Math.floorMod(x, k) | Remainder in 0..k−1 even for negative x | O(1) |
"Have I seen this before?"
→ HashSet
O(1) average add/contains, and add returning false is the check.
Count occurrences
→ int[] if keys are small and dense, else HashMap + merge
The array skips hashing and boxing. The map handles any key.
Value → index (complement lookups)
→ HashMap<Integer, Integer>
One-pass Two Sum style lookups.
One-to-one pairing (pattern ↔ word)
→ Two maps, one per direction (or two int[256] for chars)
One map can't notice two keys sharing the same value.
Keys in sorted order, or nearest key ≤ / ≥ x
→ TreeMap / TreeSet
O(log n) with ordered queries a hash map can't do.
Iterate in the order keys were inserted
→ LinkedHashMap / LinkedHashSet
Hash speed plus predictable order.
Many range-sum queries on a fixed array
→ Prefix-sum array
O(n) once, then O(1) per query.
Range sums with frequent updates
→ Fenwick / segment tree
O(log n) update and query, where prefix sums need O(n) per update.
07 · recognise
Spot the pattern
"Find a pair / complement" in an unsorted array, return indices
HashMap value → index, look up target − x before storing x
"Duplicate", "seen before", "unique", "first repeating"
HashSet (add returns false on repeats)
"Frequency", "most common", "anagram", "top k"
Counting map or int[26]; then bucket sort or a heap
"Can you build X from the letters of Y?"
Count Y's letters, then spend them on X (int[26])
"Longest palindrome you can build" from given letters
Use every pair; one leftover letter can sit in the middle
"Pattern", "isomorphic": each symbol must stand for exactly one thing
Two maps (or two arrays), one per direction
"Sum of a range" asked many times on a fixed array
Prefix sums: build once, answer in O(1)
"Number of subarrays with sum = k" and values can be negative
Prefix sum + HashMap of counts (sliding window breaks with negatives)
"Longest subarray with equal 0s and 1s" / "balanced"
Map 0 → −1, then the longest subarray with sum 0. Store the first index of each prefix
"Divisible by k", "multiple of k"
Prefix sums mod k: equal remainders bracket a divisible subarray
O(n) required and sorting is too slow
Hash set or map: trade O(n) memory for time
08 · traps & tricks
Mistakes to avoid, tricks to use
traps
- Forgetting
seen.put(0, 1)(orprefix[0] = 0). Subarrays that start at index 0 are silently missed. - Storing into the map before looking up. In Two Sum,
[3]with target 6 would pair 3 with itself. - Comparing
Integerobjects with==or!=. It works only for −128..127 (the Integer cache), then breaks. Use.equals,Objects.equals, or unbox toint. - Java's
%can be negative:-7 % 5 == -2. UseMath.floorMod(x, k)or((x % k) + k) % kfor remainders. - Overriding
equalswithouthashCode, or mutating a key after inserting it. The entry becomes unreachable. - Relying on
HashMapiteration order. It's unspecified and may change after a resize or between Java versions. - Using sliding window for "subarray sum = k" with negative numbers. Growing the window no longer increases the sum.
- Iterating
numsinstead of theHashSetin Longest Consecutive Sequence. With many duplicates, sequence starts get re-walked and it degrades towards O(n²). - Checking a one-to-one mapping in only one direction (Isomorphic Strings, Word Pattern): two keys can quietly share one value.
tricks
- Say "O(1) average" in interviews, and mention the O(log n) worst case from Java 8 treeification. It shows depth.
- Name the map by its meaning:
seen,indexOf,countOf,firstIndexOfBalance. Half the bugs vanish. - For prefix-sum problems, write the identity on the whiteboard first:
sum(j..i) = P[i+1] − P[j]. Then the map key falls out. - Before reaching for a map, ask: are the keys small integers? Then an array is simpler and faster.
- Dry-run with a tiny input that has a 0 or a negative. These break naive window solutions and confirm your map logic.
- Pre-size maps when you know the size:
new HashMap<>(n * 4 / 3 + 1)avoids every resize (Java 19+ also hasHashMap.newHashMap(n)). - Bas itna yaad rakho: a map answers "what do I know about this key?"; a prefix array answers "how much up to here?". Most problems in this chapter are one of those two questions.
09 · why & how
Questions you might be too shy to ask
If collisions exist, how can lookups be O(1)?
Because collisions are rare when the table isn't too full. With load factor ≤ 0.75 and a good hash function, the expected chain length is a small constant, so you check about one or two keys. The worst case (everything in one bucket) is O(n) for a plain chain. Since Java 8 a long chain becomes a red-black tree, which brings it down to O(log n) (guaranteed when the keys are Comparable, like Integer and String).
What is a hash code, really? Is it the object's memory address?
It is just an int that hashCode() returns. HashMap squeezes it down to a bucket number.
Each class decides how to compute it: Integer.valueOf(5).hashCode() is 5, and a String mixes its characters ("ab".hashCode() is 97 * 31 + 98 = 3105).
The default Object.hashCode() is identity-based, but the Java spec does not promise it is an address — never treat it as one.
Why does HashMap resize at 0.75, not 1.0?
It's a speed–memory trade-off. A fuller table means more collisions and longer chains; an emptier one wastes buckets. The HashMap Javadoc calls the default 0.75 "a good tradeoff between time and space costs"; you can pass another value to the constructor, but you rarely need to.
Why is a resize not a disaster if it touches every element?
Because it doubles the capacity. A resize from capacity m to 2m happens at 0.75m entries, and the next one only at 1.5m, so another 0.75m cheap puts come first: as many as the entries just copied. Spread over those puts, the O(n) copy adds only O(1) per put. That's what amortized O(1) means.
What exactly is the equals/hashCode contract, and what breaks if I ignore it?
Equal objects must return the same hashCode. HashMap first jumps to the bucket chosen by hashCode, and only then calls equals. If two equal points return different hash codes, the lookup usually checks the wrong bucket. Even in the same bucket, HashMap compares the hash codes before it calls equals. Either way it never calls equals and says "not found".
Can I use an int[] as a HashMap key, like [1, 2]?
Technically yes, but it won't work the way you want. Arrays do not override equals/hashCode, so two arrays with the same numbers are different keys.
new int[]{1, 2}.equals(new int[]{1, 2}) is false.
Use a key whose equals compares contents: a String like Arrays.toString(arr), a List.of(1, 2), or a record.
Dhyan do: never change a list after using it as a key.
HashMap or TreeMap?
Default to HashMap: O(1) average. Pick TreeMap only when you need order: sorted iteration, smallest/largest key, or "nearest key ≤ x". Then O(log n) is the price of that order.
Why does the prefix array have n + 1 slots?
prefix[0] = 0 stands for "sum of nothing". Then the sum of nums[l..r] is always prefix[r+1] − prefix[l], including when l = 0. Without it you'd need an if for ranges that start at the beginning.
Why put 0 → 1 in the map for Subarray Sum Equals K?
It's the HashMap version of prefix[0] = 0. If the running sum itself equals k, the matching "earlier prefix" is the empty prefix, and the map must already contain it.
I keep mixing up sum - k and k - sum, and why do we add a count instead of just 1?
A subarray ending here sums to k when current - earlier = k, so the earlier prefix must be current - k. Write that line first, then the code follows.
Several earlier prefixes can have that same value. Each one is the start of a different subarray.
Example: [1, -1, 1], k = 1. Prefixes are 0, 1, 0, 1. At the last 1 we need 0, seen twice → [1, -1, 1] and [1]. So add 2, not 1.
Subarray Sum Equals K stores a count, Contiguous Array stores the first index. How do I know which to store?
Store what the question asks about. "How many subarrays?" → store how many times each prefix appeared. "Longest subarray?" → store the first index of each prefix, and never overwrite it (the earliest start gives the longest span). "Shortest subarray?" → store the latest index, overwriting each time.
When do I use sliding window and when prefix sum + map?
Sliding window needs a monotonic sum: all numbers non-negative, so growing the window never decreases the sum. With negatives, or when you must count exact sums, use prefix sum + HashMap.
Isomorphic Strings: one map from s to t feels enough. Why two?
One map only checks that each character of s always becomes the same character. It can't see two different characters becoming the same one: "badc" → "baba" maps b→b and d→b without any conflict in the s → t map. The second map, t → s, catches exactly that.
I gave a HashMap solution and the interviewer asked, "Can you do it without extra space?" What now?
Don't panic — they are testing whether you know the trade-off. The usual answer is sorting: Contains Duplicate becomes "sort, then compare neighbours" in O(n log n) time and only the sort's own memory. Say the cost out loud: "I save memory, but time goes from O(n) to O(n log n), and sorting in place changes the input. Is that okay?" If you need original indices (Two Sum), sorting loses them, so mention that too.
10 · practice
Interview problems
Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.
- #1
- #217
- #383Ransom NoteEasyAppleMicrosoftAmazon
- #409Longest PalindromeEasyGoogleAmazonMicrosoft
- #205Isomorphic StringsEasyLinkedInBloombergAmazonGoogle
- #290Word PatternEasyBloombergGoogleAmazon
- #128
- #36Valid SudokuMediumAppleUberAmazonMicrosoft
- #303
- #304Range Sum Query 2D - ImmutableMediumGoogleAmazonMeta
- #560
- #525Contiguous ArrayMediumMetaAmazonGoogle
- #974Subarray Sums Divisible by KMediumAmazonMeta
Also practise this here
Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.
- #347Top K Frequent ElementsMediumin Heaps
Count with a HashMap first; bucketing values by frequency then finishes the job in O(n) (its bucket-sort approach).
- #49Group AnagramsMediumin Arrays & Strings
Hashing with a canonical key: every anagram produces the same letter-count string, so a HashMap buckets them in one pass.
11 · revise
The whole chapter on one screen
10 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.
- what it is
A hash table turns a key into a bucket index (
hash & (capacity − 1)), so get / put / contains cost O(1) on average. - what it is
A prefix sum stores running totals once:
sum(l..r) = p[r + 1] − p[l]— one subtraction per query. - use it when
Seen before? → HashSet. Value → index or count → HashMap. Many range sums → prefix array. Sum = k with negatives → prefix + map.
- cost
HashMap: O(1) average, O(log n) worst (Java 8+ trees), O(n) memory. Prefix sums: O(n) build, O(1) query, O(n) per update.
- template
seen.put(0, 1);then per x:sum += x; count += seen.getOrDefault(sum - k, 0); seen.merge(sum, 1, Integer::sum); - trap
Override
equalswithouthashCode(or change a key afterput) and the entry is lost: lookups go to the wrong bucket. - trap
Integerobjects compared with==or!=work only for −128..127. Use.equals,Objects.equals, or unbox toint. - trap
Java's
%can be negative:-7 % 3 == -1. UseMath.floorMod(x, k)when the remainder is a map key. - pro tip
Keys small and dense (letters, 1..n, remainders)? An
int[]is a perfect hash table — no hashing, no boxing. - pro tip
One-to-one mapping (isomorphic, word pattern)? Two maps, one per direction — a single map can't see two keys sharing a value.
12 · recall
Flip, remember, repeat
HashMap get/put in Java 8+: average and worst-case cost?
Question 1: HashMap get/put in Java 8+: average and worst-case cost?
13 · check yourself
Quick quiz
A HashMap has capacity 16 and the default load factor. On which put does it resize?
Two keys have the same hashCode but are not equal. What does HashMap do?
You override equals in Point but not hashCode. What usually happens to `set.contains(new Point(1, 2))` after adding an equal point?
nums = [2, 4, 1, 3]. prefix = [0, 2, 6, 7, 10]. What is the sum of nums[1..3]?
Why does Subarray Sum Equals K need a HashMap instead of a sliding window?
In Java 8+, when does a HashMap bucket become a red-black tree?
In Java, what is `-7 % 3`?
With only a map s → t, which pair would be wrongly reported as isomorphic?
What is the longest palindrome you can build from the letters of "aaabbbcc"?
14 · learn more
A second teacher, if you want one
- ▶ videoHashing | Maps | Time Complexity | Collisions | Division Rule of Hashing | Strivers A2Z DSA Course ↗Striver (take U forward) · EnglishHow hashing turns 'how many times did I see this?' into an O(1) lookup, covering frequency arrays, maps, collisions and the division method.
- ▶ videoCount Subarray sum Equals K | Brute - Better -Optimal ↗Striver (take U forward) · EnglishThe prefix sum + HashMap trick, built up slowly from brute force. It's the hardest idea in this chapter.
- ✎ articleIntroduction to Prefix Sums ↗USACO Guide · EnglishRange sums in O(1) after one pass, with Java code and a graded list of practice problems when you want more reps.