AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

13 STL containers / Lesson 4 of 6

std::map and std::set — the ordered containers

14 min · +10 XP · 2 exercises · 7 questions

Read the text14 min
Exercises0/2
Quiz7 q

Quick answer

What is std::map in C++?

std::map<Key, T> is an ordered associative container that stores unique keys, each with a value, as std::pair<const Key, T> nodes in a balanced binary tree — a red–black tree in mainstream libraries. Find, insert and erase are O(log n), iteration visits the keys in sorted order, and lower_bound(k) finds the first key not less than k. std::set<Key> is the same structure with keys only.

std::map and std::set are balanced binary trees (red–black trees in every mainstream implementation) that keep their elements sorted by key. Everything about them follows from that: lookup, insertion and erasure are O(log n); iteration visits the elements in key order for free; and "the first key not less than k" is a single call, which is the question a hash table cannot answer at all. This lesson covers the interface that matters — operator[] and the insertion it hides, find/count/contains, what insert returns, emplace and try_emplace, lower_bound/upper_bound, in-order iteration with structured bindings — then custom comparators, the multi variants, and extract/merge in brief.

The model

A std::set<Key> holds unique keys; a std::map<Key, T> holds unique keys, each with a value, stored as std::pair<const Key, T>. The key is const because changing it in place would break the tree's ordering. Ordering comes from a comparator, std::less<Key> by default, so the key type needs operator< (or a comparator you supply); std::string, the arithmetic types, std::pair and std::tuple all qualify. Each element is a separately allocated node, so — as with std::list — iterators, pointers and references to an element stay valid until that element is erased, no matter how many others are inserted.

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock{{"pear", 4}, {"apple", 7}};
    stock["fig"] = 12;                    // inserts fig
    stock["apple"] += 1;                  // updates apple
    for (const auto& [name, qty] : stock) {
        std::cout << name << ' ' << qty << '\n';   // apple 8, fig 12, pear 4 — key order
    }
    return 0;
}

operator[] inserts

m[key] returns a reference to the value for key, inserting a value-initialised element first if the key is absent — 0 for numbers, an empty string, an empty vector. That is exactly right for counting (++counts[word]) and grouping (groups[key].push_back(x)), and exactly wrong for looking up: if (m["ghost"] == 0) has just created ghost. It also means operator[] is not available on a const std::map (it cannot insert) and requires the value type to be default-constructible. For a lookup that must not insert, use find, count, contains (C++20) or at (throws std::out_of_range when absent):

if (auto it = stock.find("kiwi"); it != stock.end()) std::cout << it->second;   // no insertion
if (stock.contains("fig")) std::cout << stock.at("fig");                          // C++20
std::cout << stock.count("pear");   // 0 or 1 in a map; the "how many" form for a multimap

The if with initialiser (Module 3, lesson 1) keeps the iterator scoped to the test.

insert, emplace and what they return

insert and emplace never overwrite. They return a std::pair<iterator, bool>: the iterator points at the element with that key (new or pre-existing) and the bool says whether an insertion happened. Reading that bool is how "first time we have seen this key" is written without a separate lookup:

std::map<std::string, int> ids;
auto [where, inserted] = ids.insert({"ada", 1});     // inserted == true
auto [again, second] = ids.insert({"ada", 2});       // second == false; ada is still 1
std::cout << again->second << '\n';                   // 1
ids.emplace("bob", 2);                                // constructs the pair in place
ids.try_emplace("bob", 99);                           // C++17: does nothing, bob stays 2
ids.insert_or_assign("bob", 3);                       // C++17: overwrites; bob is 3

try_emplace matters when constructing the value is expensive: unlike emplace, it builds nothing at all when the key exists. insert_or_assign is the spelling of "put" that overwrites and still reports, through its bool, whether the key was new. On a std::set, s.insert(x).second is the one-line membership-and-add.

lower_bound and upper_bound

lower_bound(k) returns an iterator to the first element whose key is not less than k; upper_bound(k) to the first whose key is greater than k; both return end() when there is none, and equal_range(k) gives both at once. They are the reason to choose an ordered container: "the next departure at or after 09:30", "the largest key below x", "every key in [lo, hi)" are all one or two calls:

