C++ • Problem-Solving Patterns

unordered_map for Counting — Use unordered_map for counting/frequency and grouping (intro)

Master the hash-based frequency pattern that solves countless interview and systems problems in O(n) time.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 9 figures

SECTION 1

Historical Context & Motivation

Counting the frequency of elements in a collection is one of the oldest and most fundamental operations in computing. Long before hash tables existed in standard libraries, programmers resorted to sorted arrays and binary search trees to tally occurrences—approaches that imposed an O(n log n) lower bound on what should be a conceptually simple task. The desire for a constant-time lookup structure that could map arbitrary keys to integer counts drove decades of research into hashing algorithms, collision resolution strategies, and eventually the standardization of hash-based containers in mainstream languages.

In C++, the journey toward a standard hash map was notably long. The original STL shipped with std::map, a red-black tree offering O(log n) operations and guaranteed ordering. Several compiler vendors provided non-standard hash_map extensions throughout the 1990s and 2000s, but portability remained an issue. It was not until C++11 that the language formally introduced std::unordered_map, giving every C++ programmer access to an amortized O(1) associative container backed by a hash table.

1953 • Hash Table Concept

Hans Peter Luhn at IBM files an internal memo describing chained hashing, establishing the theoretical foundation for O(1) average-case lookup in associative data structures.

1998 • C++98 & std::map

The first ISO C++ standard ships with std::map (a balanced BST) but no hash-based container, leaving vendors to offer proprietary hash_map extensions.

2005 • TR1 Proposal

Technical Report 1 proposes std::tr1::unordered_map, unifying the fragmented ecosystem of vendor-specific hash maps under one interface.

2011 • C++11 Standardization

std::unordered_map becomes part of the official standard, providing amortized O(1) insertion, deletion, and lookup backed by a hash table with separate chaining.

2020s • Modern Interview & Systems Usage

The frequency-counting pattern with unordered_map becomes a staple of competitive programming, technical interviews, and production systems—from log analysis to natural language processing.

The central question this lesson addresses is deceptively simple: given a sequence of elements, how do you efficiently count how many times each distinct element appears, and how do you group elements by some shared property? The answer—a single pass over the data, inserting or incrementing entries in an unordered_map—is a pattern so pervasive that internalizing it unlocks solutions to dozens of seemingly unrelated problems.

SECTION 2

Core Principles & Definitions

Before diving into code, it is essential to understand the foundational ideas that make the counting pattern with unordered_map both correct and efficient. The pattern rests on the interplay between hashing, default-value semantics, and the distinction between frequency counting and grouping—two closely related but subtly different operations.

1

Hash-Based Amortized O(1) Access

std::unordered_map uses a hash function to map keys to buckets. On average, operator[], find(), and insert() all run in O(1) amortized time, degenerating to O(n) only under pathological hash collisions.
2

Value-Initialization via operator[]

When you write map[key]++, if key does not exist, operator[] value-initializes the mapped type (0 for int) before the increment. This eliminates the need for explicit existence checks.
3

Frequency Counting

Map each element to the number of times it appears. The map type is typically unordered_map<T, int>. A single pass through the input yields a complete frequency table.
4

Grouping / Bucketing

Map a derived key (e.g., first letter, modulo class) to a collection of elements sharing that key. The map type is unordered_map<K, vector<T>>. Grouping generalizes frequency counting: instead of incrementing a count, you append to a vector.
5

Single-Pass Linear Scan

Both patterns share the same algorithmic skeleton: iterate once over the input (O(n)), and for each element perform an O(1) map operation. The total work is O(n) on average, which is optimal because every element must be inspected at least once.
✦

KEY TAKEAWAY

Think of an unordered_map as a bank of labeled mailboxes. Each unique key gets its own mailbox. For frequency counting, you drop a tally mark into the box each time you see that key. For grouping, you drop the actual item into the box. The hash function is the postal code that routes each item to its box in constant time—no need to search through every box.

