Chapter 14: Hash Tables, Maps and Sets
14.07Beginner / Intermediate5 topics16 min5 interactive animations

Set-based problems

Problems solved with sets and hashing: union and intersection, membership tests, the longest consecutive sequence, two sum with a hash and counting distinct values in every window.

Learning goals

  • Compute union, intersection and difference with sets
  • Solve longest consecutive sequence in O(n) by only starting at run starts
  • Maintain a distinct count over a sliding window

Prerequisites

Java setsCounting patterns
14.07.1

Set union/intersection

Sets model the mathematical operations directly. **Union** a.addAll(b) keeps everything; **intersection** a.retainAll(b) keeps what b also has; **difference** a.removeAll(b) drops what b has; the **symmetric difference** is the union minus the intersection. With hash sets each membership test is O(1), so every operation is O(|a| + |b|).

Choose an operation in the animation. A LinkedHashSet keeps the result in insertion order, which makes the output predictable: for A = {1, 2, 3, 4} and B = {3, 4, 5, 6} the union is [1, 2, 3, 4, 5, 6], the intersection [3, 4], the difference [1, 2] and the symmetric difference [1, 2, 5, 6].

Java
1static void setOps(List<Integer> listA, List<Integer> listB) {
2 Set<Integer> a = new LinkedHashSet<>(listA), b = new LinkedHashSet<>(listB);
3 Set<Integer> union = new LinkedHashSet<>(a); union.addAll(b);
4 Set<Integer> inter = new LinkedHashSet<>(a); inter.retainAll(b);
5 Set<Integer> diff = new LinkedHashSet<>(a); diff.removeAll(b);
6 Set<Integer> sym = new LinkedHashSet<>(union); sym.removeAll(inter);
7 System.out.println(union + " " + inter + " " + diff + " " + sym);
8}

Set union, intersection and difference

Interactive · edit the input

Your input

Try

A

1
2
3
4

B

3
4
5
6

A ∪ B

∅
Setup

Both sets use hashing, so each membership test below costs O(1); the whole operation is O(|A| + |B|). A LinkedHashSet keeps insertion order so the result is predictable.

Step 1 / 10
Java
1Set<Integer> a = new LinkedHashSet<>(listA), b = new LinkedHashSet<>(listB);
2Set<Integer> union = new LinkedHashSet<>(a); union.addAll(b); // option 0
3Set<Integer> inter = new LinkedHashSet<>(a); inter.retainAll(b); // option 1: keep what b also has
4Set<Integer> diff = new LinkedHashSet<>(a); diff.removeAll(b); // option 2: drop what b has
5Set<Integer> sym = new LinkedHashSet<>(union); sym.removeAll(inter); // option 3: in exactly one set

Live state

|A|

4

|B|

4

No condition evaluated on this step
Execution trace (dry run table)Show 10 steps
StepLineStateWhat happens
1L1|A|=4 |B|=4Both sets use hashing, so each membership test below costs O(1); the whole operation is O(|A| + |B|). A LinkedHashSet keeps insertion order so the result is predictable.
2L2value=1 in B=false1: every element of A belongs to the union, so it is added to the result.
3L2value=2 in B=false2: every element of A belongs to the union, so it is added to the result.
4L2value=3 in B=true3: every element of A belongs to the union, so it is added to the result.
5L2value=4 in B=true4: every element of A belongs to the union, so it is added to the result.
6L2value=3 in A=true3: already in the result, so it is skipped.
7L2value=4 in A=true4: already in the result, so it is skipped.
8L2value=5 in A=false5: addAll adds B's elements; a duplicate is ignored, so it is added to the result.
9L2value=6 in A=false6: addAll adds B's elements; a duplicate is ignored, so it is added to the result.
10L2size=6 return=[1, 2, 3, 4, 5, 6]Result: [1, 2, 3, 4, 5, 6].
14.07.2

Membership

The basic set question is *is x in the collection?* A list answers it in O(n); a sorted array in O(log n) with binary search; a hash set in expected O(1). When a program tests membership many times, building a set once (O(n)) and querying it pays for itself quickly.

Try the first preset of the hash table animation and add ?22 and ?99: a hit scans part of one chain, a miss scans one whole chain and nothing else.

StructureBuildcontains(x)
Unsorted listO(n)O(n)
Sorted arrayO(n log n)O(log n)
HashSetO(n)O(1) expected
TreeSetO(n log n)O(log n)

Hash table with separate chaining

Interactive · edit the input

Your input

Try

bucket 0

∅

bucket 1

∅

bucket 2

∅

bucket 3

∅

bucket 4

∅
Setup

5 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain.

