Study Index

Hashing — Complete Study Guide

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.

⭐ TRICK — the insight that makes it easy ⚠ PITFALL — common mistake / gotcha 💡 TIP — interview-relevant aside [+] — my own addition / correction

I. Notebook Summary (copy by hand)

Card-sized blocks, hand-copyable and deck-ready. Telegraphic — arrows over sentences.

HASH TABLE — PRINCIPLE ====================== Goal = quick insert + search via a hash FUNCTION mapping key→bucket. Insert : key →[hashFn]→ bucket idx → store in that bucket. Search : key →[SAME hashFn]→ same bucket → scan only that bucket. Example : hashFn y = x % 5 ; key 1987 → bucket 2 ; key 24 → bucket 4. TWO DESIGN FACTORS ================== 1. Hash function → spread keys UNIFORMLY across buckets. tradeoff: #buckets vs bucket capacity. perfect 1-1 map rare. 2. Collision resolution (collisions ~inevitable) - small constant N per bucket → ARRAY/list in bucket. - large/variable N → balanced BST in bucket (O(log N)). doodle: [b0][b1][b2:1987→2][b3][b4:24] ; collision = 2 keys, same bucket HASH SET vs HASH MAP ==================== | HashSet | HashMap | | stores VALUES only | stores (key,value) PAIRS | | "have I seen x?" | "what's attached to key x?" | | add/remove/contains| put/get/remove/containsKey | all avg O(1) insert/search/remove. O(1) REMOVE FROM BUCKET ======================= array-list remove = O(n) (shift n-i elems). To make O(1): ⭐ swap target with LAST elem, then drop last. (order lost, fine for set) ⭐ or use a LINKED LIST in the bucket. (order kept)
3 REUSABLE TEMPLATES ==================== A) FIND DUPLICATES (set): for k in keys: if k in set → DUP; else set.add(k) B) AGGREGATE BY KEY (map): for k in keys: map[k] = update(map[k]) // count / sum / replace C) DESIGN THE KEY: build a canonical key so same-group items collide. - order doesn't matter → sorted(str/arr) as key (anagrams) - care only about offset → diffs/offset tuple as key (shifted strs) - tree node group → serialized subtree as key - matrix cell group → row, col, or block id as key (sudoku: r/3,c/3)

CORE PROBLEM CARDS ================== CONTAINS DUPLICATE [set] seen-before? → set. O(n)/O(n). LC 217 SINGLE NUMBER [xor] ⭐ a^a=0 → XOR all → the lonely one. O(n)/O(1). LC 136 HAPPY NUMBER [set cycle] sum of sq digits; loop till 1 or repeat. LC 202 TWO SUM [map v→i] need=T-x; seen? → done. O(n)/O(n). LC 1 ISOMORPHIC STRINGS [map+set] x→y consistent AND y used once. LC 205 FIRST UNIQUE CHAR [freq map] count, then first with count 1. LC 387 GROUP ANAGRAMS [design key] sorted word = key → bucket. LC 49 LONGEST SUBSTR NO REPEAT [set+window] slide R, shrink L on dup. O(n). LC 3 TOP K FREQUENT [freq+heap] count → heap / bucket sort. LC 347 INSERT DEL GETRANDOM O(1) [map+list] map val→idx; swap-last on delete. LC 380
PREFIX-AGGREGATE + MAP (subarray family) ⭐ ONE PATTERN ======================================================== idea: sum(i+1..j)=pre[j]-pre[i]. want =K → look up pre[j]-K in map. LENGTH problem → store EARLIEST index, seed {0:-1}. COUNT problem → store FREQUENCY, seed {0:1}. XOR variant → swap (sum-K) for (xr^K). seed {0:1}. (XOR self-inverse) ⚠ negatives ⇒ sliding window INVALID here; must use prefix+map. LONGEST SUBARRAY SUM K earliest-idx map. O(n). GfG COUNT SUBARRAYS SUM K freq map, read-before-write. LC 560 COUNT SUBARRAYS XOR K freq map, key xr^K. GfG EQUAL 0s/1s 0→-1 ⇒ "sum 0". LC 525 LONGEST CONSEC SEQUENCE set; start run only if x-1 absent. O(n). LC 128 SUBARRAY SUMS DIV BY K freq of prefix%K, fix neg: ((r%K)+K)%K. LC 974
⭐ THE WHOLE TOOLKIT IN ONE LINE. A hash table answers "have I seen this / what's attached to this" in O(1). Three modes: remember presence (set) → duplicates/cycles; remember relationships (map key→info) → two-sum/mapping; remember running aggregates (prefix sum or XOR in a map) → subarray problems. When the obvious key won't group correctly, design a canonical key.

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.