SECTION 3

Visual Explanation — The Counting Pipeline

The following diagram illustrates the complete lifecycle of the frequency-counting pattern. An input array is scanned left to right; each element is hashed and routed to its bucket in the unordered_map. The resulting frequency table is then available for downstream queries such as finding the most common element or detecting duplicates.

Frequency Counting PipelineInput Arrayabacbafor eachHash Functionhash(key) → bucketrouteunordered_map<char, int>'a'→3'b'→2'c'→1queryDownstream Queriesmax freq · duplicates · top-k · anagram checkStep-by-step tracei=0 elem='a' map['a']++ → {a:1}i=1 elem='b' map['b']++ → {a:1,b:1}i=2 elem='a' map['a']++ → {a:2,b:1}i=3 elem='c' map['c']++ → {a:2,b:1,c:1}i=4 elem='b' map['b']++ → {a:2,b:2,c:1}i=5 elem='a' map['a']++ → {a:3,b:2,c:1}Final: {'a':3, 'b':2, 'c':1}Total work: O(n) average · O(n) space for k distinct keys ≤ n
The left panel traces each iteration: map[elem]++ either inserts a new key (value-initialized to 0, then incremented to 1) or increments an existing count. The right panel shows the hash function routing keys to buckets, producing the final frequency table used for downstream queries.

Notice how the pattern requires no sorting, no binary search, and no pre-allocation of a fixed-size array. The unordered_map grows dynamically as new keys appear, and the hash function distributes entries across buckets so that each lookup or insertion takes amortized constant time. The entire pipeline—scan, hash, increment, query—is both conceptually clean and computationally optimal for the general case.

SECTION 4

How It Works — Mechanics & Complexity

Understanding the precise mechanics of operator[] is critical to writing idiomatic frequency-counting code. When you invoke freq[key] on an unordered_map<Key, Value>, the container first computes hash(key) to identify the target bucket. If key is not already present, a new entry is default-constructed with value-initialization—for integral types this means zero. The operator then returns a reference to the value, enabling in-place mutation such as ++ or += 1.

The Canonical Counting Idiom

The idiomatic one-liner for frequency counting in C++ is:

COUNTING IDIOM

for (auto& x : data) freq[x]++;

data — input container (vector, string, etc.). freq — unordered_map<T, int>. x — current element, bound by reference to avoid copies.

The Grouping Idiom

GROUPING IDIOM

for (auto& x : data) groups[keyFn(x)].push_back(x);

groups — unordered_map<K, vector<T>>. keyFn(x) — a function that extracts the grouping key from element x. If the key does not exist, operator[] default-constructs an empty vector<T> before push_back.

Complexity Analysis

TIME COMPLEXITY

T(n)=n×O(1)amortized=O(n)averagecaseT(n) = n × O(1)_amortized = O(n) average case

n = number of elements in input. Each operator[] call is O(1) amortized under a good hash. Worst case (all keys collide into one bucket) degrades to O(n²).

SPACE COMPLEXITY

S(n)=O(k)wherek=numberofdistinctkeys,k≤nS(n) = O(k) where k = number of distinct keys, k ≤ n

In the worst case (all elements distinct), k = n and space is O(n). In practice k is often much smaller, e.g., 26 for lowercase English letters.

💡

When to use find() vs operator[]

Use operator[] when you want auto-insertion (counting, grouping). Use find() or count() when you want to check existence without creating a new entry—for instance, when querying a prebuilt frequency table.

SECTION 5

Common Counting & Grouping Patterns

The basic counting idiom appears in many disguises across algorithm design. The following diagram classifies the most common sub-patterns, showing how a single unordered_map scan can be adapted for frequency queries, duplicate detection, anagram testing, and grouping. Recognizing which sub-pattern applies to a given problem is the key skill this lesson develops.