Step 1 / 30
Java
1class ChainedHashSet {
2 private final int m, variant, a; // m buckets
3 private final LinkedList<Integer>[] table; // one chain per bucket
4 int hash(int key) { // the option picks one variant
5 switch (variant) {
6 case 0: return Math.floorMod(key, m);
7 case 1: return (int) (m * ((key * 0.6180339887) % 1.0));
8 case 2: return 0; // worst case: every key collides
9 default: return ((a * key + 7) % 101) % m; // universal-style family
10 }
11 }
12 boolean add(int key) {
13 LinkedList<Integer> chain = table[hash(key)];
14 if (chain.contains(key)) return false; // walk the chain: duplicate
15 chain.add(key); return true; // append to the chain
16 }
17 boolean contains(int key) { return table[hash(key)].contains(key); }
18 boolean remove(int key) { return table[hash(key)].remove((Integer) key); }
19}

Live state

0 key comparisons

size

0

load factor

0.00

longest chain

0

No condition evaluated on this step
Execution trace (dry run table)Show 30 steps
StepLineStateWhat happens
1L3size=0 load factor=0.00 longest chain=05 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain.
2L6size=0 load factor=0.00 longest chain=0 op=add(12) hash=2add(12): hash = 12 % 5 = 2, so go to bucket 2.
3L15size=1 load factor=0.20 longest chain=1 op=add(12) return=true12 is not in the chain (the bucket was empty): append it to bucket 2.
4L6size=1 load factor=0.20 longest chain=1 op=add(7) hash=2add(7): hash = 7 % 5 = 2, so go to bucket 2.
5L14size=1 load factor=0.20 longest chain=1 op=add(7) hash=212 ≠ 7: keep walking the chain.
6L15size=2 load factor=0.40 longest chain=2 op=add(7) return=true7 is not in the chain (1 comparison): append it to bucket 2.
7L6size=2 load factor=0.40 longest chain=2 op=add(22) hash=2add(22): hash = 22 % 5 = 2, so go to bucket 2.
8L14size=2 load factor=0.40 longest chain=2 op=add(22) hash=212 ≠ 22: keep walking the chain.
9L14size=2 load factor=0.40 longest chain=2 op=add(22) hash=27 ≠ 22: keep walking the chain.
10L15size=3 load factor=0.60 longest chain=3 op=add(22) return=true22 is not in the chain (2 comparisons): append it to bucket 2.
11L6size=3 load factor=0.60 longest chain=3 op=add(17) hash=2add(17): hash = 17 % 5 = 2, so go to bucket 2.
12L14size=3 load factor=0.60 longest chain=3 op=add(17) hash=212 ≠ 17: keep walking the chain.
13L14size=3 load factor=0.60 longest chain=3 op=add(17) hash=27 ≠ 17: keep walking the chain.
14L14size=3 load factor=0.60 longest chain=3 op=add(17) hash=222 ≠ 17: keep walking the chain.
15L15size=4 load factor=0.80 longest chain=4 op=add(17) return=true17 is not in the chain (3 comparisons): append it to bucket 2.
16L6size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2contains(22): hash = 22 % 5 = 2, so go to bucket 2.
17L17size=4 load factor=0.80 longest chain=4 op=contains(22) hash=212 ≠ 22: keep walking the chain.
18L17size=4 load factor=0.80 longest chain=4 op=contains(22) hash=27 ≠ 22: keep walking the chain.
19L17size=4 load factor=0.80 longest chain=4 op=contains(22) hash=222 equals 22: found after 3 comparisons.
20L17size=4 load factor=0.80 longest chain=4 op=contains(22) return=truecontains(22) is true.
21L6size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2remove(7): hash = 7 % 5 = 2, so go to bucket 2.
22L18size=4 load factor=0.80 longest chain=4 op=remove(7) hash=212 ≠ 7: keep walking the chain.
23L18size=4 load factor=0.80 longest chain=4 op=remove(7) hash=27 equals 7: found after 2 comparisons.
24L18size=3 load factor=0.60 longest chain=3 op=remove(7) return=true7 unlinked from bucket 2.
25L6size=3 load factor=0.60 longest chain=3 op=add(32) hash=2add(32): hash = 32 % 5 = 2, so go to bucket 2.
26L14size=3 load factor=0.60 longest chain=3 op=add(32) hash=212 ≠ 32: keep walking the chain.
27L14size=3 load factor=0.60 longest chain=3 op=add(32) hash=222 ≠ 32: keep walking the chain.
28L14size=3 load factor=0.60 longest chain=3 op=add(32) hash=217 ≠ 32: keep walking the chain.
29L15size=4 load factor=0.80 longest chain=4 op=add(32) return=true32 is not in the chain (3 comparisons): append it to bucket 2.
30L19size=4 load factor=0.80 longest chain=4 return=[[],[],[12,22,17,32],[],[]]4 keys in 5 buckets; the longest chain has 4. Lookup cost is the chain length, so the spread of the hash function decides the speed.
14.07.3

Longest consecutive sequence