Why it matters. Every technique in this guide rests on this: computing where a key lives beats searching for it. The cost is honest only as average O(1) — collisions can push it higher.

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:

💡 TIP — interview-ready answer. "How does a HashMap work internally?" → array of buckets → hash function → index → collisions resolved by chaining (list, upgraded to a balanced tree when a bucket gets big). [+] Add the load factor: entries ÷ buckets; Java resizes (doubles + rehashes) past 0.75 to keep chains short.

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.

Why it matters. Choosing set vs map is the first fork in almost every hash problem. Two Sum is the canonical lesson: if you only needed "does a pair exist?" a set suffices; because you must return indices, you need a map.

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:

  1. 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) ).
  2. Linked list bucket. Splice the node out in O(1) and keep insertion order.
⭐ TRICK. The swap-with-last move shows up far beyond hash tables — it's how you get O(1) deletion from any unordered array, and it powers LC 380 (problem 11 below).

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".

⭐ Design-the-key cheat sheet.
  • 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−c or r+c.

6 · Contains Duplicate hash set · Template A

Return true if any value appears at least twice. [1,2,3,1] → true.

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.

💡 TIP. A one-liner alternative: if new HashSet<>(list).size() != list.size() there was a duplicate. The streaming version above is better when you want to stop early.

7 · Single Number XOR (beats hashing)

Every element appears twice except one; find the loner. [4,1,2,1,2] → 4.

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.

⭐ TRICK — XOR everything. Since 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.
Why it matters. A reminder that hashing isn't always the optimal answer — recognising an algebraic shortcut (XOR, math) can drop the space cost entirely.

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).

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.

⭐ TRICK. "Detect an infinite loop" = "have I seen this state before?" = a job for a hash set. [+] The O(1)-space variant uses Floyd's slow/fast pointers on the same transformation — worth mentioning in interviews.

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].

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.

⭐ TRICK. Check the complement before inserting the current element — that single-pass ordering both finds the pair and guarantees you never reuse one element.

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).

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.

⭐ TRICK. You need both guards: the map enforces "each x → one y," the set enforces "no two x's share a y." Mapping a char to itself is allowed; mapping two chars to one is not.
⚠ PITFALL. Different lengths → not isomorphic, bail early. [+] With boxed Character, compare via .equals() — == only works by luck for cached small values.

11 · First Unique Character frequency map · Scenario II

Return the index of the first non-repeating character, or −1. "leetcode" → 0.

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.

⭐ TRICK. Two passes beat one clever pass here — counting then scanning is O(n) and trivially correct. The update strategy on an existing key is simply "increment," which is what makes this Template B.

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]].

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>>.

⭐ TRICK. The key must satisfy both rules: same-group words collide (sorted form is identical) and different-group words don't (different letter multisets → different sorted form). [+] If words are long but the alphabet is small, a 26-length count signature like "1#0#0#...#2" is an O(L) key vs O(L log L) sorting.

13 · Longest Substring Without Repeating Characters hash set + sliding window

Length of the longest substring with all distinct characters. "abcabcbb" → 3 ("abc").

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.

⭐ TRICK. Storing char → last index (a map, not just a set) lets you jump the left pointer straight past the previous occurrence instead of stepping one at a time — O(n) with no back-tracking.
Why it matters. First example of "hash table + another technique" — the conclusion of the topic: real problems often need a hash table combined with a window, heap, or second structure.

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].

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).