std::set<int> departures{600, 645, 700, 900};
auto next = departures.lower_bound(650);          // → 700
auto prevIt = departures.lower_bound(650);
if (prevIt != departures.begin()) --prevIt;        // → 645, the last key < 650
for (auto it = departures.lower_bound(640); it != departures.upper_bound(700); ++it) {
    std::cout << *it << ' ';                       // 645 700 — the keys in [640, 700]
}

Use the member versions. The free std::lower_bound(s.begin(), s.end(), k) from <algorithm> compiles on a set but walks it linearly, because a tree iterator is bidirectional, not random access — O(n) instead of O(log n).

Erasing

m.erase(key) removes the element with that key and returns how many it removed (0 or 1 for a map or set); m.erase(it) removes the element at an iterator and returns the iterator after it; m.erase(first, last) removes a range. Erasing invalidates only iterators to the erased element, which is what makes the it = m.erase(it) loop of lesson 6 safe, and std::erase_if(m, pred) (C++20) removes every element matching a predicate on the pair.

Custom comparators

The third template argument decides the order. std::greater<Key> reverses it; a struct with a const operator() (a functor, Module 11, lesson 4) orders by anything you like. The comparator must be a strict weak ordering: irreflexive (comp(a, a) is false), so <= is never correct — a comparator that returns true for equal keys corrupts the tree and the symptoms are lost elements and infinite loops.

struct ByLengthThenText {
    bool operator()(const std::string& a, const std::string& b) const {
        if (a.size() != b.size()) return a.size() < b.size();
        return a < b;
    }
};
std::set<std::string, ByLengthThenText> words{"pear", "fig", "apple", "kiwi"};
// fig kiwi pear apple

std::map<std::string, int, std::less<>> byName;    // transparent comparator
byName.find(std::string_view{"ada"});               // looks up without building a std::string

std::less<> — the transparent comparator, C++14 — lets find, count, lower_bound and contains take anything comparable with the key (a std::string_view, a const char*) without constructing a temporary std::string. It is worth writing by default for string-keyed maps.

multiset and multimap

The multi variants allow equal keys and keep them in insertion order among themselves. insert always succeeds and returns a plain iterator; count(k) is the number of matches; equal_range(k) yields the run of them; erase(k) removes all of them — erase one with erase(find(k)). A std::multiset<int> is a sorted bag that supports "remove one copy of x" in O(log n), which makes it the standard choice for a running median or a sliding-window minimum with deletions.

extract and merge, in brief

C++17 added node handles. m.extract(key) (or extract(it)) unlinks the node and returns a handle that owns it; the handle's key() is mutable, so a key can be changed and the node put back with m.insert(std::move(handle)) — no copy of the value, no reallocation. a.merge(b) moves every node of b whose key is absent from a into a, leaving the duplicates in b. Both are rare in everyday code and common in interview follow-ups.

Pitfalls

MistakeWhat happens
if (m[key] == 0) to test presenceInserts key with 0; use find, count or contains
m[key] on a const std::map& parameterDoes not compile; use find or at
Expecting insert to overwriteIt keeps the old value; use m[k] = v or insert_or_assign
std::lower_bound(s.begin(), s.end(), k) on a setCorrect but O(n); call s.lower_bound(k)
A comparator using <=Not a strict weak ordering; the tree misbehaves
for (auto [k, v] : m)Copies every pair; const auto& [k, v] reads, auto& [k, v] modifies v
ms.erase(x) on a multiset to remove one copyRemoves all copies; ms.erase(ms.find(x))

Key takeaways

  • std::map/std::set are balanced trees: O(log n) find, insert and erase; iteration is in key order; elements are pair<const Key, T> nodes whose iterators survive other insertions.
  • operator[] inserts a value-initialised element when the key is absent — right for counting, wrong for lookups; use find, count, contains or at.
  • insert/emplace never overwrite and return {iterator, bool}; try_emplace skips construction, insert_or_assign overwrites.
  • Member lower_bound/upper_bound answer "first key ≥ k" and "first key > k" in O(log n) — the reason to pick an ordered container.
  • A comparator is a strict weak ordering (never <=); std::less<> allows heterogeneous lookup; multi variants keep duplicates and erase(key) removes them all.

Common questions

Does operator[] insert into std::map?