Find the length of the longest run of consecutive integers (such as 1, 2, 3, 4) in an unsorted array, in O(n). Put all values in a set. A value x *starts* a run only if x - 1 is not in the set. For each run start, walk x+1, x+2, … while they are in the set.

The “start of a run” test is what makes it linear: each value is visited as a run start at most once, and walked over at most once as part of a run, so the total is O(n) lookups. Without the test the same run would be re-walked from every element and the cost would be O(n²).

Java
1static int longestConsecutive(int[] nums) {
2 Set<Integer> set = new HashSet<>();
3 for (int x : nums) set.add(x);
4 int best = 0;
5 for (int x : set) {
6 if (set.contains(x - 1)) continue; // not the start of a run
7 int end = x;
8 while (set.contains(end + 1)) end++; // walk the run
9 best = Math.max(best, end - x + 1);
10 }
11 return best;
12}

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(n)

Longest consecutive sequence with a HashSet

Interactive · edit the input

Your input

Try

int[] arr · length 6

100
4
200
1
3
2
012345
Run start being testedCurrent runBest runNot a run start

set (iteration order of a HashSet is unspecified; the answer does not depend on it)

100
4
200
1
3
2
Setup

The set has 6 distinct values. Sorting would cost O(n log n); instead only the first value of each run starts a walk, so every value is visited O(1) times.

Step 1 / 14
Java
1static int longestConsecutive(int[] nums) {
2 Set<Integer> set = new HashSet<>();
3 for (int x : nums) set.add(x);
4 int best = 0;
5 for (int x : set) {
6 if (set.contains(x - 1)) continue; // not the start of a run
7 int end = x;
8 while (set.contains(end + 1)) end++; // walk the run
9 best = Math.max(best, end - x + 1);
10 }
11 return best;
12}

Live state

0 set lookups

n

6

distinct

6

No condition evaluated on this step
Execution trace (dry run table)Show 14 steps
StepLineStateWhat happens
1L3n=6 distinct=6The set has 6 distinct values. Sorting would cost O(n log n); instead only the first value of each run starts a walk, so every value is visited O(1) times.
2L6x=100 best=099 is not in the set: 100 starts a run.
3L9x=100 end=100 length=1 best=1101 is not in the set: the run ends with length 1. best = 1.
4L6x=4 best=13 is in the set, so 4 is in the middle of a run: skip it.
5L6x=200 best=1199 is not in the set: 200 starts a run.
6L9x=200 end=200 length=1 best=1201 is not in the set: the run ends with length 1. best = 1.
7L6x=1 best=10 is not in the set: 1 starts a run.
8L8x=1 end=2 length=2 best=12 is in the set: the run 1..2 now has 2 values.
9L8x=1 end=3 length=3 best=13 is in the set: the run 1..3 now has 3 values.
10L8x=1 end=4 length=4 best=14 is in the set: the run 1..4 now has 4 values.
11L9x=1 end=4 length=4 best=45 is not in the set: the run ends with length 4. best = 4.
12L6x=3 best=42 is in the set, so 3 is in the middle of a run: skip it.
13L6x=2 best=41 is in the set, so 2 is in the middle of a run: skip it.
14L11return=4Longest consecutive run: 4 (1..4). 12 set lookups in total.
14.07.4

Two-sum hash

Two Sum asks for two indices whose values add up to a target. Scan once and keep a map from value to index of what you have seen. For each x, check whether target - x is already in the map: if it is, the pair is complete; if not, record x. This replaces the O(n²) nested loops by one O(n) pass with O(n) extra space.

Checking before inserting handles duplicates such as [3, 3] with target 6 and never pairs an element with itself.

Java
1static int[] twoSum(int[] nums, int target) {
2 Map<Integer, Integer> seen = new HashMap<>(); // value -> index
3 for (int i = 0; i < nums.length; i++) {
4 Integer j = seen.get(target - nums[i]); // beware: target - nums[i] can overflow for extreme values
5 if (j != null) return new int[]{j, i};
6 seen.put(nums[i], i);
7 }
8 return new int[0];
9}

Optimised: remember what you have seen

Interactive · edit the input

Your input

Try

int[] arr · length 6

7
11
2
15
4
9
012345
Being comparedMatch / answerStored in the map

seen (value:index)

empty
Setup

A hash map answers “have I seen this value?” in O(1) on average, so one pass is enough.

Step 1 / 7
Java
1public static int[] twoSum(int[] nums, int target) {
2 Map<Integer, Integer> seen = new HashMap<>(); // value -> index
3 for (int i = 0; i < nums.length; i++) {
4 int need = target - nums[i];
5 if (seen.containsKey(need)) {
6 return new int[] { seen.get(need), i };
7 }
8 seen.put(nums[i], i);
9 }
10 return new int[] { -1, -1 };
11}

Live state