⭐ TRICK — bucket sort beats the heap. Frequencies are bounded by n, so index buckets by frequency and read from the top. No comparisons, O(n). This "index by the bounded quantity" idea generalises well.

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.

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:

⭐ TRICK — swap-with-last (from §4). To remove a value in O(1): look up its index, swap it with the last list element, update that element's index in the map, then pop the last slot. This avoids the O(n) shift of a normal list removal.
Why it matters. It's the practical payoff of the bucket-removal trick and the clearest example of two structures covering each other's weak spot (list lacks O(1) lookup; map lacks O(1) random pick).

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} ).

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).

⭐ TRICK. Seed {0 : −1} so a subarray starting at index 0 measures correctly (i − (−1) = i + 1 ).
⚠ PITFALL — store the EARLIEST index. Insert each prefix only the first time (putIfAbsent ); a later index would shrink the measured length.
⚠ PITFALL — no sliding window. With negatives the running sum isn't monotonic in window size, so growing/shrinking a window is invalid. Prefix + map is the tool. (Sliding window only works when all elements are non-negative.)

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.

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.

⭐ TRICK — the knob. Length → store earliest index, seed {0:−1}. Count → store frequency, seed {0:1}. Recognising which is the whole skill.
⚠ PITFALL — order. Read 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.

18 · Count Subarrays with XOR K prefix XOR + map (frequency)

Count subarrays whose XOR equals K. [4,2,2,6,4], K=6 → 4.

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.

⭐ TRICK. Where sum used currentSum − K, XOR uses currentXor ^ K — ^ is its own inverse, playing the role minus did.
⚠ PITFALL. Look up xr ^ K, but insert xr (not xr ^ K ). Seed {0:1}.

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.

⭐ TRICK — reduce to a solved problem. Replace every 0 with −1; "equal 0s and 1s" becomes "sum = 0," i.e. problem 16 with K=0. Run the longest-sum-K machine targeting 0.
Why it matters. The meta-skill: transform an unfamiliar problem into one you own rather than inventing a new algorithm.

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).

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….

⭐ TRICK. The 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).

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.

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.

⭐ TRICK. Store row reached, not a raw count — a value repeated within one row would inflate a plain frequency. Gating on "stored == previous row" advances each value by at most one per row.
⚠ PITFALL. Index is 0-based but rows are 1-based → store i+1.

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).

For each (first, second): if map.get(second) == first, the mirror was seen → print. Else store map[first] = second.

⭐ TRICK. No need to store both orderings — the current pair's second is exactly the key a mirror would have inserted.
💡 TIP — Java vs Python. 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.

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

PrincipleDemo.java javac PrincipleDemo.java && java PrincipleDemo
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)");
    }
}
Expected output
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

MyHashSet.java javac MyHashSet.java && java MyHashSet
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
    }
}
Expected output
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

MyHashMap.java javac MyHashMap.java && java MyHashMap
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)
    }
}
Expected output
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

HashUsageDemo.java javac HashUsageDemo.java && java HashUsageDemo
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
    }
}
Expected output
false size=2
2 has2=false

Templates.java — ↑ notes

Templates.java javac Templates.java && java Templates
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}
    }
}
Expected output
true
{a=2, b=1}

ContainsDuplicate.java — ↑ notes

ContainsDuplicate.java javac ContainsDuplicate.java && java ContainsDuplicate
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
    }
}
Expected output
true
false

SingleNumber.java — ↑ notes

SingleNumber.java javac SingleNumber.java && java SingleNumber
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
    }
}
Expected output
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

HappyNumber.java javac HappyNumber.java && java HappyNumber
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)
    }
}
Expected output
true
false

TwoSum.java — ↑ notes

TwoSum.java javac TwoSum.java && java TwoSum
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]
    }
}
Expected output
[1, 2]

Isomorphic.java — ↑ notes

