C++ • Collections and STL

std::map & unordered_map — Use std::map/unordered_map for key–value storage (intro)

Master the two primary associative containers in the STL for efficient key–value lookups.

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

SECTION 1

Historical Context & Motivation

Long before the C++ Standard Template Library (STL) was standardized, programmers needed efficient ways to associate one piece of data with another — mapping student IDs to grade records, words to their frequencies, or network addresses to socket descriptors. Early C programmers typically hand-rolled associative data structures using sorted arrays with binary search, hand-coded binary search trees, or simple hash tables with open addressing. These ad hoc implementations were error-prone, rarely reusable, and required careful memory management. The desire for a standardized, generic, and type-safe approach to key–value storage was a primary motivation behind the design of the STL's associative containers.

Alexander Stepanov, drawing on ideas from generic programming and abstract algebra, envisioned a library where algorithms and data structures would be decoupled through iterators. His collaboration with Meng Lee at Hewlett-Packard produced the initial STL proposal, which included std::map as a sorted associative container backed by a balanced binary search tree. The unordered (hash-based) counterpart took longer to arrive, finally joining the standard in C++11 after years of use as a Boost and compiler-extension library.

1994 • STL Proposal Accepted

Stepanov and Lee's STL proposal, including std::map and std::multimap, is accepted into the draft C++ standard by the ISO committee.

1998 • C++98 Standardized

The first ISO C++ standard (C++98) ships with four ordered associative containers: map, multimap, set, and multiset.

2005 • TR1 Hash Containers

Technical Report 1 (TR1) proposes std::tr1::unordered_map based on Boost.Unordered, bringing hash-table–based associative containers into the standard library's orbit.

2011 • C++11 — Unordered Containers Official

std::unordered_map, unordered_set, and their multi variants become first-class citizens in <unordered_map> and <unordered_set>. Move semantics and initializer lists further enhance usability.

2017–23 • Modern Enhancements

C++17 adds structured bindings, try_emplace, insert_or_assign, and node handles for splicing between maps. C++20 introduces contains() for cleaner membership checks.

The central question this lesson addresses is straightforward yet deep: when should you reach for std::map, when for std::unordered_map, and how do their internal structures shape the performance guarantees your code relies on?

SECTION 2

Core Principles & Definitions

Both std::map and std::unordered_map model the same abstract concept: an associative container that stores elements formed by the combination of a unique key and a mapped value, collectively called a key–value pair (represented in C++ as std::pair<const Key, Value>). Despite this shared interface, they differ fundamentally in their internal organization and the complexity guarantees they provide. Understanding these foundational principles will let you make informed trade-off decisions for any codebase.

1

Ordered vs. Unordered

std::map maintains its elements in sorted order by key (using a comparator, default std::less<Key>). std::unordered_map imposes no ordering; elements are grouped into buckets determined by a hash function.
2

Unique Keys

Both containers enforce key uniqueness. Inserting a duplicate key with insert is a no-op; using operator[] on an existing key overwrites the mapped value. For duplicate keys, use std::multimap or std::unordered_multimap.
3

Iterator Stability

In std::map, iterators remain valid across insertions and erasures (except for the erased element). In std::unordered_map, a rehash can invalidate all iterators, although references to elements survive.
4

Complexity Contracts

std::map guarantees O(log n) for lookup, insertion, and deletion. std::unordered_map offers amortized O(1) average case but O(n) worst case when many keys collide.
✦

KEY TAKEAWAY

Think of std::map as a library card catalog where cards are filed alphabetically — you can always find a card in logarithmic time and walk the catalog in sorted order, but filing a new card requires finding the right slot. std::unordered_map is more like a hash-addressed post office: a hash of the recipient's name sends you straight to the right PO box in constant time, but if too many names hash to the same box the clerk has to search linearly through that box's contents.

SECTION 3

Visual Explanation — Internal Data Structures

