Hashing Pattern for Problem Solving
Hashing as a Pattern
The Hashing section explains how hash maps and sets work. This page is about when to reach for one while solving a problem.
The core move is always the same: trade memory for time by remembering what you have already seen, so that a question like "has a matching element appeared before?" costs O(1) instead of a full rescan.
WITHOUT a hash structure: WITH one:
for each element: for each element:
scan everything else ask the map / set ← O(1) average
→ O(n²) → O(n)
The Five Recurring Shapes
1. COMPLEMENT LOOKUP "is the partner I need already here?" key: value → position e.g. two values summing to a target 2. SIGNATURE GROUPING "which items are equivalent?" key: canonical form → list e.g. words with the same letters 3. FREQUENCY COUNT "how many of each?" key: item → count e.g. first character seen exactly once 4. MEMBERSHIP SET "does this value exist at all?" set of values e.g. longest run of consecutive integers 5. LAST-SEEN INDEX "where did I see this most recently?" key: value → latest position e.g. a repeat within distance k
Shape 1: Complement Lookup
Receipt item prices (unsorted): [12, 7, 30, 5, 18, 9]
Which two items cost exactly 25?
i=0 12 → need 13 → not seen → store {12:0}
i=1 7 → need 18 → not seen → store {7:1}
i=2 30 → need -5 → not seen → store {30:2}
i=3 5 → need 20 → not seen → store {5:3}
i=4 18 → need 7 → seen at 1 ✓ → positions 1 and 4
Sorting + two pointers also finds the pair, but loses the original positions and costs O(n log n). The map keeps positions and runs in O(n).
Shape 2: Signature Grouping
words: stone pale notes bird leap onset plea tones
signature = letters sorted:
stone → enost pale → aelp notes → enost bird → bdir
leap → aelp onset → enost plea → aelp tones → enost
groups: {enost: stone notes onset tones} {aelp: pale leap plea} {bdir: bird}
Sorting each word costs O(L log L). For lowercase letters, a 26-slot count array (turned into a string or tuple) gives an O(L) signature.
Shape 3: Frequency Count
"swiss" → counts {s:3, w:1, i:1}
second pass in original order: s(3) w(1) ← first with count 1 → 'w'
Two passes: count everything, then scan in the original order. Many "compare two collections" problems are also frequency maps: two strings are rearrangements of each other ⇔ identical counts.
Shape 4: Membership Set
values: [10, 4, 21, 3, 11, 5, 2, 12, 13] (unsorted)
longest run of consecutive integers?
set = {10, 4, 21, 3, 11, 5, 2, 12, 13}
only START a walk at v when v−1 is NOT in the set:
10: 9 absent → walk 10,11,12,13 → length 4
4: 3 present → skip (not a start)
21: 20 absent → walk 21 → length 1
2: 1 absent → walk 2,3,4,5 → length 4
...
best = 4 — in O(n), no sorting.
Shape 5: Last-Seen Index
events: [4, 9, 2, 7, 9, 4], "same event twice within k positions?" i=4 value 9: last seen at 1 → distance 3 k = 2 → too far; k = 3 → yes Store only the LATEST position — an older one can never be closer.
Code: All Five Shapes
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class HashingPatterns {
// 1. Complement lookup — two positions whose values add to target
static int[] pairIndices(int[] values, int target) {
Map<Integer, Integer> indexOf = new HashMap<>();
for (int i = 0; i < values.length; i++) {
Integer j = indexOf.get(target - values[i]);
if (j != null) return new int[]{j, i};
indexOf.put(values[i], i);
}
return null;
}
// 2. Signature grouping — words made of the same letters (insertion order kept)
static List<List<String>> groupByLetters(String[] words) {
Map<String, List<String>> groups = new LinkedHashMap<>();
for (String w : words) {
char[] letters = w.toCharArray();
Arrays.sort(letters);
groups.computeIfAbsent(new String(letters), k -> new ArrayList<>()).add(w);
}
return new ArrayList<>(groups.values());
}
// 3. Frequency count — first character that appears exactly once
static char firstUniqueChar(String s) {
Map<Character, Integer> freq = new HashMap<>();
for (char ch : s.toCharArray()) freq.merge(ch, 1, Integer::sum);
for (char ch : s.toCharArray()) if (freq.get(ch) == 1) return ch;
return '-';
}
// 4. Membership set — longest streak of consecutive integers, unsorted input
static int longestStreak(int[] values) {
Set<Integer> present = new HashSet<>();
for (int v : values) present.add(v);
int best = 0;
for (int v : present) {
if (present.contains(v - 1)) continue; // not the start of a streak
int length = 1;
while (present.contains(v + length)) length++;
best = Math.max(best, length);
}
return best;
}
// 5. Last-seen index — any repeat within distance k?
static boolean repeatWithin(int[] values, int k) {
Map<Integer, Integer> last = new HashMap<>();
for (int i = 0; i < values.length; i++) {
Integer prev = last.get(values[i]);
if (prev != null && i - prev <= k) return true;
last.put(values[i], i);
}
return false;
}
public static void main(String[] args) {
int[] receipt = {12, 7, 30, 5, 18, 9};
for (int budget : new int[]{25, 39, 100}) {
int[] p = pairIndices(receipt, budget);
System.out.println("items summing to " + budget + ": "
+ (p == null ? "none" : "positions " + p[0] + " and " + p[1]));
}
String[] words = {"stone", "pale", "notes", "bird", "leap", "onset", "plea", "tones"};
for (List<String> g : groupByLetters(words)) System.out.println("group: " + String.join(" ", g));
for (String s : new String[]{"swiss", "level", "aabb"})
System.out.println("first unique in " + s + ": " + firstUniqueChar(s));
int[] nums = {10, 4, 21, 3, 11, 5, 2, 12, 13};
System.out.println("longest streak in " + Arrays.toString(nums) + ": " + longestStreak(nums));
int[] log = {4, 9, 2, 7, 9, 4};
for (int k : new int[]{2, 3})
System.out.println("repeat within " + k + " in " + Arrays.toString(log) + ": " + repeatWithin(log, k));
}
}Output:
items summing to 25: positions 1 and 4
items summing to 39: positions 2 and 5
items summing to 100: none
group: stone notes onset tones
group: pale leap plea
group: bird
first unique in swiss: w
first unique in level: v
first unique in aabb: -
longest streak in [10, 4, 21, 3, 11, 5, 2, 12, 13]: 4
repeat within 2 in [4, 9, 2, 7, 9, 4]: false
repeat within 3 in [4, 9, 2, 7, 9, 4]: true
Choosing the Right Key
The real skill in hashing problems is designing the key. Ask: what do two items have in common when they should be treated as the same?
| Problem says… | Key |
|---|---|
| "same letters, any order" | sorted string, or 26-count tuple |
"same shape after shifting" (e.g. abc ~ bcd) | differences between neighbouring letters |
| "same row/column/box" (grids) | (r, c), r − c, r + c, (r/3, c/3) |
| "same remainder" | value % k |
| "same prefix sum" | the running total (see Prefix Sum) |
| "same point" (geometry) | (x, y) tuple or x * BIG + y |
Complexity and Trade-offs
TIME: O(1) average per insert / lookup → most patterns are O(n)
Worst case O(n) per operation under heavy collisions.
SPACE: O(n) for the stored keys — the price of the speed-up.
ALTERNATIVES when memory is tight:
- sort + two pointers O(n log n) time, O(1) extra
- a fixed-size array as a "map" when keys are small integers or letters
(int[26] for lowercase letters is faster
than any hash map)
How to Spot It
✓ "Have I seen ... before?" / "find a duplicate" / "first unique" ✓ "Pair / complement" on UNSORTED data where positions matter ✓ "Group items that are equivalent" ✓ "Count occurrences" / "most frequent" / "same counts" ✓ An O(n²) brute force whose inner loop is just a search → replace with a lookup
Common Mistakes
Inserting before looking up in complement problems — a value pairs with itself.
Using a mutable or unhashable key. Python lists can't be dict keys (use tuples or strings). In Java, mutating an object after using it as a key breaks lookups.
Assuming insertion order. Java HashMap and C++ unordered_map don't preserve it. Use LinkedHashMap, a separate vector, or sort the output if the order matters.
Counting with a set. A set only knows "present or not". If multiplicity matters, use a map of counts.
Overusing maps for small fixed alphabets. int[26] is simpler and faster than a map of 26 letters.
Interview Questions
Q: Why is the consecutive-streak solution O(n) even though there is a while loop inside the for loop?
The inner walk only starts at values whose predecessor is missing — the smallest member of each streak. Each value is then visited by exactly one walk (its own streak's), so all inner iterations together total at most n.
Q: How would you group words by letters in O(total characters) instead of O(L log L) per word?
Use a 26-element count array per word as the signature (converted to a string like "1#0#2#..." or a tuple). Building it is O(L), and two words share it exactly when they have the same letter counts.
Q: When is a hash map the wrong choice?
When the keys need to be ordered (use a balanced tree map for "next larger key" queries), when memory is constrained and sorting is acceptable, or when keys are small dense integers where a plain array indexed by value is faster and simpler.
Summary
- ›Hashing trades O(n) memory for O(1) lookups, turning many O(n²) searches into O(n).
- ›Five shapes: complement, signature grouping, frequency count, membership set, last-seen index.
- ›The craft is in designing the key that captures "these should be treated as the same".
- ›Look up before inserting, keep counts when multiplicity matters, and prefer arrays for small fixed alphabets.
Next, Binary Search Pattern shows how to recognise "search on a sorted space" in problems that never mention searching — including searching over the answer itself.
Unsorted list, find two positions whose values add to a target. What does the hash map store?