Counting & Grouping Pattern Taxonomyunordered_map PatternFrequency CountingGrouping / BucketingComparison / MatchingMost Frequent Elementmap[x]++then max over valuesO(n) timeDuplicate Detectionif (map[x]++ > 0) → duplicate foundO(n) timeGroup by Propertygroups[key(x)] .push_back(x)O(n) timeAnagram Groupskey = sorted(word)groups[key].push_backO(n·k log k)Two-Map ComparisonfreqA == freqB→ anagram / permutationO(n + m) timeAll sub-patterns share the same skeleton: single scan + O(1) map operation per element
The taxonomy divides unordered_map usage into three families: frequency counting (most-frequent, duplicate detection), grouping (by property, anagram groups), and comparison (two-map equality for permutation checks). Each sub-pattern modifies only the inner loop body.
Sub-PatternMap TypeInner Loop BodyClassic Problem
Frequency Countunordered_map<T, int>freq[x]++Majority Element, Top-K Frequent
Duplicate Detectionunordered_map<T, int>if (freq[x]++ > 0) ...Contains Duplicate (LeetCode 217)
Group by Keyunordered_map<K, vector<T>>groups[keyFn(x)].push_back(x)Group Anagrams (LeetCode 49)
Two-Map CompareTwo separate freq mapsfreqA == freqBValid Anagram (LeetCode 242)
Sliding Window Frequnordered_map<T, int>freq[in]++; freq[out]--Minimum Window Substring

Common sub-patterns built on unordered_map

SECTION 6

Worked Example — Top-K Frequent Elements

Given an integer vector nums = {1, 1, 1, 2, 2, 3} and k = 2, return the two most frequent elements. This is LeetCode 347 and is a canonical application of the counting pattern. We will solve it in two phases: build a frequency map, then extract the top-k keys.

Top-K Frequent Elements

  1. 1

    Step 1 — Build the Frequency Map

    Declare unordered_map<int, int> freq; and iterate over nums, incrementing the count for each element. After the loop, freq holds {1:3, 2:2, 3:1}.
    freq = {1→3, 2→2, 3→1}
  2. 2

    Step 2 — Create Bucket Array (Bucket Sort by Frequency)

    Create a vector of vectors vector<vector<int>> buckets(n + 1); where n = nums.size(). For each entry (val, count) in freq, place val into buckets[count]. This inverts the map: bucket index = frequency.
    buckets[1]={3}, buckets[2]={2}, buckets[3]={1}
  3. 3

    Step 3 — Collect Top-K from Buckets

    Iterate buckets from index n down to 0, appending elements to the result vector until its size reaches k. From bucket 3 we get {1}, from bucket 2 we get {2}. Result size is now 2 = k, so we stop.
    Result: {1, 2}
  4. 4

    Step 4 — Analyze Complexity

    Building the frequency map is O(n). Creating and filling the bucket array is O(n) (iterating over at most n entries in the map). Collecting top-k elements scans at most n buckets. Total time: O(n). Space: O(n) for the map and the bucket array. This is strictly better than the O(n log k) heap-based approach when k is not negligibly small.
    O(n) time, O(n) space
ℹ

Full C++ Code

vector<int> topKFrequent(vector<int>& nums, int k) { unordered_map<int, int> freq; for (int x : nums) freq[x]++; int n = nums.size(); vector<vector<int>> buckets(n + 1); for (auto& [val, cnt] : freq) buckets[cnt].push_back(val); vector<int> result; for (int i = n; i >= 0 && (int)result.size() < k; --i) for (int v : buckets[i]) result.push_back(v); return result; }

SECTION 7

Strengths, Limitations & Comparisons

While unordered_map is the go-to container for frequency counting, it is not universally optimal. Understanding when to use it—and when alternatives are preferable—separates competent from expert C++ problem solvers. The table below compares unordered_map against the most common alternatives for counting tasks.