Yes. m[key] returns a reference to the value and first inserts a value-initialised element — 0, an empty string — when the key is absent. That suits counting with ++counts[word], but a presence test written with m[key] creates the key. Look up with find, count, contains (C++20) or at, which throws std::out_of_range.

What is the difference between find, count and contains in std::map?

All three look up without inserting and run in O(log n). find returns an iterator to the element or end(), so it also gives you the value; count returns 0 or 1, or the number of matches in a multimap; contains, added in C++20, returns a bool. Use find when you need the value and contains when you only test.

What do lower_bound and upper_bound return on a std::set?

lower_bound(k) returns an iterator to the first element whose key is not less than k, and upper_bound(k) to the first whose key is greater than k; both return end() when there is none. Use the member versions: the free std::lower_bound compiles on a set but walks it in O(n), because tree iterators are not random access.

What is the difference between insert, emplace and try_emplace in std::map?

None of them overwrites an existing key, and each returns {iterator, bool} saying whether it inserted. insert takes a ready pair, emplace constructs the pair in place, and try_emplace (C++17) constructs nothing at all when the key is already present. To overwrite, use m[k] = v or insert_or_assign.

What is the difference between std::map and std::multimap?

std::multimap and std::multiset allow equal keys, kept in insertion order among themselves. insert always succeeds, count(k) returns the number of matches, equal_range(k) gives the run of them, and erase(k) removes all of them — remove just one with erase(find(k)).

Mark as readSign in to keep your place.

C++ Map and Set: Find, Insert and Lower Bound Explained

std::map and std::set are balanced trees with O(log n) lookup. Why the subscript operator inserts, what insert returns, and lower and upper bound queries.

  • Course: C++ study plan
  • Module: STL containers
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Clang 18 · C++20

What is std::map in C++?

std::map<Key, T> is an ordered associative container that stores unique keys, each with a value, as std::pair<const Key, T> nodes in a balanced binary tree — a red–black tree in mainstream libraries. Find, insert and erase are O(log n), iteration visits the keys in sorted order, and lower_bound(k) finds the first key not less than k. std::set<Key> is the same structure with keys only.

Lesson

std::map and std::set are balanced binary trees (red–black trees in every mainstream implementation) that keep their elements sorted by key. Everything about them follows from that: lookup, insertion and erasure are O(log n); iteration visits the elements in key order for free; and "the first key not less than k" is a single call, which is the question a hash table cannot answer at all. This lesson covers the interface that matters — operator[] and the insertion it hides, find/count/contains, what insert returns, emplace and try_emplace, lower_bound/upper_bound, in-order iteration with structured bindings — then custom comparators, the multi variants, and extract/merge in brief.

The model

A std::set<Key> holds unique keys; a std::map<Key, T> holds unique keys, each with a value, stored as std::pair<const Key, T>. The key is const because changing it in place would break the tree's ordering. Ordering comes from a comparator, std::less<Key> by default, so the key type needs operator< (or a comparator you supply); std::string, the arithmetic types, std::pair and std::tuple all qualify. Each element is a separately allocated node, so — as with std::list — iterators, pointers and references to an element stay valid until that element is erased, no matter how many others are inserted.

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock{{"pear", 4}, {"apple", 7}};
    stock["fig"] = 12;                    // inserts fig
    stock["apple"] += 1;                  // updates apple
    for (const auto& [name, qty] : stock) {
        std::cout << name << ' ' << qty << '\n';   // apple 8, fig 12, pear 4 — key order
    }
    return 0;
}

operator[] inserts

m[key] returns a reference to the value for key, inserting a value-initialised element first if the key is absent — 0 for numbers, an empty string, an empty vector. That is exactly right for counting (++counts[word]) and grouping (groups[key].push_back(x)), and exactly wrong for looking up: if (m["ghost"] == 0) has just created ghost. It also means operator[] is not available on a const std::map (it cannot insert) and requires the value type to be default-constructible. For a lookup that must not insert, use find, count, contains (C++20) or at (throws std::out_of_range when absent):

if (auto it = stock.find("kiwi"); it != stock.end()) std::cout << it->second;   // no insertion
if (stock.contains("fig")) std::cout << stock.at("fig");                          // C++20
std::cout << stock.count("pear");   // 0 or 1 in a map; the "how many" form for a multimap

