Chapter 14: Hash Tables, Maps and Sets
14.06Beginner / Intermediate6 topics18 min6 interactive animations

Counting patterns

Hash maps as counters: the frequency map, majority element, grouping anagrams, first unique element, duplicate detection and subarray sums equal to k.

Learning goals

  • Count occurrences with merge and read the counts back
  • Use sorted-letter keys to group anagrams
  • Combine prefix sums with a map to count subarrays with sum k

Prerequisites

Java mapsJava sets
14.06.1

Frequency map

The **frequency map** (Map<T, Integer>) is the most common hash-table pattern: loop once, counts.merge(x, 1, Integer::sum), and then ask questions of the counts. It replaces nested loops that would otherwise compare every pair (O(n²)) with a single O(n) pass.

When the keys are small dense integers or letters, an int[] count array is faster and smaller; use a map when keys are strings, large numbers or objects. Run the animation with “Count with merge”.

Java
1static Map<Character, Integer> letterCounts(String s) {
2 Map<Character, Integer> counts = new HashMap<>();
3 for (char c : s.toCharArray()) counts.merge(c, 1, Integer::sum);
4 return counts;
5}

getOrDefault, merge and computeIfAbsent

Interactive · edit the input

Your input

Try

words

to0
be1
or2
not3
to4
be5

map (shown in first-insertion order; HashMap order is not guaranteed)

∅
Setup

An empty map from a word to how many times it has appeared.

Step 1 / 14
Java
1Map<String, Integer> counts = new HashMap<>();
2for (String w : words) counts.put(w, counts.getOrDefault(w, 0) + 1); // option 0: a default for a missing key
3for (String w : words) counts.merge(w, 1, Integer::sum); // option 1: insert 1, or combine with the old value
4Map<Integer, List<String>> byLength = new HashMap<>();
5for (String w : words) byLength.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w); // option 2: create the list on first use

Live state

words

6

No condition evaluated on this step
Execution trace (dry run table)Show 14 steps
StepLineStateWhat happens
1L1words=6An empty map from a word to how many times it has appeared.
2L2w=to getOrDefault=0getOrDefault("to", 0): the key is missing, so it returns the default 0.
3L2w=to count=1put("to", 0 + 1): two map operations (a get and a put) for one update.
4L2w=be getOrDefault=0getOrDefault("be", 0): the key is missing, so it returns the default 0.
5L2w=be count=1put("be", 0 + 1): two map operations (a get and a put) for one update.
6L2w=or getOrDefault=0getOrDefault("or", 0): the key is missing, so it returns the default 0.
7L2w=or count=1put("or", 0 + 1): two map operations (a get and a put) for one update.
8L2w=not getOrDefault=0getOrDefault("not", 0): the key is missing, so it returns the default 0.
9L2w=not count=1put("not", 0 + 1): two map operations (a get and a put) for one update.
10L2w=to getOrDefault=1getOrDefault("to", 0) returns the stored 1.
11L2w=to count=2put("to", 1 + 1): two map operations (a get and a put) for one update.
12L2w=be getOrDefault=1getOrDefault("be", 0) returns the stored 1.
13L2w=be count=2put("be", 1 + 1): two map operations (a get and a put) for one update.
14L2return={to=2, be=2, or=1, not=1}Final map: {to=2, be=2, or=1, not=1}.
14.06.2

Majority element

A majority element appears **more than n/2 times**. Count with a map and return as soon as a count passes n/2. At most one value can exceed half the array, so the first one that does is the answer.

Exactly half is not enough: in [1, 1, 2, 2] no value exceeds 2, so there is no majority. The Boyer-Moore voting algorithm finds the majority in O(1) space, but the map version is simpler and also handles variants such as “more than n/3”.

Java
1static int majority(int[] nums) {
2 Map<Integer, Integer> counts = new HashMap<>();
3 for (int x : nums) {
4 int c = counts.merge(x, 1, Integer::sum);
5 if (c > nums.length / 2) return x; // strictly more than half of all elements
6 }
7 return -1; // no majority
8}

Best

O(1)

Average

O(n)

Worst

O(n)

Space

O(n)

Majority element with a frequency map