ApproachTime per OpOrdered?Best WhenWeakness
unordered_map<T,int>O(1) amortizedNoGeneral-purpose; key type is hashableCache-unfriendly; worst-case O(n) per op
map<T,int>O(log n)YesNeed sorted output; key type has no hashSlower constant factor; tree overhead
int arr[SIZE]O(1) trueImplicitlyKeys are small integers (chars, indices)Fixed size; impractical for large/sparse keys
Sort + linear scanO(n log n) totalYesAlready sorted data; in-place constraintMutates input; O(n log n) baseline

Counting approaches compared

⚙

CHOOSE THE RIGHT TOOL

When your keys are lowercase English letters, a plain int freq[26] = {}; array outperforms any map due to zero hashing overhead and perfect cache locality. Reserve unordered_map for when the key domain is large, sparse, or non-integral—strings, pairs, composite keys, or arbitrary integers. Think of a fixed array as a row of numbered lockers and an unordered_map as a storage warehouse with a lookup desk: the lockers are faster if your items fit, but the warehouse scales to anything.
⚠

Common Pitfall: Hash Collisions

In competitive programming, adversarial test cases can trigger worst-case O(n²) behavior in unordered_map by crafting inputs whose keys all hash to the same bucket. A common defense is to use a custom hash function that XORs the key with a random seed at program startup, making collision attacks infeasible. Libraries like Codeforces' custom_hash struct are widely used for this purpose.

SECTION 8

Connection to Advanced Techniques

The simple counting pattern introduced here is the foundation for several advanced algorithmic techniques. As you progress, you will encounter scenarios where the basic pattern is extended with sliding windows, multi-map coordination, or probabilistic data structures. Understanding the baseline pattern makes these extensions natural rather than mysterious.