The if with initialiser (Module 3, lesson 1) keeps the iterator scoped to the test.

insert, emplace and what they return

insert and emplace never overwrite. They return a std::pair<iterator, bool>: the iterator points at the element with that key (new or pre-existing) and the bool says whether an insertion happened. Reading that bool is how "first time we have seen this key" is written without a separate lookup:

std::map<std::string, int> ids;
auto [where, inserted] = ids.insert({"ada", 1});     // inserted == true
auto [again, second] = ids.insert({"ada", 2});       // second == false; ada is still 1
std::cout << again->second << '\n';                   // 1
ids.emplace("bob", 2);                                // constructs the pair in place
ids.try_emplace("bob", 99);                           // C++17: does nothing, bob stays 2
ids.insert_or_assign("bob", 3);                       // C++17: overwrites; bob is 3

try_emplace matters when constructing the value is expensive: unlike emplace, it builds nothing at all when the key exists. insert_or_assign is the spelling of "put" that overwrites and still reports, through its bool, whether the key was new. On a std::set, s.insert(x).second is the one-line membership-and-add.

lower_bound and upper_bound

lower_bound(k) returns an iterator to the first element whose key is not less than k; upper_bound(k) to the first whose key is greater than k; both return end() when there is none, and equal_range(k) gives both at once. They are the reason to choose an ordered container: "the next departure at or after 09:30", "the largest key below x", "every key in [lo, hi)" are all one or two calls:

std::set<int> departures{600, 645, 700, 900};
auto next = departures.lower_bound(650);          // → 700
auto prevIt = departures.lower_bound(650);
if (prevIt != departures.begin()) --prevIt;        // → 645, the last key < 650
for (auto it = departures.lower_bound(640); it != departures.upper_bound(700); ++it) {
    std::cout << *it << ' ';                       // 645 700 — the keys in [640, 700]
}

Use the member versions. The free std::lower_bound(s.begin(), s.end(), k) from <algorithm> compiles on a set but walks it linearly, because a tree iterator is bidirectional, not random access — O(n) instead of O(log n).

Erasing

m.erase(key) removes the element with that key and returns how many it removed (0 or 1 for a map or set); m.erase(it) removes the element at an iterator and returns the iterator after it; m.erase(first, last) removes a range. Erasing invalidates only iterators to the erased element, which is what makes the it = m.erase(it) loop of lesson 6 safe, and std::erase_if(m, pred) (C++20) removes every element matching a predicate on the pair.

Custom comparators

The third template argument decides the order. std::greater<Key> reverses it; a struct with a const operator() (a functor, Module 11, lesson 4) orders by anything you like. The comparator must be a strict weak ordering: irreflexive (comp(a, a) is false), so <= is never correct — a comparator that returns true for equal keys corrupts the tree and the symptoms are lost elements and infinite loops.

struct ByLengthThenText {
    bool operator()(const std::string& a, const std::string& b) const {
        if (a.size() != b.size()) return a.size() < b.size();
        return a < b;
    }
};
std::set<std::string, ByLengthThenText> words{"pear", "fig", "apple", "kiwi"};
// fig kiwi pear apple

std::map<std::string, int, std::less<>> byName;    // transparent comparator
byName.find(std::string_view{"ada"});               // looks up without building a std::string

std::less<> — the transparent comparator, C++14 — lets find, count, lower_bound and contains take anything comparable with the key (a std::string_view, a const char*) without constructing a temporary std::string. It is worth writing by default for string-keyed maps.

multiset and multimap

The multi variants allow equal keys and keep them in insertion order among themselves. insert always succeeds and returns a plain iterator; count(k) is the number of matches; equal_range(k) yields the run of them; erase(k) removes all of them — erase one with erase(find(k)). A std::multiset<int> is a sorted bag that supports "remove one copy of x" in O(log n), which makes it the standard choice for a running median or a sliding-window minimum with deletions.

extract and merge, in brief

C++17 added node handles. m.extract(key) (or extract(it)) unlinks the node and returns a handle that owns it; the handle's key() is mutable, so a key can be changed and the node put back with m.insert(std::move(handle)) — no copy of the value, no reallocation. a.merge(b) moves every node of b whose key is absent from a into a, leaving the duplicates in b. Both are rare in everyday code and common in interview follow-ups.

Pitfalls