Interactive · edit the input

Your input

Try

int[] arr · length 7

2
2
1
1
2
3
2
0123456
Current elementCountedMajority element

counts

∅
Setup

A majority element appears more than ⌊7/2⌋ = 3 times, so it needs at least 4.

Step 1 / 9
Java
1static int majority(int[] nums) {
2 Map<Integer, Integer> counts = new HashMap<>();
3 for (int x : nums) {
4 int c = counts.merge(x, 1, Integer::sum);
5 if (c > nums.length / 2) return x; // strictly more than half of all elements
6 }
7 return -1; // no majority
8}

Live state

0 map updates

n

7

needed

> 3

No condition evaluated on this step
Execution trace (dry run table)Show 9 steps
StepLineStateWhat happens
1L2n=7 needed=> 3A majority element appears more than ⌊7/2⌋ = 3 times, so it needs at least 4.
2L5x=2 count=12 has been seen 1 time; not above 3 yet.
3L5x=2 count=22 has been seen 2 times; not above 3 yet.
4L5x=1 count=11 has been seen 1 time; not above 3 yet.
5L5x=1 count=21 has been seen 2 times; not above 3 yet.
6L5x=2 count=32 has been seen 3 times; not above 3 yet.
7L5x=3 count=13 has been seen 1 time; not above 3 yet.
8L5x=2 count=42 has now been seen 4 times, more than half of the 7 elements.
9L5return=2Majority element 2: found after 7 of 7 elements, no second pass needed.
14.06.3

Group anagrams

Anagrams share exactly the same letters, so sorting the letters of a word gives a **canonical key**: eat, tea and ate all become aet. Put each word into the list stored under its key. The values of the map are the groups.

Sorting a word of length k costs O(k log k), so n words cost O(n · k log k). If words only use a–z, a 26-count signature is an alternative key that avoids the sort. A LinkedHashMap keeps groups in first-seen order, which makes the output reproducible: [[eat, tea, ate], [tan, nat], [bat]] for the classic input.

Java
1static List<List<String>> groupAnagrams(String[] words) {
2 Map<String, List<String>> groups = new LinkedHashMap<>();
3 for (String w : words) {
4 char[] cs = w.toCharArray(); Arrays.sort(cs);
5 groups.computeIfAbsent(new String(cs), k -> new ArrayList<>()).add(w); // anagrams share the sorted key
6 }
7 return new ArrayList<>(groups.values());
8}

Group anagrams by sorted key

Interactive · edit the input

Your input

Try

words

eat0
tea1
tan2
ate3
nat4
bat5

map: sorted key → group

∅
Setup

Two words are anagrams exactly when their letters sorted give the same string, so the sorted word is a perfect hash key for the group.

Step 1 / 14
Java
1static List<List<String>> groupAnagrams(String[] words) {
2 Map<String, List<String>> groups = new LinkedHashMap<>();
3 for (String w : words) {
4 char[] cs = w.toCharArray(); Arrays.sort(cs);
5 groups.computeIfAbsent(new String(cs), k -> new ArrayList<>()).add(w); // anagrams share the sorted key
6 }
7 return new ArrayList<>(groups.values());
8}

Live state

words

6

No condition evaluated on this step
Execution trace (dry run table)Show 14 steps
StepLineStateWhat happens
1L2words=6Two words are anagrams exactly when their letters sorted give the same string, so the sorted word is a perfect hash key for the group.
2L4word=eat key=aetSort the letters of "eat": key "aet".
3L5word=eat key=aet group size=1Key "aet" is new: start a group with "eat".
4L4word=tea key=aetSort the letters of "tea": key "aet".
5L5word=tea key=aet group size=2Key "aet" exists: "tea" joins that group.
6L4word=tan key=antSort the letters of "tan": key "ant".
7L5word=tan key=ant group size=1Key "ant" is new: start a group with "tan".
8L4word=ate key=aetSort the letters of "ate": key "aet".
9L5word=ate key=aet group size=3Key "aet" exists: "ate" joins that group.
10L4word=nat key=antSort the letters of "nat": key "ant".
11L5word=nat key=ant group size=2Key "ant" exists: "nat" joins that group.
12L4word=bat key=abtSort the letters of "bat": key "abt".
13L5word=bat key=abt group size=1Key "abt" is new: start a group with "bat".
14L7groups=3 return=[[eat, tea, ate], [tan, nat], [bat]]3 groups: [[eat, tea, ate], [tan, nat], [bat]]. Cost: O(n · k log k) for n words of length k.
14.06.4

