Java • Problem-Solving Patterns

HashMap for Counting & Grouping — Use HashMap for counting/frequency and grouping

Master the most common pattern in coding interviews: tallying occurrences and organizing data into groups with HashMap.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 9 figures

SECTION 1

Historical Context & Motivation

Counting things and sorting them into categories are two of the oldest tasks in computing. Before modern data structures existed, programmers had to scan through lists repeatedly to answer simple questions like "how many times does this word appear?" or "which items belong together?" These brute-force approaches were painfully slow, especially as data sets grew. The hash table — the underlying idea behind Java's HashMap — was invented to solve exactly this kind of problem: giving near-instant lookups so you can count and group data efficiently.

1953 • Birth of Hashing

IBM researcher Hans Peter Luhn files an internal memo describing hash tables — a technique for mapping keys to array slots using a mathematical function. This idea made lookups dramatically faster than scanning every element.

1995 • Java 1.0 Ships Hashtable

Java's first release included the Hashtable class, bringing hash-based key-value storage to a mainstream language. It was thread-safe but slower than many programmers needed.

1998 • Java Collections Framework & HashMap

Java 1.2 introduced the Collections Framework, including HashMap. It was faster for single-threaded code and quickly became the go-to structure for counting and grouping tasks.

2014 • Java 8 Supercharges HashMap

Java 8 added methods like getOrDefault(), merge(), and computeIfAbsent(), making counting and grouping patterns much more concise and readable.

The core question HashMap answers is simple but powerful: given a collection of items, how can we instantly look up how many times each item appears, or which items share a common property? Without a HashMap, you would need nested loops that run in O(n²) time. With one, you can do it in a single pass — O(n) time.

SECTION 2

Core Principles & Definitions

A HashMap stores data as key-value pairs. You give it a key (like a word), and it gives you back the associated value (like a count). Under the hood, it uses a hash function to convert each key into an index in an internal array, which is why lookups are nearly instantaneous. Two dominant patterns emerge when you use HashMaps in problem solving: counting (frequency mapping) and grouping (categorization).

1

Key-Value Pair

Every entry in a HashMap consists of a key (unique identifier) and a value (the data associated with that key). Example: key = "apple", value = 3.
2

Counting Pattern

Use the item as the key and an Integer count as the value. For each occurrence, increment the count by 1. This builds a frequency map.
3

Grouping Pattern

Use the shared property as the key and a List as the value. For each item, append it to the list that matches its category. This organizes items into buckets.
4

O(1) Average Lookup

Retrieving or updating a value by key takes constant time on average. This is why a single loop over your data is enough — each HashMap operation inside the loop is O(1).
✦

KEY TAKEAWAY

Think of a HashMap like a set of labeled mailboxes. Each mailbox has a unique name tag (the key). For counting, each mailbox holds a tally counter — every time you see a letter addressed to that name, you click the counter up by one. For grouping, each mailbox holds a folder — you drop every related letter into the matching folder. Either way, you never need to search through all the mailboxes; you go straight to the right one.

SECTION 3

Visual Explanation — The Counting Pattern

The diagram below shows what happens when you process the array ["cat", "dog", "cat", "bird", "dog", "cat"] through a frequency-counting HashMap. Each element is read once (left to right), and the map is updated in constant time at each step.

Counting Frequency: Processing an Array into a HashMapINPUT ARRAYcatdogcatbirddogcatSTEP-BY-STEP MAP STATEAfter index 0:{cat → 1}After index 1:{cat → 1, dog → 1}After index 2:{cat → 2, dog → 1}After index 3:{cat → 2, dog → 1, bird → 1}After index 4:{cat → 2, dog → 2, bird → 1}After index 5:{cat → 3, dog → 2, bird → 1}FINAL FREQUENCY MAPcat → 3dog → 2bird → 1
The input array is processed left to right. Each element becomes a key in the HashMap, and its value (the count) is incremented. After one complete pass, we have a full frequency map. Notice that "cat" appears 3 times — the map reflects this without ever needing a nested loop.

The key insight here is that we only loop through the array once. For each element, we check if the key already exists in the map. If it does, we add 1 to the current count. If it doesn't, we insert it with a count of 1. Since each put and get operation on a HashMap is O(1) on average, the entire process runs in O(n) time, where n is the length of the array.

SECTION 4

How It Works — Code Patterns

Pattern 1: Counting / Frequency Map