The most important distinction between std::map and std::unordered_map comes down to their underlying data structures. The diagram below illustrates both side by side: a red-black tree for the ordered map on the left and a hash table with separate chaining for the unordered map on the right. Notice how the tree naturally maintains key order (an in-order traversal yields keys in ascending sequence), while the hash table distributes entries across buckets with no inherent ordering.

std::map (Red-Black Tree)std::unordered_map (Hash Table)30155010204070In-order: 10 → 15 → 20 → 30 → 40 → 50 → 70● Red node● Black nodeLookup: O(log n) guaranteedKeys stored as std::pair<const K, V>Requires operator< or custom Compare[0][1][2][3][4][5][6]70→"sev"15→"fif"30→"thi"10→"ten"50→"fif"40→"for"20→"twe"Collision at bucket [3]: two entries chainedLookup: O(1) average, O(n) worstKeys stored as std::pair<const K, V>Requires std::hash<K> + operator==
Left: A red-black tree storing keys {10, 15, 20, 30, 40, 50, 70}. An in-order traversal yields the sorted sequence. Right: A hash table with 7 buckets. Keys 10 and 50 collide at bucket [3] and are chained together (shown in pink), illustrating separate chaining. std::map requires operator<; std::unordered_map requires std::hash<K> and operator==.

Observe several structural consequences visible in the diagram. In the red-black tree, the path from the root (key 30) to any leaf differs in length by at most a factor of two, which is the balance invariant that guarantees O(log n) operations. In the hash table, most buckets contain a single entry, enabling O(1) access; however, bucket [3] chains two entries (10 and 50), demonstrating that collisions degrade to a linear scan within that bucket. The maximum load factor (default 1.0) determines when the table rehashes — doubling its bucket count and redistributing all entries — to keep chains short.

SECTION 4

How It Works — Complexity & Hashing

Understanding the performance of these containers requires formalizing the relationship between the number of stored elements and the cost of fundamental operations. Let n denote the number of key–value pairs currently stored. For std::map, the red-black tree height is bounded by 2 × log₂(n + 1), yielding logarithmic worst-case performance. For std::unordered_map, average-case performance depends on the load factor α = n / B, where B is the number of buckets.

RED-BLACK TREE HEIGHT BOUND

h≤2×log2(n+1)h ≤ 2 × log₂(n + 1)

where h is the tree height, and n is the number of stored entries. This guarantees O(log n) lookup, insertion, and deletion in the worst case.

HASH TABLE LOAD FACTOR

α=n/Bα = n / B

where α is the load factor, n is the element count, and B is the bucket count. When α exceeds max_load_factor() (default 1.0), a rehash doubles the bucket count.

EXPECTED CHAIN LENGTH (UNIFORM HASHING)

E[chainlength]=α=n/BE[chain length] = α = n / B

Under the simple uniform hashing assumption, each bucket has on average α entries. Because the default max_load_factor() keeps α ≤ 1, the expected lookup cost is O(1).

The hash function std::hash<Key> is specialized for fundamental types (int, std::string, pointers, etc.). For user-defined types, you must provide a custom specialization or pass a hash functor as a template parameter. A good hash function distributes keys uniformly across buckets; a poor one concentrates keys in a few buckets, pushing average-case behavior toward the O(n) worst case.

ℹ

Rehash Cost

A single rehash is O(n) because every element must be re-hashed and re-inserted into the new bucket array. However, because rehashes occur only when the load factor threshold is crossed (roughly when the container doubles), the amortized cost per insertion remains O(1). This is directly analogous to the amortized analysis of std::vector::push_back with geometric capacity growth.

SECTION 5

API Deep Dive — Common Operations

Both containers share a remarkably similar API surface, which is by design — the STL's concept-based approach lets you swap one for the other with minimal code changes. The table below catalogs the most important member functions, highlighting where the two diverge. Afterward, a second diagram illustrates the typical insertion and lookup workflow for each container.