First unique

To find the first element that appears exactly once, count everything in a first pass, then scan the array again in order and return the first index whose count is 1. Two passes are needed because you cannot know an element is unique until you have seen the whole array.

The array order, not the map order, decides which unique element comes first, which is why the second pass walks the array and not the map.

Java
1static int firstUnique(int[] a) {
2 Map<Integer, Integer> count = new HashMap<>();
3 for (int x : a) count.merge(x, 1, Integer::sum); // pass 1: count every value
4 for (int i = 0; i < a.length; i++)
5 if (count.get(a[i]) == 1) return i; // pass 2: first value with count 1
6 return -1;
7}

First unique element with two passes

Interactive · edit the input

Your input

Try

int[] arr · length 7

4
2
4
1
2
3
3
0123456
Current elementCountedFirst uniqueRepeats

counts

∅
Setup

Pass 1 counts each value; pass 2 walks the array again in order and stops at the first value whose count is 1.

Step 1 / 13
Java
1static int firstUnique(int[] a) {
2 Map<Integer, Integer> count = new HashMap<>();
3 for (int x : a) count.merge(x, 1, Integer::sum); // pass 1: count every value
4 for (int i = 0; i < a.length; i++)
5 if (count.get(a[i]) == 1) return i; // pass 2: first value with count 1
6 return -1;
7}

Live state

n

7

No condition evaluated on this step
Execution trace (dry run table)Show 13 steps
StepLineStateWhat happens
1L2n=7Pass 1 counts each value; pass 2 walks the array again in order and stops at the first value whose count is 1.
2L3x=4 count=1Count 4: now 1.
3L3x=2 count=1Count 2: now 1.
4L3x=4 count=2Count 4: now 2.
5L3x=1 count=1Count 1: now 1.
6L3x=2 count=2Count 2: now 2.
7L3x=3 count=1Count 3: now 1.
8L3x=3 count=2Count 3: now 2.
9L5i=0 x=4 count=24 appears 2 times: skip it.
10L5i=1 x=2 count=22 appears 2 times: skip it.
11L5i=2 x=4 count=24 appears 2 times: skip it.
12L5i=3 x=1 count=11 appears once: it is the first unique element.
13L5return=3Return index 3 (value 1).
14.06.5

Duplicate detection

To detect a duplicate, remember what you have seen. Set.add returns false when the value was already present, so the check and the insertion happen in one expression. The scan stops at the first duplicate, so the work done is proportional to the position of that duplicate, not to n.

Alternatives: sorting first (O(n log n), no extra space beyond the sort) and, for values in a small range, a boolean array. The hash set is the O(n) general answer.

Java
1static boolean containsDuplicate(int[] a) {
2 Set<Integer> seen = new HashSet<>();
3 for (int x : a)
4 if (!seen.add(x)) return true; // add returns false when x was already in the set
5 return false;
6}

Duplicate detection with a HashSet

Interactive · edit the input

Your input

Try

int[] arr · length 6

3
1
4
1
5
9
012345
Current elementIn the setDuplicate

seen (HashSet)

∅
Setup

An empty set. add(x) returns false when x is already stored, which answers 'seen before?' and records x in one O(1) step.

Step 1 / 6
Java
1static boolean containsDuplicate(int[] a) {
2 Set<Integer> seen = new HashSet<>();
3 for (int x : a)
4 if (!seen.add(x)) return true; // add returns false when x was already in the set
5 return false;
6}

Live state

n

6

No condition evaluated on this step
Execution trace (dry run table)Show 6 steps
StepLineStateWhat happens
1L2n=6An empty set. add(x) returns false when x is already stored, which answers 'seen before?' and records x in one O(1) step.
2L4x=3 add returns=true3 is new: add stores it and returns true.
3L4x=1 add returns=true1 is new: add stores it and returns true.
4L4x=4 add returns=true4 is new: add stores it and returns true.
5L4x=1 add returns=false1 is already in the set: add returns false, so this is a duplicate.
6L4return=trueDuplicate 1 found at index 3. Return true after scanning 4 of 6 elements.
14.06.6