The counting pattern follows a simple template. You declare a HashMap<KeyType, Integer>, then loop through your data. For each element, you use getOrDefault(key, 0) to fetch the current count (defaulting to 0 if the key is new), add 1, and store the updated count back. Here's the standard idiom:

COUNTING IDIOM

map.put(key,map.getOrDefault(key,0)+1);map.put(key, map.getOrDefault(key, 0) + 1);

key — the element you are counting. getOrDefault(key, 0) — returns the current count, or 0 if the key hasn't been seen yet. The + 1 increments the tally.

A complete example counting character frequencies in a string looks like this:

FULL COUNTING EXAMPLE

HashMap<Character,Integer>freq=newHashMap<>();for(charc:str.toCharArray())freq.put(c,freq.getOrDefault(c,0)+1);HashMap<Character, Integer> freq = new HashMap<>(); for (char c : str.toCharArray()) { freq.put(c, freq.getOrDefault(c, 0) + 1); }

After this loop, freq maps every character in str to the number of times it appears.

Pattern 2: Grouping / Categorization

The grouping pattern uses a HashMap<KeyType, List<ValueType>>. The key is whatever property you're grouping by, and the value is a list collecting all items that share that property. The critical step is creating the list the first time you encounter a new key.

GROUPING IDIOM

map.computeIfAbsent(key,k−>newArrayList<>()).add(item);map.computeIfAbsent(key, k -> new ArrayList<>()).add(item);

computeIfAbsent(key, k -> new ArrayList<>()) — if the key doesn't exist yet, it creates a new empty list and associates it with the key. Either way, it returns the list. .add(item) — appends the current item to that list.

FULL GROUPING EXAMPLE — GROUP WORDS BY LENGTH

HashMap<Integer,List<String>>groups=newHashMap<>();for(Stringword:words)groups.computeIfAbsent(word.length(),k−>newArrayList<>()).add(word);HashMap<Integer, List<String>> groups = new HashMap<>(); for (String word : words) { groups.computeIfAbsent(word.length(), k -> new ArrayList<>()).add(word); }

After this loop, groups maps each word length (e.g., 3, 5, 7) to a list of all words with that length.

💡

Alternative Counting Syntax

You may also see map.merge(key, 1, Integer::sum); — this does the same thing as the getOrDefault pattern in one method call. It inserts 1 if the key is absent, or adds 1 to the existing value using Integer::sum. Both approaches are correct; use whichever feels more readable to you.

SECTION 5

Detailed Breakdown — Grouping in Action

Grouping is one of the most versatile patterns you'll encounter. A classic example is the group anagrams problem: given a list of words, group together all words that are anagrams of each other (contain the same letters rearranged). The trick is figuring out a good key. If you sort each word's characters alphabetically, all anagrams produce the same sorted string. That sorted string becomes the HashMap key, and the original words are collected in the list value.

Grouping Anagrams with HashMapINPUT WORDSeatteatanatenatbatSTEP: Sort each word's characters → use as KEYaetaetantaetantabtRESULTING HASHMAPKEYVALUE (List)"aet"→eatteaate"ant"→tannat"abt"→bat
Each input word is sorted alphabetically to create a canonical key. Words like "eat", "tea", and "ate" all sort to "aet", so they land in the same group. The HashMap's final state shows three groups, one per unique sorted key.

The grouping pattern is incredibly flexible. You can group students by grade level, group transactions by date, group files by extension, or group numbers by their remainder when divided by some value. The only thing that changes is how you compute the key. Once you pick the right key, the HashMap does the rest.

Grouping ScenarioKeyValue (List elements)
Group anagramsSorted characters of each wordOriginal words
Group words by first letterFirst character of each wordFull words
Group numbers by even/oddnum % 2 (0 or 1)The numbers themselves
Group students by gradeGrade level (9, 10, 11, 12)Student names

Common grouping scenarios and their key-value choices

SECTION 6

Worked Example — Finding the Most Frequent Element

Let's solve a classic problem step by step: given an array of integers, find the element that appears the most often. If there is a tie, return any one of the most frequent elements.