Operationstd::mapstd::unordered_map
operator[](key)O(log n) — inserts default value if key absentO(1) avg — same insert-on-miss semantics
at(key)O(log n) — throws std::out_of_range if absentO(1) avg — throws std::out_of_range if absent
insert({k,v})O(log n) — no-op if key existsO(1) avg — no-op if key exists
emplace(args...)O(log n) — constructs in-placeO(1) avg — constructs in-place
find(key)O(log n) — returns iterator or end()O(1) avg — returns iterator or end()
contains(key) (C++20)O(log n) — returns boolO(1) avg — returns bool
erase(key)O(log n) — returns count (0 or 1)O(1) avg — returns count (0 or 1)
Iteration orderSorted by keyArbitrary (bucket order)
lower_bound / upper_boundO(log n) — range queries on sorted keysNot available — no ordering

Common operations on std::map vs. std::unordered_map

Insertion Workflow Comparisonstd::map::insert({key, val})1. Compare key vs. root node2. Traverse left or right subtree3. Repeat until leaf position found4. Insert node at leaf5. Rebalance (rotations + recolor)Total: O(log n) comparisonsunordered_map::insert({key, val})1. Compute hash(key)2. bucket = hash % bucket_count3. Scan chain for duplicate key4. Append to chain (or no-op)5. Rehash if α > max_load_factorTotal: O(1) amortized average
The left column traces insertion into std::map: the tree is traversed via comparisons, a new leaf is inserted, and then rotations/recoloring restore the red-black invariant. The right column traces insertion into std::unordered_map: the hash is computed, the bucket is located, the chain is scanned for duplicates, and a conditional rehash may follow.
⚠

operator[] Pitfall

A common source of bugs: myMap[key] will silently default-construct a value if the key does not exist. This means if (myMap[k] == 0) inserts a zero entry for k as a side effect, inflating the map's size. Prefer find() or contains() (C++20) for existence checks.

SECTION 6

Worked Example — Word Frequency Counter

A classic application of associative containers is counting word frequencies in a text corpus. We will implement this task using both std::map and std::unordered_map, observing the differences in output ordering and performance characteristics.

Counting Word Frequencies

  1. 1

    Step 1 — Include Headers & Define Input

    We include <map>, <unordered_map>, <string>, <sstream>, and <iostream>. The input string is: "the cat sat on the mat the cat".
  2. 2

    Step 2 — Populate the Ordered Map

    We declare std::map<std::string, int> ordered; and stream words from the input. For each word w, we write ordered[w]++;. The operator[] default-inserts 0 for a new key and then increments it. After processing all words, the map contains: {"cat":2, "mat":1, "on":1, "sat":1, "the":3} — note the alphabetical ordering.
    Map contents (sorted): cat→2, mat→1, on→1, sat→1, the→3
  3. 3

    Step 3 — Populate the Unordered Map

    We repeat the same logic with std::unordered_map<std::string, int> unordered;. The results are identical in content — the same five key–value pairs — but iterating over unordered yields an arbitrary order such as: on→1, mat→1, sat→1, the→3, cat→2 (depends on hash function and bucket count).
    Map contents (unordered): implementation-defined order
  4. 4

    Step 4 — Iterate with Structured Bindings (C++17)

    We can elegantly iterate using for (const auto& [word, count] : ordered). The structured binding decomposes each std::pair<const std::string, int> into named variables. This syntax works identically for both map types.
  5. 5

    Step 5 — Analyze Complexity

    For the ordered map, each operator[] call is O(log n), so building the frequency table from W words costs O(W × log n) where n grows up to the number of distinct words. For the unordered map, each call is O(1) amortized on average (assuming a good hash), so the total is O(W). For large corpora with many distinct words, the unordered variant is typically faster.
    std::map: O(W log n) total | std::unordered_map: O(W) amortized total

SECTION 7

Strengths, Limitations & When to Choose Each

Neither container is universally superior; the right choice depends on your access patterns, key types, memory constraints, and whether you need ordered iteration. The following table provides a systematic comparison across the dimensions that matter most in practice.