Subarray sum equals k

Count the contiguous subarrays whose sum is k. With prefix sums, the sum of a[i..j] is prefix[j+1] - prefix[i], so a subarray ending at j has sum k exactly when an earlier prefix equals current - k. Keep a map from prefix sum to how many times it has occurred, and add map[current - k] to the answer at every step.

Seed the map with {0: 1} so subarrays that start at index 0 are counted. This handles negative numbers and zeros, which break the sliding window technique. The animation marks every element that ends a matching subarray.

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(n)

Count subarrays with sum k (prefix sums + hash map)

Interactive · edit the input

Your input

Try

int[] arr · length 6

1
2
3
-3
1
1
012345
Current elementEnds a matching subarrayProcessed

seen: prefix → times

0:1
Setup

A subarray a[l..r] has sum k exactly when prefix[r + 1] - prefix[l] = k. Remember how often each prefix has occurred.

Step 1 / 14
Java
1static int countSubarraysWithSum(int[] a, int k) {
2 Map<Integer, Integer> seen = new HashMap<>();
3 seen.put(0, 1); // the empty prefix
4 int prefix = 0, count = 0;
5 for (int x : a) {
6 prefix += x;
7 count += seen.getOrDefault(prefix - k, 0); // earlier prefixes that leave sum k
8 seen.merge(prefix, 1, Integer::sum);
9 }
10 return count;
11}

Live state

prefix

0

count

0

k

3

No condition evaluated on this step
Execution trace (dry run table)Show 14 steps
StepLineStateWhat happens
1L3prefix=0 count=0 k=3A subarray a[l..r] has sum k exactly when prefix[r + 1] - prefix[l] = k. Remember how often each prefix has occurred.
2L7i=0 prefix=1 need=-2 hits=0 count=0Nothing ending at 0 has prefix -2 before it: no new match.
3L8i=0 prefix=1 count=0Record prefix 1 (now seen 1 time).
4L7i=1 prefix=3 need=0 hits=1 count=1prefix 3 - k 3 = 0 was seen 1 time, so 1 subarray ending at 1 sum to 3. Count = 1.
5L8i=1 prefix=3 count=1Record prefix 3 (now seen 1 time).
6L7i=2 prefix=6 need=3 hits=1 count=2prefix 6 - k 3 = 3 was seen 1 time, so 1 subarray ending at 2 sum to 3. Count = 2.
7L8i=2 prefix=6 count=2Record prefix 6 (now seen 1 time).
8L7i=3 prefix=3 need=0 hits=1 count=3prefix 3 - k 3 = 0 was seen 1 time, so 1 subarray ending at 3 sum to 3. Count = 3.
9L8i=3 prefix=3 count=3Record prefix 3 (now seen 2 times).
10L7i=4 prefix=4 need=1 hits=1 count=4prefix 4 - k 3 = 1 was seen 1 time, so 1 subarray ending at 4 sum to 3. Count = 4.
11L8i=4 prefix=4 count=4Record prefix 4 (now seen 1 time).
12L7i=5 prefix=5 need=2 hits=0 count=4Nothing ending at 5 has prefix 2 before it: no new match.
13L8i=5 prefix=5 count=4Record prefix 5 (now seen 1 time).
14L10return=44 subarrays sum to 3. One pass, O(n) time and space; negative numbers are fine, unlike a sliding window.

Practice ladder

Now try it yourself

Easy

  • Majority Element
  • First Unique Character in a String
  • Valid Anagram

Medium

  • Group Anagrams
  • Subarray Sum Equals K
  • Top K Frequent Elements

Advanced

  • Contiguous Array
  • Count Number of Nice Subarrays

Key takeaways

  • A frequency map turns nested loops into one pass: merge(x, 1, Integer::sum).
  • A canonical key such as sorted letters groups equivalent items.
  • Prefix sum plus a map counts subarrays with sum k in O(n), even with negatives.