Isomorphic.java javac Isomorphic.java && java Isomorphic
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
    }
}
Expected output
true
false

FirstUnique.java — ↑ notes

FirstUnique.java javac FirstUnique.java && java FirstUnique
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
    }
}
Expected output
0
2
-1

GroupAnagrams.java — ↑ notes

GroupAnagrams.java javac GroupAnagrams.java && java GroupAnagrams
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"}));
    }
}
Expected output
[[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

LongestSubstr.java javac LongestSubstr.java && java LongestSubstr
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
    }
}
Expected output
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

TopKFrequent.java javac TopKFrequent.java && java TopKFrequent
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]
    }
}
Expected output
[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

RandomizedSet.java javac RandomizedSet.java && java RandomizedSet
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)
    }
}
Expected output
true
false
true
2

LongestSubarraySumK.java — ↑ notes

LongestSubarraySumK.java javac LongestSubarraySumK.java && java LongestSubarraySumK
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
    }
}
Expected output
4

CountSubarraySumK.java — ↑ notes

CountSubarraySumK.java javac CountSubarraySumK.java && java CountSubarraySumK
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
    }
}
Expected output
3
3

CountSubarrayXorK.java — ↑ notes

CountSubarrayXorK.java javac CountSubarrayXorK.java && java CountSubarrayXorK
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
    }
}
Expected output
4

EqualZeroOne.java — ↑ notes

EqualZeroOne.java javac EqualZeroOne.java && java EqualZeroOne
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
    }
}
Expected output
6

LongestConsecutive.java — ↑ notes

LongestConsecutive.java javac LongestConsecutive.java && java LongestConsecutive
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
    }
}
Expected output
4

CommonInRows.java — ↑ notes

CommonInRows.java javac CommonInRows.java && java CommonInRows
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
    }
}
Expected output
3 

SymmetricPairs.java — ↑ notes

SymmetricPairs.java javac SymmetricPairs.java && java SymmetricPairs
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}});
    }
}
Expected output
(30, 40)
(5, 10)

IV. Hands-On Lab (practice)

Six graded exercises, Warm-up → Challenge. Predict the output before running, then verify. Solutions collapsed.

One-time setup.
  1. Check Java: java -version (need 11+; examples target 17). Install a JDK if missing (e.g. brew install openjdk ).
  2. mkdir hashing && cd hashing
  3. Drop Section III files in; javac Name.java && java Name.
  4. No external libraries — java.util.* only.
Exercise 1 · Warm-up — Find duplicates (Template A)

Goal: cement the hash-set "seen before?" pattern.

  1. Write boolean hasDup(int[] a) using a HashSet.
  2. Return true on the first value add rejects.
  3. 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.

  1. Count each char into a size-26 array (or map).
  2. Scan left to right; return the first index with count 1.
  3. 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.

  1. First write a version returning only true/false using a HashSet.
  2. Then change it to return indices — notice you now must store value→index in a map.
  3. 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.

  1. For each word, sort its letters to form the key.
  2. Bucket words in a Map <String, List<String>> via computeIfAbsent.
  3. 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.

💡 Try the O(L) count-signature key as a stretch: build "#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.

  1. Implement count-subarrays-sum-K with a frequency map seeded {0:1}; read before write.
  2. Test on {1,2,3}, K=3 and {0,0,0}, K=0.
  3. Now copy it, swap the running sum for a running XOR, and change the lookup key from sum−K to xr^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.

  1. Back a set with a List (random access) + Map<value,index> (lookup).
  2. For delete: swap the target with the last list element, fix that element's index in the map, pop the last slot.
  3. 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.

The thread through all of it. A hash table is an O(1) memory for one of three things: presence (set → duplicates, cycles, membership), relationships (map key→info → two-sum, mappings, designed keys), or running aggregates (prefix sum/XOR in a map → subarray length and count). When the natural key won't group correctly, design one that satisfies both grouping rules. Master those modes and most "hard" hash problems become a choice between four or five moves you already own.

End of guide · Hashing · Java 17