Hashing
Hash tables from scratch — the principle, hash set vs map, collision resolution, three reusable templates (find duplicates, aggregate by key, design the key), and the prefix-sum + map engine for subarray problems.
After this you should be able to
- Explain the core idea in Hashing
- Apply Hashing in a representative exercise
Everything I need on hash tables: the principle, how to design one from scratch, the reusable templates (find-duplicates, aggregate-by-key, design-the-key), and the problem families they unlock — including the prefix-sum + hash-map engine behind subarray problems. Built to be the only thing I revise from. Java throughout.
I. Notebook Summary (copy by hand) #
Card-sized blocks, hand-copyable and deck-ready. Telegraphic — arrows over sentences.
II. Detailed Study Notes #
Concepts first, then problems grouped by the technique they teach. Each opens with a one-line claim; [+] marks my additions. Code cross-links to Section III.
1 · The Principle of a Hash Table foundation #
A hash table organises data with a hash function so insertion and search are average O(1) instead of O(n).
The whole idea is to map keys to buckets. On insert, the hash function decides which bucket a key belongs to and stores it there. On search, the same hash function recomputes that bucket and you scan only that one bucket — never the whole table.
Worked example
with hash function y = x % 5: key 1987 → 1987 % 5 = 2
→ bucket 2; key 24 → bucket 4. To search for 1987, recompute → bucket 2 → found. To search 23 → bucket 3
→ not there → 23 isn't in the table. [+] Note 1987 and 2 both map to bucket 2 —
that's a collision, handled below.
→ code: PrincipleDemo.java
2 · Two Keys to Designing a Hash Table foundation #
Designing a hash table is balancing two things: the hash function and the collision-resolution strategy.
Factor 1 — the hash function#
The hash function maps a key to a bucket index and depends on the range of keys and the number of buckets. Designing a good one is an open problem: the aim is to scatter keys as uniformly as possible. A perfect hash function would be a one-to-one key→bucket mapping, but that's rare — in practice it's a trade-off between the number of buckets and the capacity of each bucket. More buckets → fewer collisions but more memory; fewer buckets → less memory but longer chains.
Factor 2 — collision resolution#
Collisions are almost inevitable (e.g. with y = x % 5, both 1987 and 2 land in bucket 2). A collision strategy must answer: how do we organise values in the
same bucket, what happens when too many pile up, and how do we search within a bucket? Let
N
= the size of the largest bucket:
- N small and constant → store the bucket's keys in a plain array / list and scan it. [+] This is "separate chaining."
-
N large or variable
→ use a height-balanced BST
in the bucket so lookups are O(log N) instead of O(N).
[+] This is exactly what Java's
HashMapdoes — it tree-ifies a bucket once its chain exceeds 8 nodes and the table holds at least 64 buckets (MIN_TREEIFY_CAPACITY). Below that it resizes instead, on the theory that a small crowded table is under-sized rather than genuinely collision-prone. The tree reverts to a list if a later resize drops the bin to 6 entries. One catch worth knowing: the tree orders entries by hash, so when the hashes themselves all collide it needs the keys to implementComparableto break the tie. Keys with onlyequals/hashCodefall back to a scan, and the bucket stays O(N) despite being a "tree".
→ code: MyHashSet.java, MyHashMap.java
3 · Hash Set vs Hash Map foundation #
A hash set stores values (no duplicates); a hash map stores key→value pairs. Same machinery, different question.
| Hash Set | Hash Map | |
|---|---|---|
| Stores | Values only | (key, value) pairs |
| Answers | "Have I seen x?" | "What information is attached to key x?" |
| Core ops |
add, remove, contains
|
put, get, remove, containsKey
|
| Use when | You only care about presence/uniqueness | You need to look up extra info by key |
Both give average O(1) insert/search/remove. Reach for a set when a boolean "seen it?" is enough; reach for a map the moment you also need which index / how many / what value.
→ code: HashUsageDemo.java
4 · Making Bucket Removal O(1) foundation · [+] depth #
Removing from an array-backed bucket is O(n); two tricks bring it to O(1).
When you remove the i-th element of an array list of size n, the built-in shifts every later element one
slot forward — that's n − i
moves, O(n) worst case (on average (n−1)/2
). Two ways to avoid the shift:
- Swap-with-last. Swap the target with the last element, then delete the last. O(1) — but element order is destroyed (fine for a set, and the key trick behind Insert/Delete/GetRandom O(1) ).
- Linked list bucket. Splice the node out in O(1) and keep insertion order.
5 · The Three Reusable Templates framework #
Most hash problems are one of three templates: find duplicates, aggregate by key, or design the key.
Template A — Find duplicates (hash set)#
A set is the natural tool for "has this value ever appeared?" Iterate; if the value is already present you've found a duplicate, otherwise add it. This is the skeleton for Contains Duplicate, cycle detection in Happy Number, and the "seen" check in many others.
Template B — Aggregate by key (hash map)#
When you need to collect information per key — a count, a sum, a latest value — use a map and decide your update strategy on an existing key. That strategy is the problem: count occurrences (First Unique Character), sum values, or overwrite with the newest. [+] Naming the update rule explicitly is the fastest way to turn a vague problem into code.
Template C — Design the key#
Sometimes the obvious key won't group things correctly and you must build a canonical key. A valid key-design function must satisfy both: (1) everything that belongs together maps to the same key, and (2) things that must stay apart never collide. (A hash function only guarantees rule 1 — this is stricter.) Example: group anagrams by their sorted letters, so "eat" and "ate" both become "aet".
- Order doesn't matter → sorted string/array as key (anagrams).
- Only the offset matters → use the offset/diff pattern as key (shifted strings).
- Tree subtree grouping → serialize the subtree as key (duplicate subtrees).
-
Matrix grouping → use row, column, or a block id
(Sudoku:
(r/3, c/3)); diagonals:r−corr+c.
→ code: Templates.java
6 · Contains Duplicate hash set · Template A #
Return true if any value appears at least twice. [1,2,3,1] → true.
Practice: LeetCode 217
The textbook Template-A problem. Walk the array, insert each value into a set; the first time contains
is true, return. Brute force compares all pairs in O(n²); the set makes each "seen before?" check O(1) →
O(n) total.
new HashSet<>(list).size() != list.size()
there was a duplicate. The streaming version above is better when you want to stop early.
→ code: ContainsDuplicate.java
7 · Single Number XOR (beats hashing) #
Every element appears twice except one; find the loner. [4,1,2,1,2] → 4.
Practice: LeetCode 136
The hash-set solution toggles membership (add on first sight, remove on second; whatever remains is the answer) — O(n) time, O(n) space. But there's a better tool.
a ^ a = 0
and a ^ 0 = a, XOR-ing all elements cancels every pair and leaves the unique value. O(n) time, O(1)
space
— no hash table at all. This is the same self-inverse property that powers the XOR-subarray problems.
→ code: SingleNumber.java
8 · Happy Number hash set · cycle detection #
Repeatedly replace n with the sum of squares of its digits; it's "happy" if it reaches 1. 19 → 1 (happy).
Practice: LeetCode 202
The process either reaches 1 or loops forever. Use a set to remember every number seen; if you revisit one, you're in a cycle → not happy. If you hit 1, it's happy.
→ code: HappyNumber.java
9 · Two Sum hash map (value→index) · Scenario I #
Return the indices of the two numbers that add to target
(one solution, no reuse). [3,2,4], t=6 → [1,2].
Practice: LeetCode 1
This is the canonical "provide more information
" case. If you only needed to know whether
a pair exists, a set would do — store values, check for target − x. But you must return indices, so the key is the value and the value is its index. For each x, look up the complement target − x; if present, you're done.
→ code: TwoSum.java
10 · Isomorphic Strings map + set (bijection) · Scenario I #
Two strings are isomorphic if a consistent one-to-one character replacement turns one into the other.
ACAB ↔ XCXY
✓ (A→X, B→Y, C→C).
Practice: LeetCode 205
This is the "check the value first before acting" twist on Scenario I. Keep a map x → y
and a set of already-used y
's. If x
was seen, it must map to the same y. If x
is new, y
must be unused — then record it.
Character, compare via .equals()
— ==
only works by luck for cached small values.
→ code: Isomorphic.java
11 · First Unique Character frequency map · Scenario II #
Return the index of the first non-repeating character, or −1. "leetcode" → 0.
Practice: LeetCode 387
The textbook "aggregate by key " problem. Pass 1: count every character's occurrences in a map (or size-26 array). Pass 2: scan the string left to right and return the first character whose count is 1.
→ code: FirstUnique.java
12 · Group Anagrams design the key · Template C #
Group words that are anagrams of each other. ["eat","tea","tan","ate","nat","bat"] → [[eat,tea,ate],[tan,nat],[bat]].
Practice: LeetCode 49
You can't key on the raw string ("eat" ≠ "ate" as strings, but they're the same group). Design a
canonical key:
sort each word's letters — "eat" and "ate" both become "aet" — and bucket words by that key in a Map <String, List<String>>.
"1#0#0#...#2"
is an O(L) key vs O(L log L) sorting.
→ code: GroupAnagrams.java
13 · Longest Substring Without Repeating Characters hash set + sliding window #
Length of the longest substring with all distinct characters. "abcabcbb" → 3
("abc").
Practice: LeetCode 3
Combine a hash structure with a sliding window. Expand the right edge; keep a set (or a map of char→last index) of the window's characters. On a repeat, shrink the left edge until the window is valid again, tracking the max length.
→ code: LongestSubstr.java
14 · Top K Frequent Elements frequency map + heap / bucket sort #
Return the k most frequent values. [1,1,1,2,2,3], k=2 → [1,2].
Practice: LeetCode 347
Count frequencies in a map (Template B), then select the top k. Two ways: a min-heap of size k over the entries — O(n log k); or bucket sort where bucket[f] holds all values with frequency f, scanned from high to low — O(n).
→ code: TopKFrequent.java
15 · Insert Delete GetRandom O(1) map + dynamic array · swap-with-last #
Design a structure with average O(1) insert, remove, and random-element. [+] The capstone "combine structures" problem.
Practice: LeetCode 380
Pair a List
(for O(1) random access) with a Map<value, index>
(for O(1) lookup). Insert appends to the list and records its index. getRandom
indexes the list at a random position. The clever part is delete:
→ code: RandomizedSet.java
Prefix-aggregate + hash-map family. The subarray problems — one pattern, two knobs. (These came up in my own practice; they're the natural extension of "aggregate by key" to running sums.)
16 · Longest Subarray with Sum K prefix sum + map (index) #
Longest contiguous subarray summing to K; array may have negatives. {5,6,-5,5,3,5,3,-2,0}, K=8 → 4
({-5,5,3,5}
).
Practice: GfG — Longest Sub-Array with Sum K (LeetCode 325 is premium).
Brute force
sums every (i,j) pair, O(n²). The leap — prefix sums:
sum(i+1…j) = prefix[j] − prefix[i]. Wanting that to equal K means an earlier prefix equals currentSum − K. A map of prefix sum → earliest index
answers "did I see that?" in O(1).
{0 : −1}
so a subarray starting at index 0 measures correctly (i − (−1) = i + 1
).
putIfAbsent
); a later index would shrink the measured length.
→ code: LongestSubarraySumK.java
17 · Count Subarrays with Sum K prefix sum + map (frequency) #
Count how many contiguous subarrays sum to K. [1,2,1,2], K=3 → 3.
Practice: LeetCode 560
Same identity, but we want every
qualifying earlier prefix, not the longest gap. So the map stores frequency
of each prefix sum. Add map[currentSum − K]
to the count each step.
{0:−1}. Count → store frequency, seed {0:1}. Recognising which is the whole skill.
count += map.get(sum−K)
before
inserting the current sum, or K=0
counts the empty subarray. And use getOrDefault(key,0)
— get
on a missing key returns null and throws on unboxing.
→ code: CountSubarraySumK.java
18 · Count Subarrays with XOR K prefix XOR + map (frequency) #
Count subarrays whose XOR equals K. [4,2,2,6,4], K=6 → 4.
Practice: GfG — Subarray with given XOR
XOR is associative and self-inverse, so xorOf(i+1…j) = prefXor[j] ^ prefXor[i]. Wanting that = K gives prefXor[i] = prefXor[j] ^ K
— problem 17 with one substitution.
currentSum − K, XOR uses currentXor ^ K
— ^
is its own inverse, playing the role minus did.
xr ^ K, but
insert xr
(not xr ^ K
). Seed {0:1}.
→ code: CountSubarrayXorK.java
19 · Largest Subarray with Equal 0s and 1s transform → prefix sum #
Longest subarray with equal counts of 0 and 1. {1,0,1,1,1,0,0} → indices 1..6.
Practice: LeetCode 525
→ code: EqualZeroOne.java
20 · Longest Consecutive Sequence hash set #
Longest run of consecutive integers in an unsorted array. [100,4,200,1,3,2] → 4
(1·2·3·4).
Practice: LeetCode 128
Put all numbers in a set. Only begin counting a run at x
where x−1
is absent (so x
is a true start), then walk x+1, x+2….
x−1 not in set
guard makes each element get walked at most once total → O(n), versus O(n²) without it. Sorting works
but is O(n log n).
→ code: LongestConsecutive.java
More practice. Other problems worked through that use the same toolbox — kept here for quick reference.
21 · Common Elements in All Rows of a Matrix hash map (value→row reached) #
Print values present in every row of an M×N matrix in one O(M·N) pass.
Practice: GfG — Common elements in all rows
Seed from row 0 with value → 1
(row reached, 1-based). For each later row i: if the value's stored row equals the previous row (i
), bump it to i+1. Reaching the last row → present in all → print.
i+1.
→ code: CommonInRows.java
22 · Symmetric Pairs hash map (first→second) #
Find pairs with a mirror: (a,b)
and (b,a). First elements distinct. {(11,20),(30,40),(5,10),(40,30),(10,5)} → (30,40),(5,10).
Practice: GfG — Find all symmetric pairs
For each (first, second): if map.get(second) == first, the mirror was seen → print. Else store map[first] = second.
second
is exactly the key a mirror would have inserted.
map.get(missing)
returns null
in Java (guard with != null
); in Python dict[missing]
raises KeyError
— use .get(). A lone (3,3)
with no second copy is correctly skipped.
→ code: SymmetricPairs.java
III. Code Samples (runnable, Java 17) #
Self-contained classes with a main. Compile javac File.java, run java File. Standard library only.
PrincipleDemo.java — ↑ notes #
public class PrincipleDemo { static int hash(int x) { return x % 5; } // the example hash function y = x % 5 public static void main(String[] a) { int[] keys = {1987, 24, 2}; for (int k : keys) System.out.println(k + " → bucket " + hash(k)); // 1987 & 2 collide at 2 System.out.println("search 23 → bucket " + hash(23) + " (scan only there)"); } }
1987 → bucket 2 24 → bucket 4 2 → bucket 2 search 23 → bucket 3 (scan only there)
Why: 1987 and 2 share bucket 2 — a collision the table must resolve. Search recomputes the same bucket, so you only scan one bucket, not the whole table.
MyHashSet.java — ↑ notes #
import java.util.*; // Hash set built from scratch: array of buckets, each a list (separate chaining). public class MyHashSet { private final int MAX = 100000; // number of buckets private List<Integer>[] set; private int idx(int key) { return key % MAX; } // hash function private int pos(int key, int i) { // find key within its bucket, -1 if absent List<Integer> b = set[i]; if (b == null) return -1; for (int j = 0; j < b.size(); j++) if (b.get(j) == key) return j; return -1; } @SuppressWarnings("unchecked") public MyHashSet() { set = new ArrayList[MAX]; } public void add(int key) { int i = idx(key); if (pos(key, i) < 0) { // only add if not present (set = no dups) if (set[i] == null) set[i] = new ArrayList<>(); set[i].add(key); } } public void remove(int key) { int i = idx(key), p = pos(key, i); if (p >= 0) set[i].remove(p); } public boolean contains(int key) { return pos(key, idx(key)) >= 0; } public static void main(String[] a) { MyHashSet s = new MyHashSet(); s.add(1); s.add(100001); // 1 and 100001 collide (both % 100000 = 1) System.out.println(s.contains(100001)); // true (found by scanning bucket 1) s.remove(1); System.out.println(s.contains(1)); // false } }
true false
Why: 1 and 100001 hash to the same bucket; the in-bucket scan distinguishes them. This is separate chaining made explicit.
MyHashMap.java — ↑ notes #
import java.util.*; // Hash map from scratch. NOTE: the original card used javafx.util.Pair; [+] // replaced with a plain int[]{key,value} so it compiles on a standard JDK. public class MyHashMap { private final int MAX = 100000; private List<int[]>[] map; // each entry: [key, value] private int idx(int key) { return key % MAX; } private int pos(int key, int i) { List<int[]> b = map[i]; if (b == null) return -1; for (int j = 0; j < b.size(); j++) if (b.get(j)[0] == key) return j; return -1; } @SuppressWarnings("unchecked") public MyHashMap() { map = new ArrayList[MAX]; } public void put(int key, int value) { int i = idx(key), p = pos(key, i); if (p < 0) { if (map[i] == null) map[i] = new ArrayList<>(); map[i].add(new int[]{key, value}); // new pair } else { map[i].set(p, new int[]{key, value}); // update existing } } public int get(int key) { int i = idx(key), p = pos(key, i); return p < 0 ? -1 : map[i].get(p)[1]; // -1 = absent } public void remove(int key) { int i = idx(key), p = pos(key, i); if (p >= 0) map[i].remove(p); } public static void main(String[] a) { MyHashMap m = new MyHashMap(); m.put(1, 10); m.put(1, 20); // update System.out.println(m.get(1)); // 20 System.out.println(m.get(2)); // -1 (absent) } }
20 -1
Why the change:
the reference solution imported javafx.util.Pair, which isn't in a standard modern JDK. Using int[]{key,value}
keeps it dependency-free and the logic identical.
HashUsageDemo.java — ↑ notes #
import java.util.*; public class HashUsageDemo { public static void main(String[] a) { // ---- Hash set: presence / uniqueness ---- Set<Integer> s = new HashSet<>(); s.add(3); s.add(2); s.add(1); s.remove(2); System.out.println(s.contains(2) + " size=" + s.size()); // false size=2 // ---- Hash map: key → value ---- Map<Integer,Integer> m = new HashMap<>(); m.putIfAbsent(1, 1); // inserts m.put(1, 2); // overwrites → 2 System.out.println(m.get(1) + " has2=" + m.containsKey(2)); // 2 has2=false } }
false size=2 2 has2=false
Templates.java — ↑ notes #
import java.util.*; public class Templates { // Template A — find duplicates with a hash set. static <T> boolean hasDuplicate(List<T> keys) { Set<T> seen = new HashSet<>(); for (T k : keys) if (!seen.add(k)) return true; // add returns false if already present return false; } // Template B — aggregate by key (here: count occurrences). static <T> Map<T,Integer> counts(List<T> keys) { Map<T,Integer> m = new HashMap<>(); for (T k : keys) m.merge(k, 1, Integer::sum); // update strategy = sum return m; } public static void main(String[] a) { System.out.println(hasDuplicate(List.of(1,2,3,2))); // true System.out.println(counts(List.of("a","b","a"))); // {a=2, b=1} } }
true
{a=2, b=1}ContainsDuplicate.java — ↑ notes #
import java.util.*; public class ContainsDuplicate { static boolean has(int[] nums) { Set<Integer> seen = new HashSet<>(); for (int x : nums) if (!seen.add(x)) return true; // add fails → duplicate return false; } public static void main(String[] a) { System.out.println(has(new int[]{1,2,3,1})); // true System.out.println(has(new int[]{1,2,3})); // false } }
true false
SingleNumber.java — ↑ notes #
public class SingleNumber { static int single(int[] nums) { int x = 0; for (int n : nums) x ^= n; // ⭐ pairs cancel (a^a=0); loner survives return x; } public static void main(String[] a) { System.out.println(single(new int[]{4,1,2,1,2})); // 4 } }
4
Why O(1) space: XOR is commutative/associative, so order doesn't matter and no storage is needed — strictly better than the hash-set solution.
HappyNumber.java — ↑ notes #
import java.util.*; public class HappyNumber { static int next(int n) { int s = 0; while (n > 0) { int d = n % 10; s += d * d; n /= 10; } // sum of squared digits return s; } static boolean isHappy(int n) { Set<Integer> seen = new HashSet<>(); while (n != 1 && seen.add(n)) n = next(n); // stop at 1, or when a state repeats (cycle) return n == 1; } public static void main(String[] a) { System.out.println(isHappy(19)); // true System.out.println(isHappy(2)); // false (enters a cycle) } }
true false
TwoSum.java — ↑ notes #
import java.util.*; public class TwoSum { static int[] twoSum(int[] nums, int target) { Map<Integer,Integer> seen = new HashMap<>(); // value → index for (int i = 0; i < nums.length; i++) { int need = target - nums[i]; if (seen.containsKey(need)) return new int[]{ seen.get(need), i }; // check BEFORE insert seen.put(nums[i], i); } return new int[]{}; } public static void main(String[] a) { System.out.println(Arrays.toString(twoSum(new int[]{3,2,4}, 6))); // [1, 2] } }
[1, 2]
Isomorphic.java — ↑ notes #
import java.util.*; public class Isomorphic { static boolean iso(String x, String y) { if (x == null || y == null || x.length() != y.length()) return false; Map<Character,Character> map = new HashMap<>(); // x → y Set<Character> used = new HashSet<>(); // y's already taken for (int i = 0; i < x.length(); i++) { char cx = x.charAt(i), cy = y.charAt(i); if (map.containsKey(cx)) { if (map.get(cx) != cy) return false; // inconsistent } else { if (used.contains(cy)) return false; // ⭐ two x's → one y forbidden map.put(cx, cy); used.add(cy); } } return true; } public static void main(String[] a) { System.out.println(iso("ACAB", "XCXY")); // true System.out.println(iso("foo", "bar")); // false } }
true false
FirstUnique.java — ↑ notes #
public class FirstUnique { static int firstUniq(String s) { int[] freq = new int[26]; // aggregate-by-key: count each char for (char c : s.toCharArray()) freq[c - 'a']++; for (int i = 0; i < s.length(); i++) // first char with count 1 if (freq[s.charAt(i) - 'a'] == 1) return i; return -1; } public static void main(String[] a) { System.out.println(firstUniq("leetcode")); // 0 System.out.println(firstUniq("loveleetcode")); // 2 System.out.println(firstUniq("aabb")); // -1 } }
0 2 -1
GroupAnagrams.java — ↑ notes #
import java.util.*; public class GroupAnagrams { static List<List<String>> group(String[] words) { Map<String, List<String>> buckets = new HashMap<>(); for (String w : words) { char[] c = w.toCharArray(); Arrays.sort(c); // ⭐ designed key: sorted letters String key = new String(c); // "eat","ate" → "aet" buckets.computeIfAbsent(key, k -> new ArrayList<>()).add(w); } return new ArrayList<>(buckets.values()); } public static void main(String[] a) { System.out.println(group(new String[]{"eat","tea","tan","ate","nat","bat"})); } }
[[eat, tea, ate], [bat], [tan, nat]]
Why:
group order isn't guaranteed (it follows hash iteration), but each group's contents are correct. computeIfAbsent
is the idiomatic "create bucket if missing, then add."
LongestSubstr.java — ↑ notes #
import java.util.*; public class LongestSubstr { static int lengthOfLongest(String s) { Map<Character,Integer> last = new HashMap<>(); // char → last index seen int best = 0, left = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (last.containsKey(c)) // ⭐ jump left past the prior occurrence left = Math.max(left, last.get(c) + 1); last.put(c, right); best = Math.max(best, right - left + 1); } return best; } public static void main(String[] a) { System.out.println(lengthOfLongest("abcabcbb")); // 3 System.out.println(lengthOfLongest("bbbbb")); // 1 System.out.println(lengthOfLongest("pwwkew")); // 3 } }
3 1 3
Why max
on left:
a stale earlier index could be behind the current window — guarding with max
prevents the left pointer from moving backwards.
TopKFrequent.java — ↑ notes #
import java.util.*; public class TopKFrequent { static int[] topK(int[] nums, int k) { Map<Integer,Integer> freq = new HashMap<>(); for (int n : nums) freq.merge(n, 1, Integer::sum); // ⭐ bucket sort: bucket[f] = values that occur f times List<Integer>[] bucket = new List[nums.length + 1]; for (var e : freq.entrySet()) { int f = e.getValue(); if (bucket[f] == null) bucket[f] = new ArrayList<>(); bucket[f].add(e.getKey()); } int[] res = new int[k]; int idx = 0; for (int f = bucket.length - 1; f >= 1 && idx < k; f--) // high freq → low if (bucket[f] != null) for (int v : bucket[f]) if (idx < k) res[idx++] = v; return res; } public static void main(String[] a) { System.out.println(Arrays.toString(topK(new int[]{1,1,1,2,2,3}, 2))); // [1, 2] } }
[1, 2]
Why O(n): frequencies are bounded by n, so indexing by frequency and scanning top-down avoids any sort or heap.
RandomizedSet.java — ↑ notes #
import java.util.*; public class RandomizedSet { private List<Integer> list = new ArrayList<>(); // O(1) random access private Map<Integer,Integer> idx = new HashMap<>(); // value → its index in list private Random rnd = new Random(); public boolean insert(int v) { if (idx.containsKey(v)) return false; idx.put(v, list.size()); list.add(v); return true; } public boolean remove(int v) { Integer i = idx.get(v); if (i == null) return false; int last = list.size() - 1, lastVal = list.get(last); list.set(i, lastVal); idx.put(lastVal, i); // ⭐ swap target with last… list.remove(last); idx.remove(v); // …then pop last → O(1) return true; } public int getRandom() { return list.get(rnd.nextInt(list.size())); } public static void main(String[] a) { RandomizedSet r = new RandomizedSet(); System.out.println(r.insert(1)); // true System.out.println(r.remove(2)); // false (absent) r.insert(2); System.out.println(r.remove(1)); // true System.out.println(r.getRandom()); // 2 (only element left) } }
true false true 2
LongestSubarraySumK.java — ↑ notes #
import java.util.*; public class LongestSubarraySumK { static int longest(int[] nums, int k) { Map<Integer,Integer> first = new HashMap<>(); first.put(0, -1); // ⭐ prefix 0 at index -1 int sum = 0, best = 0; for (int i = 0; i < nums.length; i++) { sum += nums[i]; if (first.containsKey(sum - k)) best = Math.max(best, i - first.get(sum - k)); first.putIfAbsent(sum, i); // ⚠ earliest index only } return best; } public static void main(String[] a) { System.out.println(longest(new int[]{5,6,-5,5,3,5,3,-2,0}, 8)); // 4 } }
4
CountSubarraySumK.java — ↑ notes #
import java.util.*; public class CountSubarraySumK { static int count(int[] nums, int k) { Map<Integer,Integer> freq = new HashMap<>(); freq.put(0, 1); // ⭐ empty prefix once; value = FREQUENCY int sum = 0, cnt = 0; for (int x : nums) { sum += x; cnt += freq.getOrDefault(sum - k, 0); // ⚠ read first… freq.merge(sum, 1, Integer::sum); // …then write } return cnt; } public static void main(String[] a) { System.out.println(count(new int[]{1,2,1,2}, 3)); // 3 System.out.println(count(new int[]{1,-1,0}, 0)); // 3 } }
3 3
CountSubarrayXorK.java — ↑ notes #
import java.util.*; public class CountSubarrayXorK { static int count(int[] nums, int k) { Map<Integer,Integer> freq = new HashMap<>(); freq.put(0, 1); int xr = 0, cnt = 0; for (int x : nums) { xr ^= x; cnt += freq.getOrDefault(xr ^ k, 0); // ⭐ need earlier prefix = xr ^ k freq.merge(xr, 1, Integer::sum); // ⚠ insert xr, not xr^k } return cnt; } public static void main(String[] a) { System.out.println(count(new int[]{4,2,2,6,4}, 6)); // 4 } }
4
EqualZeroOne.java — ↑ notes #
import java.util.*; public class EqualZeroOne { static int maxLen(int[] arr) { Map<Integer,Integer> first = new HashMap<>(); first.put(0, -1); int sum = 0, best = 0; for (int i = 0; i < arr.length; i++) { sum += (arr[i] == 0) ? -1 : 1; // ⭐ 0 → -1 ⇒ "equal" == "sum 0" if (first.containsKey(sum)) best = Math.max(best, i - first.get(sum)); else first.put(sum, i); } return best; } public static void main(String[] a) { System.out.println(maxLen(new int[]{1,0,1,1,1,0,0})); // 6 } }
6
LongestConsecutive.java — ↑ notes #
import java.util.*; public class LongestConsecutive { static int longest(int[] nums) { Set<Integer> set = new HashSet<>(); for (int x : nums) set.add(x); int best = 0; for (int x : set) { if (!set.contains(x - 1)) { // ⭐ only start at a run's beginning int y = x, len = 1; while (set.contains(y + 1)) { y++; len++; } best = Math.max(best, len); } } return best; } public static void main(String[] a) { System.out.println(longest(new int[]{100,4,200,1,3,2})); // 4 } }
4
CommonInRows.java — ↑ notes #
import java.util.*; public class CommonInRows { static void common(int[][] mat) { if (mat == null || mat.length == 0) return; Map<Integer,Integer> row = new HashMap<>(); // value → highest row reached (1-based) for (int i = 0; i < mat.length; i++) for (int j = 0; j < mat[0].length; j++) { int v = mat[i][j]; if (i == 0) row.put(v, 1); else if (row.getOrDefault(v, 0) == i) { // ⚠ reached previous row? row.put(v, i + 1); if (i == mat.length - 1) System.out.print(v + " "); } } System.out.println(); } public static void main(String[] a) { int[][] mat = {{2,4,3,8,7},{4,7,1,3,6},{3,5,2,1,3},{4,5,0,2,3}}; common(mat); // 3 } }
3
SymmetricPairs.java — ↑ notes #
import java.util.*; public class SymmetricPairs { static void find(int[][] arr) { Map<Integer,Integer> map = new HashMap<>(); // first → second for (int[] p : arr) { int first = p[0], second = p[1]; Integer val = map.get(second); // ⭐ mirror would have keyed on 'second' if (val != null && val == first) // ⚠ null-guard (Java returns null) System.out.println("(" + second + ", " + first + ")"); else map.put(first, second); } } public static void main(String[] a) { find(new int[][]{{11,20},{30,40},{5,10},{40,30},{10,5}}); } }
(30, 40) (5, 10)
IV. Hands-On Lab (practice) #
Six graded exercises, Warm-up → Challenge. Predict the output before running, then verify. Solutions collapsed.
-
Check Java:
java -version(need 11+; examples target 17). Install a JDK if missing (e.g.brew install openjdk). -
mkdir hashing && cd hashing -
Drop Section III files in;
javac Name.java && java Name. -
No external libraries —
java.util.*only.
Exercise 1 · Warm-up — Find duplicates (Template A)
Goal: cement the hash-set "seen before?" pattern.
-
Write
boolean hasDup(int[] a)using aHashSet. -
Return true on the first value
addrejects. -
Test on
{1,2,3,4}and{1,2,2,3}.
🔮 Predict both booleans.
Solution
See ContainsDuplicate.java. Answers: false, then true. set.add(x)
returns false when x
already exists — no separate contains
call needed.
Exercise 2 · Warm-up — First Unique Character (Template B)
Goal: practise aggregate-by-key with a two-pass count.
- Count each char into a size-26 array (or map).
- Scan left to right; return the first index with count 1.
-
Test on
"loveleetcode"and"aabb".
🔮 Predict both results before running.
Solution
See FirstUnique.java. "loveleetcode"
→ 2
('v'); "aabb"
→ -1. The update rule on an existing key is "increment" — that choice is what makes it Template
B.
Exercise 3 · Core — Two Sum (set vs map decision)
Goal: feel why this needs a map, not a set.
-
First write a version returning only true/false
using a
HashSet. - Then change it to return indices — notice you now must store value→index in a map.
-
Test on
{2,7,11,15}, t=9.
🔮 Predict the indices. Then articulate in one sentence why the set version can't return them.
Solution
See TwoSum.java
→ [0, 1]. The set knows a complement exists
but not where
— returning indices is the "provide more information" jump from set to map.
Exercise 4 · Core — Group Anagrams (design the key)
Goal: build a canonical key.
- For each word, sort its letters to form the key.
-
Bucket words in a
Map <String, List<String>>viacomputeIfAbsent. -
Test on
["abc","bca","cab","xyz","zxy","foo"].
🔮 Predict how many groups you'll get, and which words share one.
Solution
See GroupAnagrams.java. 3 groups: [abc,bca,cab], [xyz,zxy], [foo]. The sorted key ("abc", "xyz", "foo"
) collides same-group words and separates the rest.
"#1#1#1#0…"
from a 26-array instead of sorting.
Exercise 5 · Challenge — Count Subarrays with Sum K (then XOR)
Goal: the prefix + frequency-map engine, and the one-line port to XOR.
-
Implement count-subarrays-sum-K with a frequency map seeded
{0:1}; read before write. -
Test on
{1,2,3}, K=3and{0,0,0}, K=0. -
Now copy it, swap the running sum for a running XOR, and change the lookup key from
sum−Ktoxr^K. Test on{4,2,2,6,4}, K=6.
🔮 Predict all three counts. The {0,0,0}
case catches the read-before-write bug.
Solution
See CountSubarraySumK.java
and CountSubarrayXorK.java. {1,2,3},K=3
→ 2; {0,0,0},K=0
→ 6
(if you got more, you inserted before reading); XOR case → 4. Remember: insert xr, not xr^K.
Exercise 6 · Challenge — Insert/Delete/GetRandom O(1)
Goal: combine two structures and use swap-with-last.
-
Back a set with a
List(random access) +Map<value,index>(lookup). - For delete: swap the target with the last list element, fix that element's index in the map, pop the last slot.
- Verify insert/remove return values and that getRandom only yields present elements.
🔮 Before coding delete, predict what breaks if you forget to update the moved element's index in the map.
Solution
See RandomizedSet.java. Forgetting the index update leaves the map pointing at the wrong slot, so a later remove
corrupts the list. The swap-with-last trick is the same O(1)-removal idea from §4.
Self-check rubric — how to know you've got it#
| You can… | Signal of mastery |
|---|---|
| Explain a hash table internally | "buckets → hash function → index → collisions via chaining → tree-ify big buckets," plus load factor. |
| Choose set vs map instantly | Set for presence; map the moment you need index/count/value. |
| Name the three templates | Find-duplicates, aggregate-by-key, design-the-key — and which a new problem fits. |
| Design a key | Sorted form for anagrams; offset for shifts; block id for Sudoku; serialization for subtrees. |
| Run the prefix engine |
Earliest index for length, frequency for count; sum−K
↔ xr^K.
|
| Justify "no sliding window" | Negatives break monotonicity → prefix + map instead. |
| Use swap-with-last | Achieve O(1) deletion from an unordered array (LC 380). |
V. More Practice #
The rest of the hash-table problem set — name, pattern, the one insight, and a verified link. Drill these to round out coverage.
Hash-table classics#
| Problem | Pattern | Key insight | Link |
|---|---|---|---|
| Intersection of Two Arrays | Set | Put one array in a set; collect members of the other. Result is unique. | LC 349 |
| Intersection of Two Arrays II | Frequency map | Count one array; decrement while scanning the other to keep multiplicities. | LC 350 |
| Contains Duplicate II | Map (val→last idx) / sliding set | Duplicate within distance k: track last index, or keep a window-sized set. | LC 219 |
| Minimum Index Sum of Two Lists | Map (str→index) | Index one list; for common strings, minimise the index-sum. | LC 599 |
| Logger Rate Limiter | Map (msg→next allowed time) |
Allow only if t ≥ stored; store t+10
on accept.
| LC 359 |
| Jewels and Stones | Set | Put jewels in a set; count stones that are members. | LC 771 |
| 4Sum II | Map of pair-sums |
Count sums of A+B; for each C+D look up −(C+D). O(n²).
| LC 454 |
| Two Sum III — Data Structure Design | Frequency map |
Store counts; find
scans keys for a complement (mind x+x
).
| LC 170 |
Design the key#
| Problem | Designed key | Key insight | Link |
|---|---|---|---|
| Group Shifted Strings | Offset/diff sequence | Normalise by char differences mod 26 → all shifts of a word share one key. | LC 249 |
| Valid Sudoku | Row / col / block id |
One pass; key cells as "r#"+v, "c#"+v, (r/3)+","+(c/3)+"#"+v
in a set.
| LC 36 |
| Find Duplicate Subtrees | Serialized subtree | Post-order serialize each subtree to a string; a repeated string = a duplicate. | LC 652 |
| Unique Word Abbreviation | Abbreviation string |
Key word as first+len+last; unique iff no conflicting word shares it.
| LC 288 |
Prefix-aggregate extras#
| Problem | Pattern | Key insight | Link |
|---|---|---|---|
| Subarray Sums Divisible by K | Prefix % K + freq |
Equal remainders bound a divisible sum; normalise neg: ((r%K)+K)%K.
| LC 974 |
| Longest Subarray with Sum 0 | Prefix sum + earliest idx | Repeated prefix ⇒ zero-sum gap (= problem 16 with K=0). | GfG |
★ Patterns Recap #
Every detailed problem mapped to technique, the unlocking insight, and complexity.
| # | Problem | Technique | Key insight | Time / Space |
|---|---|---|---|---|
| 6 | Contains Duplicate | Hash set (Template A) |
First failed add
= duplicate
| O(n) / O(n) |
| 7 | Single Number | XOR |
a^a=0
cancels pairs → loner survives
| O(n) / O(1) |
| 8 | Happy Number | Set cycle detection | Repeat seen ⇒ infinite loop | O(log n)* / O(log n) |
| 9 | Two Sum | Map val→idx (Scenario I) | Look up complement before insert | O(n) / O(n) |
| 10 | Isomorphic Strings | Map + set (bijection) | Map: x→one y; set: y used once | O(n) / O(n) |
| 11 | First Unique Character | Frequency map (Template B) | Count, then first with count 1 | O(n) / O(1) |
| 12 | Group Anagrams | Design the key (Template C) | Sorted letters as canonical key | O(n·L log L) / O(n·L) |
| 13 | Longest Substr No Repeat | Set/map + sliding window | Char→last index lets left pointer jump | O(n) / O(min(n,Σ)) |
| 14 | Top K Frequent | Freq map + bucket sort | Index buckets by frequency, scan top-down | O(n) / O(n) |
| 15 | Insert/Del/GetRandom O(1) | Map + list (swap-last) | List for random, map for lookup, swap on delete | O(1) avg / O(n) |
| 16 | Longest Subarray Sum K | Prefix sum + map (index) |
Look up sum−K; store earliest index
| O(n) / O(n) |
| 17 | Count Subarrays Sum K | Prefix sum + map (freq) | Store frequency; read before write | O(n) / O(n) |
| 18 | Count Subarrays XOR K | Prefix XOR + map (freq) |
xr^K
replaces sum−K
| O(n) / O(n) |
| 19 | Equal 0s and 1s | Transform → prefix sum | 0→−1 turns it into "sum 0" | O(n) / O(n) |
| 20 | Longest Consecutive Seq | Hash set |
Start a run only where x−1
absent
| O(n) / O(n) |
| 21 | Common Elements in Rows | Map val→row reached | Bump only if stored == previous row | O(M·N) / O(N) |
| 22 | Symmetric Pairs | Map first→second |
Mirror keys on second
| O(n) / O(n) |
*Happy Number iterations are bounded by how fast the digit-square-sum shrinks; effectively near-constant for ints.
End of guide · Hashing · Java 17