Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 17

Hash Maps for O(1) Frequency Counting

Recommended resources

Hello! Welcome to your next lesson in the "Hashing for Efficient Lookups" module.

In the previous lesson, we demystified hash tables, exploring how hash functions and collision resolution strategies work together to provide the magical average time complexity for lookups, insertions, and deletions. We established the theory behind their speed.

Today, we transition from theory to application. We'll take that lookup power and use it to solve a huge category of interview problems. Your learning outcome is to apply hash maps for average time complexity lookups in frequency counting problems. This is one of the most common and high-return patterns you'll encounter in technical interviews.

1. The Frequency Counting Pattern

At its core, frequency counting is exactly what it sounds like: counting the occurrences of elements in a collection. This simple idea is incredibly powerful. Many complex-sounding problems, which might naively suggest an solution (like comparing every element to every other element), can be simplified to a linear solution by first counting frequencies.

The key insight is that for problems where the order of elements doesn't matter, their frequency distribution is often all the information you need.

Frequency Counting Introduction

The article "Frequency Counting Introduction" from AlgoMaster provides an excellent overview of this pattern. It explains what it is, why it's effective, and when you should think of using it.

Read the sections "Introduction to Frequency Counting" and "Why Frequency Counting Works". Focus on how this pattern transforms expensive comparisons into simple count comparisons.

The data structure that enables this efficiency is, of course, the hash map. By using a hash map, we can iterate through a collection of items and build a complete frequency count in time, because each of the updates to the map is an average time operation. This is a classic example of a time-space tradeoff: we use extra space (the hash map) to achieve a much faster runtime.


2. The Core Implementation: Building a Frequency Map

The fundamental pattern for building a frequency map (or "frequency counter") is a loop and a hash map. Let's see how this looks in practice.

Data Structure and Algorithm Patterns for LeetCode Interviews – Tutorial

This clip from freeCodeCamp.org's tutorial directly introduces the 'frequency map' pattern and shows the basic code structure you'll use repeatedly.

Watch the segment from 13:56 to 15:00. Pay close attention to the logic: if an item isn't in the map, initialize its count; otherwise, increment it.

Given your proficiency in Python, you have several ways to implement this, each with its own level of elegance. Let's consider an example: counting character frequencies in the string "hello world".

Method 1: The Basic if/else
This is the most explicit implementation, mirroring the logic from the video.

s = "hello world"
freq_map = {}
for char in s:
    if char in freq_map:
        freq_map[char] += 1
    else:
        freq_map[char] = 1
# Result: {'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}

Method 2: Using dict.get()
A slightly more concise way is to use the get() method with a default value. freq_map.get(char, 0) will return the current count of char if it exists, or 0 if it doesn't.

s = "hello world"
freq_map = {}
for char in s:
    freq_map[char] = freq_map.get(char, 0) + 1

Method 3: Using collections.defaultdict
This is the most idiomatic and efficient Python approach for this pattern. You initialize a defaultdict with a factory function (like int, which defaults to 0). When you try to access a key that doesn't exist, it's automatically created with the default value, eliminating the need for checks.

from collections import defaultdict

s = "hello world"
freq_map = defaultdict(int)
for char in s:
    freq_map[char] += 1

Using defaultdict makes your code cleaner and avoids the common KeyError.

HashMaps in Python Tutorial - Data Structures for Coding Interviews

The video "HashMaps in Python Tutorial" by Codebagel has a great segment on this exact topic, highlighting the common KeyError and how DefaultDict solves it.

Watch from 04:07 to 05:49. This will solidify your understanding of the practical implementation in Python and why DefaultDict is so useful.


3. Application: Solving "Valid Anagram"

Now let's apply this pattern to solve a classic interview problem: Valid Anagram.

Problem Statement: Given two strings, s and t, return true if t is an anagram of s, and false otherwise. An anagram is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.

  • Input: s = "anagram", t = "nagaram"

  • Output: true

  • Input: s = "rat", t = "car"

  • Output: false

How can we solve this using frequency counting? The definition of an anagram gives us the answer: two strings are anagrams if and only if they have identical character frequencies.

So, the algorithm is:

  1. Check if the strings have different lengths. If so, they can't be anagrams.
  2. Create a frequency map for the first string, s.
  3. Iterate through the second string, t, and decrement the count for each character in the map.
  4. If at any point a character from t is not in the map, or its count is already zero, they are not anagrams.
  5. After iterating through t, if all counts in the map are zero, they are anagrams.