MistakeWhat happens
if (m[key] == 0) to test presenceInserts key with 0; use find, count or contains
m[key] on a const std::map& parameterDoes not compile; use find or at
Expecting insert to overwriteIt keeps the old value; use m[k] = v or insert_or_assign
std::lower_bound(s.begin(), s.end(), k) on a setCorrect but O(n); call s.lower_bound(k)
A comparator using <=Not a strict weak ordering; the tree misbehaves
for (auto [k, v] : m)Copies every pair; const auto& [k, v] reads, auto& [k, v] modifies v
ms.erase(x) on a multiset to remove one copyRemoves all copies; ms.erase(ms.find(x))

Key takeaways

  • std::map/std::set are balanced trees: O(log n) find, insert and erase; iteration is in key order; elements are pair<const Key, T> nodes whose iterators survive other insertions.
  • operator[] inserts a value-initialised element when the key is absent — right for counting, wrong for lookups; use find, count, contains or at.
  • insert/emplace never overwrite and return {iterator, bool}; try_emplace skips construction, insert_or_assign overwrites.
  • Member lower_bound/upper_bound answer "first key ≥ k" and "first key > k" in O(log n) — the reason to pick an ordered container.
  • A comparator is a strict weak ordering (never <=); std::less<> allows heterogeneous lookup; multi variants keep duplicates and erase(key) removes them all.

Common questions

Does operator[] insert into std::map?

Yes. m[key] returns a reference to the value and first inserts a value-initialised element — 0, an empty string — when the key is absent. That suits counting with ++counts[word], but a presence test written with m[key] creates the key. Look up with find, count, contains (C++20) or at, which throws std::out_of_range.

What is the difference between find, count and contains in std::map?

All three look up without inserting and run in O(log n). find returns an iterator to the element or end(), so it also gives you the value; count returns 0 or 1, or the number of matches in a multimap; contains, added in C++20, returns a bool. Use find when you need the value and contains when you only test.

What do lower_bound and upper_bound return on a std::set?

lower_bound(k) returns an iterator to the first element whose key is not less than k, and upper_bound(k) to the first whose key is greater than k; both return end() when there is none. Use the member versions: the free std::lower_bound compiles on a set but walks it in O(n), because tree iterators are not random access.

What is the difference between insert, emplace and try_emplace in std::map?

None of them overwrites an existing key, and each returns {iterator, bool} saying whether it inserted. insert takes a ready pair, emplace constructs the pair in place, and try_emplace (C++17) constructs nothing at all when the key is already present. To overwrite, use m[k] = v or insert_or_assign.

What is the difference between std::map and std::multimap?

std::multimap and std::multiset allow equal keys, kept in insertion order among themselves. insert always succeeds, count(k) returns the number of matches, equal_range(k) gives the run of them, and erase(k) removes all of them — remove just one with erase(find(k)).

Exercises

Word frequencies in key order

Read all of standard input as whitespace-separated tokens. Normalise each token by keeping only its letters and digits (drop every other character) and lower-casing them — use <cctype> with the unsigned char cast — and skip a token that becomes empty. Count the words in a std::map<std::string, int> with ++freq[word], then print every <word> <count> on its own line in the map's own order (no sorting step), followed by distinct=<number of words> total=<number of tokens counted>.

Input: free text until end of input (possibly empty). Output: one line per distinct word, then the summary line.

The cat saw the other cat.
A cat!

prints

a 1
cat 3
other 1
saw 1
the 2
distinct=5 total=8

Next departure with lower_bound

A timetable is a std::set<int> of departure times (minutes past midnight; the input may repeat a time — the set collapses it). Read n, then n times, then q, then q query times. For each query t print <t>: next=<x> prev=<y> where x is the first departure at or after t and y the last departure strictly before t; print none for either when it does not exist.

Use the set's member lower_bound for next, and std::prev of that iterator for prev — after checking it is not begin().

Input: n, n integers, q, q integers. Output: q lines.

5
600 645 700 900 1200
4
630
700
1300
0

prints

630: next=645 prev=600
700: next=700 prev=645
1300: next=none prev=1200
0: next=600 prev=none

In this module: STL containers

← deque, list and the adaptors — stack, queue and priority_queue · The unordered containers — hashing, buckets and custom keys →