Most Frequent Element in an Array

  1. 1

    Step 1 — Understand the Input

    We are given the array int[] nums = {1, 3, 2, 1, 4, 1, 3, 3, 3};. We need to determine which number appears the most.
    Input: [1, 3, 2, 1, 4, 1, 3, 3, 3]
  2. 2

    Step 2 — Build the Frequency Map

    Create a HashMap<Integer, Integer> and loop through the array. For each element, apply the counting idiom: freq.put(num, freq.getOrDefault(num, 0) + 1);. After processing the entire array, the map contains: {1 → 3, 3 → 4, 2 → 1, 4 → 1}.
    freq = {1 → 3, 3 → 4, 2 → 1, 4 → 1}
  3. 3

    Step 3 — Find the Maximum Count

    Now iterate over the entries in the map using freq.entrySet(). Track two variables: maxCount (initially 0) and result (the key with the highest count). For each entry, if the value exceeds maxCount, update both variables.
    maxCount = 4, result = 3
  4. 4

    Step 4 — Write the Complete Code

    Putting it all together: HashMap<Integer, Integer> freq = new HashMap<>(); for (int num : nums) { freq.put(num, freq.getOrDefault(num, 0) + 1); } int maxCount = 0, result = nums[0]; for (Map.Entry<Integer, Integer> e : freq.entrySet()) { if (e.getValue() > maxCount) { maxCount = e.getValue(); result = e.getKey(); } } return result;
    Output: 3 (appears 4 times)
  5. 5

    Step 5 — Analyze Time & Space Complexity

    The first loop runs n times with O(1) per iteration, giving O(n). The second loop runs at most n times (once per unique key). Total time complexity is O(n). The HashMap stores at most n entries, so space complexity is also O(n). This is dramatically faster than a brute-force O(n²) approach that counts occurrences with nested loops.
    Time: O(n) | Space: O(n)

SECTION 7

HashMap vs. Other Approaches

HashMap is not the only way to count or group elements, but it is usually the best choice for general-purpose problems. Let's compare it with other common approaches so you understand when to reach for each tool.

ApproachTime ComplexityStrengthsLimitations
HashMapO(n) averageWorks for any key type; fast insertions and lookups; flexibleUses extra memory; unordered output
Sorting + ScanO(n log n)No extra memory (in-place sort); produces ordered resultsSlower; mutates original array; only works with comparable types
int[] (frequency array)O(n)Very fast; minimal overhead; great for small fixed rangesOnly works when keys are small non-negative integers (e.g., ASCII)
TreeMapO(n log n)Maintains sorted key order; supports range queriesSlower than HashMap; keys must be Comparable
Nested Loops (brute force)O(n²)No extra data structures; very simple codeFar too slow for large inputs; impractical

Comparison of counting/grouping approaches

⚡

WHEN TO CHOOSE WHAT

Use a HashMap as your default for counting and grouping problems — it handles virtually any key type and runs in O(n). Switch to a frequency array (e.g., int[26] for lowercase letters) when keys are limited integers, since it avoids the overhead of hashing. Use a TreeMap only when you specifically need keys in sorted order.

SECTION 8

Connection to Advanced Patterns

The counting and grouping patterns you've learned are building blocks for much more powerful techniques. Many advanced coding problems combine a HashMap with other strategies. Understanding how these patterns scale up will prepare you for AP Computer Science, competitive programming, and technical interviews.

Basic Pattern (This Lesson)Advanced ExtensionExample Problem
Frequency count of an entire arraySliding Window + HashMap — maintain a frequency map of only the elements in a moving windowFind the longest substring with at most K distinct characters
Grouping items by a propertyBucket Sort — group by frequency, then read buckets in order to find top-K elementsTop K Frequent Elements
Counting single valuesTwo-Sum / Complement Lookup — store seen values in a HashMap and check for complementsTwo Sum: find two numbers that add to a target
Frequency comparisonAnagram Detection / Permutation Check — compare two frequency maps to determine if strings are anagramsValid Anagram, Find All Anagrams in a String

How basic HashMap patterns connect to advanced problems

If you're headed toward AP Computer Science A, know that HashMap is the Swiss army knife of data structures for most algorithmic problems. Nearly every interview-style question about arrays or strings can benefit from a HashMap at some stage. Once you internalize the counting and grouping idioms from this lesson, the advanced patterns become natural extensions — you just combine the HashMap with a second technique like a sliding window, priority queue, or graph traversal.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why using a HashMap<String, Integer> to count word occurrences in a document is faster than using a nested loop approach. What is the time complexity of each approach, and why does the difference matter for large documents?
Show answer
With a HashMap, you make a single pass through the document (O(n)), and each lookup/update in the map takes O(1) on average thanks to hashing. With nested loops, for every word you would scan the entire document again to count its occurrences, resulting in O(n²) time. For a document with 100,000 words, the HashMap approach does roughly 100,000 operations, while the nested-loop approach could do up to 10 billion — making the HashMap approach thousands of times faster. This difference is the key reason HashMap is the standard tool for frequency counting.