A more direct comparison is even simpler: build a frequency map for s and another for t, and then simply check if the two maps are equal.

DSA was HARD until I Learned these 20 Patterns

This pattern is so common that it's featured in many DSA guides. The AlgoMaster blog provides a concise walkthrough for the 'Valid Anagram' problem using this exact pattern.

Find pattern #6, "Frequency Counting". Review the template and then read the Step-by-Step Walkthrough for the "Valid Anagram" problem.

Here's how you might implement the two-map comparison in Python, using defaultdict for clean code:

from collections import defaultdict

def is_anagram(s: str, t: str) -> bool:
    if len(s) != len(t):
        return False

    s_counts = defaultdict(int)
    for char in s:
        s_counts[char] += 1

    t_counts = defaultdict(int)
    for char in t:
        t_counts[char] += 1
    
    return s_counts == t_counts

# --- Analysis ---
# Time Complexity: O(n), where n is the length of the strings. 
# We iterate through each string once to build the maps (O(n) + O(n)). 
# Comparing the maps also takes O(k) time, where k is the number of unique characters (at most 26 for lowercase English, so effectively O(1)).
#
# Space Complexity: O(k), where k is the number of unique characters.
# We store at most k unique characters in our hash maps. For a fixed alphabet (like ASCII or lowercase English), this can be considered O(1) space.

Another common and clever implementation in Python uses collections.Counter, which is a specialized dictionary subclass for counting hashable objects.

from collections import Counter

def is_anagram_counter(s: str, t: str) -> bool:
    return Counter(s) == Counter(t)

This single line does exactly what our manual defaultdict implementation did. Knowing about Counter is a great sign of Python fluency in an interview.

Test your understanding!

You are given an array of strings strs. Write a function to group the anagrams together. You can return the answer in any order.

Example:
Input: strs = ["eat","tea","tan","ate","nat","bat"]
Output: [["bat"],["nat","tan"],["ate","eat","tea"]]

Hint: What property do all anagrams share? How could you create a unique "key" for each anagram group using this property? Could a hash map help you group strings by this key?

Show answer

A canonical representation for an anagram group is the sorted version of the string. For example, "eat", "tea", and "ate" all become "aet" when sorted. We can use this sorted string as the key in a hash map.

from collections import defaultdict

def group_anagrams(strs: list[str]) -> list[list[str]]:
    anagram_groups = defaultdict(list)
    
    for s in strs:
        # The sorted tuple of characters serves as a hashable key
        key = tuple(sorted(s))
        anagram_groups[key].append(s)
        
    return list(anagram_groups.values())

# Example Walkthrough:
# 1. s = "eat" -> key = ('a', 'e', 't') -> anagram_groups[('a', 'e', 't')] = ["eat"]
# 2. s = "tea" -> key = ('a', 'e', 't') -> anagram_groups[('a', 'e', 't')] = ["eat", "tea"]
# 3. s = "tan" -> key = ('a', 'n', 't') -> anagram_groups[('a', 'n', 't')] = ["tan"]
# 4. s = "ate" -> key = ('a', 'e', 't') -> anagram_groups[('a', 'e', 't')] = ["eat", "tea", "ate"]
# ... and so on.

This problem demonstrates a powerful extension of frequency counting: using a canonical representation (like a sorted string or a character-count tuple) as a key in a hash map to group similar items.


Conclusion

You've now learned one of the most fundamental and frequently applied patterns in coding interviews. By leveraging the average lookup time of hash maps, you can solve a wide range of problems in linear time that would otherwise be much slower.

Key Takeaways:

  • Frequency Counting Pattern: A technique to solve problems by counting element occurrences, especially useful when element order is irrelevant.
  • The Right Tool: Hash maps (or Python's dict, defaultdict, Counter) are the ideal data structure for this pattern, enabling you to build a frequency map in time.
  • Problem Recognition: Look for keywords like "anagram," "duplicate," "unique," or problems comparing the composition of two collections. This is your cue to consider frequency counting.
  • Complexity: The pattern typically results in an time complexity and space complexity, where is the number of elements and is the number of unique elements.

In our next lesson, we will continue exploring the power of hash maps by tackling another iconic interview problem: Two Sum. You'll see how a similar "have I seen this before?" logic can be used to find pairs of elements that satisfy a certain condition.