Criterionstd::mapstd::unordered_map
Lookup complexityO(log n) worst caseO(1) average, O(n) worst
Iteration orderSorted by key (ascending by default)Arbitrary / implementation-defined
Range queriesSupported via lower_bound / upper_boundNot supported
Key requirementsoperator< or custom comparatorstd::hash<K> + operator==
Memory overhead~3 pointers + color bit per nodeBucket array + per-node pointers; can be higher
Cache behaviorPoor — nodes scattered in heapBetter for small n; degrades with chaining
Iterator invalidationOnly erased element invalidatedAll iterators on rehash; references survive
Worst-case guaranteeO(log n) — deterministicO(n) — depends on hash quality

Head-to-head comparison of std::map and std::unordered_map

🧭

DECISION HEURISTIC

Use std::unordered_map as your default choice when you need fast key–value lookups and don't care about element ordering. Switch to std::map when you need sorted iteration, range queries (e.g., "all keys between 100 and 200"), or when your key type lacks a good hash function but has a natural ordering. In adversarial settings (e.g., competitive programming with anti-hash tests), std::map's O(log n) worst-case guarantee can prevent TLE.

SECTION 8

Connection to Advanced Topics

The ordered and unordered maps introduced here are the starting point for a rich family of associative containers and advanced techniques in C++. As your projects grow in complexity, you will encounter scenarios that push beyond basic key–value storage. The table below maps each introductory concept to its advanced counterpart, giving you a roadmap for further study.

Introductory ConceptAdvanced Extension
Unique keys (map / unordered_map)Duplicate keys via std::multimap and std::unordered_multimap
Default std::hash specializationsCustom hash functions (FNV-1a, CityHash) and open-addressing schemes (Robin Hood hashing)
Copy / move semanticsNode handles (C++17) for zero-copy splicing between maps via extract() and insert(node)
operator[] / insert()try_emplace() (C++17) — avoids constructing the value if the key already exists, eliminating wasted allocations
Single-threaded usageThread-safe hash maps: tbb::concurrent_hash_map, lock-free designs, or sharded maps with per-bucket locking
In-memory storagePersistent / disk-backed maps: B-trees, LSM trees, and libraries like RocksDB

Roadmap from introductory to advanced associative container topics

One particularly important advanced concept is transparent comparators (C++14). By defining a comparator with an is_transparent tag (e.g., std::less<>), you allow heterogeneous lookups: searching a std::map<std::string, int> with a std::string_view or a const char* without constructing a temporary std::string. This optimization is critical in performance-sensitive contexts like game engines and financial systems.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why std::unordered_map does not support lower_bound() and upper_bound(), while std::map does. What property of the underlying data structure makes range queries possible in one but not the other?
Show answer
std::map is backed by a balanced BST that maintains elements in sorted key order, so it can efficiently locate the first key ≥ a given value (lower_bound) or > a given value (upper_bound) in O(log n) time by traversing the tree. std::unordered_map distributes elements across buckets based on hash values, which bear no relationship to key ordering. To find all keys in a range [lo, hi], you would need to inspect every element, making it an O(n) scan — hence the interface does not expose range query operations. The fundamental property enabling range queries is the sorted invariant maintained by the red-black tree.

Question 2: BASIC CALCULATION

Suppose you insert 1,000 unique keys into both a std::map and a std::unordered_map with default settings. (a) What is the maximum number of comparisons for a single lookup in the map? (b) If the unordered_map's bucket count after all insertions is 1024, what is the load factor?
Show answer
(a) The red-black tree height is bounded by 2 × log₂(1001) ≈ 2 × 9.97 ≈ 19.94, so at most 20 comparisons are needed for a single lookup. In practice, the height of a red-black tree with 1,000 nodes is typically around 14–17. (b) The load factor α = n / B = 1000 / 1024 ≈ 0.977. This is just below the default max_load_factor of 1.0, so no further rehash is triggered. Each bucket contains about one element on average, yielding excellent O(1) lookup performance under uniform hashing.

Question 3: INTERMEDIATE