Question 2: BASIC CALCULATION

Given the array ["red", "blue", "red", "green", "blue", "red"], trace through the counting pattern step by step. Write out what the HashMap contains after processing each element. What is the final frequency map?
Show answer
After "red": {red → 1}. After "blue": {red → 1, blue → 1}. After "red": {red → 2, blue → 1}. After "green": {red → 2, blue → 1, green → 1}. After "blue": {red → 2, blue → 2, green → 1}. After "red": {red → 3, blue → 2, green → 1}. The final frequency map is {red → 3, blue → 2, green → 1}. Each step uses getOrDefault(key, 0) + 1 to handle both new keys and existing keys correctly.

Question 3: INTERMEDIATE

Write a Java method firstUnique(String s) that returns the first character in a string that appears exactly once. If no such character exists, return a space character ' '. Use the HashMap counting pattern. For example, firstUnique("aabbcdde") should return 'c'.
Show answer
First, build a frequency map by looping through the string: HashMap<Character, Integer> freq = new HashMap<>(); for (char c : s.toCharArray()) { freq.put(c, freq.getOrDefault(c, 0) + 1); }. Then, loop through the string a second time (to preserve order) and return the first character whose count equals 1: for (char c : s.toCharArray()) { if (freq.get(c) == 1) return c; } return ' ';. The two-pass approach is key — the first pass counts, and the second pass checks in original order. Both loops are O(n), so the total complexity is O(n).

Question 4: APPLIED

You're building a classroom tool. Given an array of student objects where each student has a grade (int, 9–12) and a name (String), write Java code using the grouping pattern to produce a HashMap<Integer, List<String>> that maps each grade level to the list of student names in that grade. Then describe how you would find which grade has the most students.
Show answer
Build the grouping map: HashMap<Integer, List<String>> gradeMap = new HashMap<>(); for (Student s : students) { gradeMap.computeIfAbsent(s.grade, k -> new ArrayList<>()).add(s.name); }. To find the grade with the most students, iterate over the map's entry set and track the entry with the largest list size: int maxGrade = -1, maxSize = 0; for (Map.Entry<Integer, List<String>> e : gradeMap.entrySet()) { if (e.getValue().size() > maxSize) { maxSize = e.getValue().size(); maxGrade = e.getKey(); } }. This combines the grouping pattern with a max-search, demonstrating how HashMap patterns compose naturally. Total time is O(n) for grouping plus O(k) for the search, where k is the number of unique grades.

Question 5: CRITICAL THINKING

Consider the problem: given two strings s and t, determine whether t is an anagram of s (same characters, same frequencies, possibly different order). Describe two different HashMap-based approaches and compare their trade-offs. Then explain under what conditions a simple int[26] array would be more efficient than a HashMap.
Show answer
Approach 1 (Two Maps): Build a frequency map for each string, then compare the two maps using map1.equals(map2). This is clean but uses extra memory for two maps. Approach 2 (Single Map): Build a frequency map for s (increment counts), then loop through t and decrement counts. If any count goes negative or if any count is non-zero at the end, they're not anagrams. This uses less memory. The int[26] array approach works when both strings contain only lowercase English letters, because 'a'–'z' map directly to indices 0–25. It avoids hashing overhead and is cache-friendly, making it both faster and more memory-efficient in that restricted scenario. However, it fails for Unicode strings, mixed-case text, or arbitrary character sets where a HashMap remains necessary.

SUMMARY

Lesson Summary

Summary

A HashMap stores key-value pairs with O(1) average-time lookups, making it the go-to structure for two essential problem-solving patterns. The counting pattern uses map.put(key, map.getOrDefault(key, 0) + 1) to build a frequency map in a single pass through the data. The grouping pattern uses map.computeIfAbsent(key, k -> new ArrayList<>()).add(item) to collect items into categorized lists based on a shared property.

These two patterns unlock efficient solutions to a wide range of problems: finding the most frequent element, detecting anagrams, identifying unique items, and organizing data into meaningful categories. Remember that a frequency array (like int[26]) can be faster when keys are limited integers, while a TreeMap is the right choice when you need keys in sorted order. Master these idioms, and you'll have a powerful toolkit for tackling array and string problems efficiently.

Varsity Tutors • Java • HashMap for Counting & Grouping