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
1998 • C++98 & std::map
std::map (a balanced BST) but no hash-based container, leaving vendors to offer proprietary hash_map extensions.2005 • TR1 Proposal
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
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.
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.Value-Initialization via operator[]
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.Frequency Counting
unordered_map<T, int>. A single pass through the input yields a complete frequency table.Grouping / Bucketing
unordered_map<K, vector<T>>. Grouping generalizes frequency counting: instead of incrementing a count, you append to a vector.Single-Pass Linear Scan
KEY TAKEAWAY
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.
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
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
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
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
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[]
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.
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-Pattern | Map Type | Inner Loop Body | Classic Problem |
|---|---|---|---|
| Frequency Count | unordered_map<T, int> | freq[x]++ | Majority Element, Top-K Frequent |
| Duplicate Detection | unordered_map<T, int> | if (freq[x]++ > 0) ... | Contains Duplicate (LeetCode 217) |
| Group by Key | unordered_map<K, vector<T>> | groups[keyFn(x)].push_back(x) | Group Anagrams (LeetCode 49) |
| Two-Map Compare | Two separate freq maps | freqA == freqB | Valid Anagram (LeetCode 242) |
| Sliding Window Freq | unordered_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
Step 1 — Build the Frequency Map
Declareunordered_map<int, int> freq;and iterate overnums, incrementing the count for each element. After the loop,freqholds{1:3, 2:2, 3:1}.freq = {1→3, 2→2, 3→1} - 2
Step 2 — Create Bucket Array (Bucket Sort by Frequency)
Create a vector of vectorsvector<vector<int>> buckets(n + 1);wheren = nums.size(). For each entry(val, count)infreq, placevalintobuckets[count]. This inverts the map: bucket index = frequency.buckets[1]={3}, buckets[2]={2}, buckets[3]={1} - 3
Step 3 — Collect Top-K from Buckets
Iterate buckets from indexndown to 0, appending elements to the result vector until its size reachesk. 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
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.
| Approach | Time per Op | Ordered? | Best When | Weakness |
|---|---|---|---|---|
unordered_map<T,int> | O(1) amortized | No | General-purpose; key type is hashable | Cache-unfriendly; worst-case O(n) per op |
map<T,int> | O(log n) | Yes | Need sorted output; key type has no hash | Slower constant factor; tree overhead |
int arr[SIZE] | O(1) true | Implicitly | Keys are small integers (chars, indices) | Fixed size; impractical for large/sparse keys |
| Sort + linear scan | O(n log n) total | Yes | Already sorted data; in-place constraint | Mutates input; O(n log n) baseline |
Counting approaches compared
CHOOSE THE RIGHT TOOL
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
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 array | Sliding 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 sequence | Two-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 map | Approximate 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 function | Multi-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_map | High-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
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
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
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
{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
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
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
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
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
Show answer
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.