0 lookups

target

13

No condition evaluated on this step
Execution trace (dry run table)Show 7 steps
StepLineStateWhat happens
1L2target=13A hash map answers “have I seen this value?” in O(1) on average, so one pass is enough.
2L5i=0 nums[i]=7 need=613 - 7 = 6, which has not appeared yet.
3L8i=0 size=1Store 7 → 0 for future lookups.
4L5i=1 nums[i]=11 need=213 - 11 = 2, which has not appeared yet.
5L8i=1 size=2Store 11 → 1 for future lookups.
6L5i=2 nums[i]=2 need=1111 is in the map: it was stored at index 1, so that value plus 2 makes 13.
7L6return=[1, 2]return [1, 2] after 3 lookups: O(n) time and O(n) extra memory. Brute force needed O(1) memory but O(n²) time.
14.07.5

Distinct window counts

How many distinct values are in each window of size k? Keep a map of counts for the current window. When a value enters, increment its count; when a value leaves, decrement its count and **remove the key when it reaches 0**; the number of distinct values is the map's size.

Forgetting to remove zero counts is the classic bug: the map would keep keys for values that are no longer in the window and size() would overcount. Every element enters and leaves once, so the scan is O(n).

Java
1static int[] distinctInWindows(int[] a, int k) {
2 int[] out = new int[a.length - k + 1];
3 Map<Integer, Integer> count = new HashMap<>();
4 for (int i = 0; i < a.length; i++) {
5 count.merge(a[i], 1, Integer::sum); // value enters the window
6 if (i >= k) {
7 int old = a[i - k]; // value leaves the window
8 if (count.merge(old, -1, Integer::sum) == 0) count.remove(old);
9 }
10 if (i >= k - 1) out[i - k + 1] = count.size(); // size() = distinct values
11 }
12 return out;
13}

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(k)

Distinct values in every window of size k

Interactive · edit the input

Your input

Try

int[] arr · length 7

1
2
1
3
4
2
3
0123456
Value enteringIn the window

count

∅
Setup

Slide a window of 4 over the array. The map holds the count of each value inside it; its size is the number of distinct values.

Step 1 / 16
Java
1static int[] distinctInWindows(int[] a, int k) {
2 int[] out = new int[a.length - k + 1];
3 Map<Integer, Integer> count = new HashMap<>();
4 for (int i = 0; i < a.length; i++) {
5 count.merge(a[i], 1, Integer::sum); // value enters the window
6 if (i >= k) {
7 int old = a[i - k]; // value leaves the window
8 if (count.merge(old, -1, Integer::sum) == 0) count.remove(old);
9 }
10 if (i >= k - 1) out[i - k + 1] = count.size(); // size() = distinct values
11 }
12 return out;
13}

Live state

n

7

k

4

No condition evaluated on this step
Execution trace (dry run table)Show 16 steps
StepLineStateWhat happens
1L3n=7 k=4Slide a window of 4 over the array. The map holds the count of each value inside it; its size is the number of distinct values.
2L5i=0 enters=1 distinct=11 enters: its count becomes 1.
3L5i=1 enters=2 distinct=22 enters: its count becomes 1.
4L5i=2 enters=1 distinct=21 enters: its count becomes 2.
5L5i=3 enters=3 distinct=33 enters: its count becomes 1.
6L10window=[0..3] distinct=3Window [0..3] holds 3 distinct values.
7L5i=4 enters=4 distinct=44 enters: its count becomes 1.
8L8i=4 leaves=1 distinct=41 leaves: its count drops to 1, so it is still inside the window.
9L10window=[1..4] distinct=4Window [1..4] holds 4 distinct values.
10L5i=5 enters=2 distinct=42 enters: its count becomes 2.
11L8i=5 leaves=2 distinct=42 leaves: its count drops to 1, so it is still inside the window.
12L10window=[2..5] distinct=4Window [2..5] holds 4 distinct values.
13L5i=6 enters=3 distinct=43 enters: its count becomes 2.
14L8i=6 leaves=1 distinct=31 leaves and its count reaches 0: remove the key, or size() would overcount.
15L10window=[3..6] distinct=3Window [3..6] holds 3 distinct values.
16L12return=[3, 4, 4, 3]Distinct counts per window: [3, 4, 4, 3]. O(n) time: each value enters and leaves once.

Practice ladder

Now try it yourself

Easy

  • Intersection of Two Arrays
  • Two Sum

Medium

  • Longest Consecutive Sequence
  • Find All Anagrams in a String

Advanced

  • Longest Substring with At Most K Distinct Characters
  • Substring with Concatenation of All Words

Key takeaways

  • Hash sets give O(1) membership, so set operations are O(|a| + |b|).
  • Only start a walk at a run start: that makes longest consecutive sequence O(n).
  • A count map over a window needs keys removed when their count reaches 0.