Write a C++ function std::map<char, int> charFrequency(const std::string& s) that returns a map of character frequencies. Then write a second version using std::unordered_map. For the input "abracadabra", state the output of iterating each map. Which version would you prefer if you needed to print characters in alphabetical order? Which if you only needed to find the most frequent character?
Show answer
Both functions use the same loop: for (char c : s) freq[c]++; where freq is either std::map<char,int> or std::unordered_map<char,int>. For "abracadabra", the sorted map yields: a→5, b→2, c→1, d→1, r→2 (alphabetical). The unordered map yields the same pairs but in an implementation-defined order. For alphabetical printing, std::map is preferable since iteration is already sorted. For finding the most frequent character, std::unordered_map is preferable because building it is O(n) rather than O(n log k) where k is the number of distinct characters, and ordering is irrelevant to a max-search.

Question 4: APPLIED

You are building a DNS cache that maps domain names (std::string) to IP addresses (std::string). The cache receives ~50,000 lookups per second. Insertions are rare (~100/sec). The cache never needs sorted iteration. (a) Which container would you choose and why? (b) If an attacker can craft domain names that all hash to the same bucket, what is the worst-case lookup time, and how might you mitigate this?
Show answer
(a) std::unordered_map is the clear choice. Lookups dominate the workload, and constant-time average access dramatically outperforms O(log n) for 50k lookups/sec with no ordering requirement. (b) If all keys hash to the same bucket, every lookup degrades to O(n) — a linear scan through the single chain. With n entries in the cache, this can be devastating. Mitigations include: using a cryptographic or keyed hash function (e.g., SipHash) so attackers cannot predict collisions; reserving sufficient buckets upfront via reserve(); or switching to an open-addressing scheme with Robin Hood hashing that limits probe sequence length. Some implementations (like libstdc++ in GCC) use a prime bucket count to reduce collision clustering.

Question 5: CRITICAL THINKING

Consider a custom type struct Point { int x, y; };. (a) What do you need to provide to use Point as a key in std::map<Point, double>? (b) What do you need for std::unordered_map<Point, double>? (c) Propose a hash function for Point and analyze whether it distributes well. (d) Is the ordering imposed by your comparator semantically meaningful for 2D points? Discuss the implications.
Show answer
(a) For std::map, you need a strict weak ordering — either overload operator< for Point, or provide a custom comparator. A common choice is lexicographic: compare x first, then y on tie. (b) For std::unordered_map, you need a specialization of std::hash<Point> and operator==. (c) A simple hash: hash(p) = std::hash<int>{}(p.x) ^ (std::hash<int>{}(p.y) << 1). This works but has a known weakness: (a,b) and (b,a) may collide if the shift doesn't sufficiently differentiate them. A better approach uses hash_combine (boost-style): seed ^= hash(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2). (d) Lexicographic ordering is mathematically valid as a strict weak ordering, but it is not geometrically meaningful — Point(1,1000) < Point(2,0) despite being farther from the origin. This means lower_bound/upper_bound on a std::map<Point,...> produce ranges along the lexicographic axis, not spatial neighborhoods. For spatial queries, a k-d tree or R-tree would be more appropriate.

SUMMARY

Summary — std::map & unordered_map

Summary

C++ provides two primary associative containers for key–value storage. std::map stores elements in a red-black tree, guaranteeing O(log n) worst-case lookup, insertion, and deletion, with elements always iterable in sorted key order. It supports range queries via lower_bound and upper_bound, and requires only operator< on the key type. std::unordered_map uses a hash table with separate chaining, delivering amortized O(1) average-case performance but O(n) worst case when collisions pile up. It requires std::hash<Key> and operator==.

Choose unordered_map as the default when ordering is irrelevant and you want raw lookup speed. Prefer map when you need sorted traversal, range queries, or deterministic worst-case guarantees. Both share a nearly identical API — operator[], find(), insert(), erase() — making it straightforward to swap one for the other as requirements evolve. Watch for the operator[] pitfall (silent default insertion), prefer contains() or find() for existence checks, and explore C++17 additions like try_emplace and structured bindings for cleaner, more efficient code.

Varsity Tutors • C++ • std::map & unordered_map — Use std::map/unordered_map for key–value storage (intro)