This Lesson (Intro)Advanced Extension
Global frequency map over entire arraySliding window frequency map — maintain counts in a window of size w, incrementing on entry and decrementing on exit. Used in minimum window substring, permutation in string, etc.
Single unordered_map for one sequenceTwo-map intersection/difference — build frequency maps for two sequences and iterate over one to compute intersections, unions, or differences. Foundation for Jaccard similarity and bag-of-words models.
Exact counts stored in mapApproximate counting — Count-Min Sketch and HyperLogLog trade exactness for sub-linear space, useful in streaming and big data contexts where the key domain is enormous.
Flat grouping by one key functionMulti-level grouping — nested maps like unordered_map<K1, unordered_map<K2, vector<T>>> for hierarchical classification (e.g., group by department, then by role).
Standard library unordered_mapHigh-performance alternatives — flat hash maps (Abseil's absl::flat_hash_map, Robin Hood hashing) provide better cache locality and lower memory overhead for performance-critical applications.

From intro pattern to advanced technique

The key insight is that mastering the basic scan-hash-increment skeleton gives you a template that extends naturally. The sliding window variant, for instance, simply adds a freq[out]-- step for the element leaving the window. Probabilistic sketches replace the exact map with a compact bit array but retain the same conceptual pipeline. Each advanced technique is a variation on the theme, not a fundamentally new idea.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why freq[x]++ works correctly even when key x does not yet exist in the map. What specific language feature makes this safe? Would the same one-liner work with std::map?
Show answer
When operator[] is called with a key not present in the map, both std::unordered_map and std::map insert a new entry whose value is value-initialized. For int, value-initialization produces 0. The operator returns a reference to this newly created 0 value, and the post-increment ++ then sets it to 1. Yes, the same idiom works identically with std::map because both containers define operator[] with the same insertion-on-absence semantics. The difference is that map performs this in O(log n) while unordered_map does it in O(1) amortized.

Question 2: BASIC CALCULATION

Given s = "abracadabra", trace the construction of a frequency map unordered_map<char, int>. Write out the final state of the map and identify the character with the highest frequency.
Show answer
Processing each character: a appears at indices 0, 3, 5, 7, 10 (count 5); b at 1, 8 (count 2); r at 2, 9 (count 2); c at 4 (count 1); d at 6 (count 1). The final map is {a:5, b:2, r:2, c:1, d:1}. The character with the highest frequency is 'a' with 5 occurrences. Verifying: 5 + 2 + 2 + 1 + 1 = 11 = length of the string, confirming correctness.

Question 3: INTERMEDIATE

Write a C++ function bool isAnagram(string s, string t) that returns true if t is an anagram of s using the frequency map pattern. Your solution should run in O(n) time and O(1) space (since the character set is bounded). Explain why a single map suffices instead of two.
Show answer
A single map suffices because we can increment for characters in s and decrement for characters in t. If s and t are anagrams, every count returns to zero. Implementation: bool isAnagram(string s, string t) { if (s.size() != t.size()) return false; unordered_map<char,int> freq; for (char c : s) freq[c]++; for (char c : t) if (--freq[c] < 0) return false; return true; } The early return on negative count detects a character in t that exceeds its frequency in s. For lowercase-only input, an int freq[26]={} array would be even more efficient.

Question 4: APPLIED

A web server logs each request's HTTP status code (200, 301, 404, 500, etc.) to a file. You are given a vector<int> logs containing millions of entries. Design a solution that (a) counts the frequency of each status code, and (b) groups the indices of all 5xx errors for further analysis. Provide C++ code and analyze time/space complexity.
Show answer
We use two maps in a single pass: unordered_map<int,int> freq; unordered_map<int,vector<int>> errors; for (int i = 0; i < (int)logs.size(); ++i) { freq[logs[i]]++; if (logs[i] >= 500 && logs[i] < 600) errors[logs[i]].push_back(i); } Time complexity is O(n) because each iteration performs O(1) map operations. Space for freq is O(k) where k is the number of distinct status codes (typically < 50). Space for errors is O(e) where e is the number of 5xx entries. In production, this is a standard log-analysis pipeline; the grouping map lets you quickly retrieve all indices for, say, status 503 to correlate with deployment timestamps.

Question 5: CRITICAL THINKING

Consider the problem of finding the length of the longest substring without repeating characters (LeetCode 3). Explain how the frequency-map pattern adapts to a sliding window context. Specifically, what invariant must the map maintain, and what is the procedure when a duplicate is detected? Analyze why this approach is O(n) despite the window potentially shrinking many times.
Show answer
The invariant is that every value in the frequency map is at most 1, ensuring all characters in the current window [left, right] are unique. We use unordered_map<char,int> freq and expand right one step at a time. When freq[s[right]] exceeds 1 after incrementing, we shrink from the left: freq[s[left]]--; left++ until the invariant is restored. Although the inner while loop may execute multiple times per outer iteration, the total number of left-pointer advances across the entire algorithm is at most n, because left only moves forward and never exceeds n. By amortized analysis, the total work for all shrink operations is O(n), and the expand operations contribute O(n), yielding O(n) overall. This demonstrates how the basic counting map pattern extends to the sliding window paradigm by adding a decrement step on window exit.

SUMMARY

Lesson Summary

Summary

The unordered_map counting pattern is one of the most versatile tools in a C++ programmer's problem-solving toolkit. At its core, it exploits amortized O(1) hash-table access and the value-initialization semantics of operator[] to build a complete frequency table in a single O(n) pass. The canonical idiom— for (auto& x : data) freq[x]++; —requires no existence checks, no pre-allocation, and no sorting.

This lesson covered two primary applications: frequency counting (mapping elements to their occurrence counts) and grouping (mapping derived keys to collections of elements). We examined sub-patterns including duplicate detection, two-map comparison for anagram checking, and bucket-sort extraction for top-k queries. When keys belong to a small, dense domain (like ASCII characters), a fixed-size array outperforms any map. For large, sparse, or composite key types, unordered_map is the right choice, and awareness of hash-collision risks ensures robust production code.

Varsity Tutors • C++ • unordered_map for Counting