📦 Phase 4 · STL 🟡 Intermediate MODULE 16

Map, Set & Unordered Containers

⏱️ 25 min read
📖 Theory + Code
🧩 5 Quiz Questions
🏗️ 1 Challenge
Your progress in Phase 4 — STL40%
🎯 What you'll learn: How to store and look up data by key with std::map and std::set, and when their hash-based cousins std::unordered_map/std::unordered_set give you faster average lookups.

Why Look Up by Key Instead of Index?

A vector is great when "the 3rd item" is a meaningful question. But often it isn't — you want to ask "what's Alice's age?" or "is this username already taken?" Doing that with a vector means scanning every element until you find a match: O(n), and it gets slower as the data grows.

The STL's associative containersmap, set, unordered_map, unordered_set — are built exactly for this. They store data so that looking something up by key is dramatically faster than a linear scan.

💡
Two families, one purpose
map/set keep their contents sorted internally using a balanced binary search tree. unordered_map/unordered_set use a hash table instead — no ordering, but typically faster lookups. Both let you check "is X here?" far faster than scanning a vector.

std::map — Ordered Key-Value Pairs

A map<K, V> stores unique keys of type K, each mapped to a value of type V. You can insert or overwrite with [], exactly like an array but indexed by any comparable type, not just integers.

map_basics.cpp
C++
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> ages;

    ages["Alice"] = 30;
    ages["Bob"] = 25;
    ages["Charlie"] = 35;

    ages["Alice"] = 31; // key exists -> overwrites the value

    for (const auto& [name, age] : ages) { // structured bindings (C++17)
        cout << name << " is " << age << " years old" << endl;
    }

    return 0;
}
map always iterates in sorted key order
Run the code above and the output always comes out Alice, Bob, Charlie — alphabetical order — no matter what order you inserted them in. This is a defining feature of map: it's built on a self-balancing binary search tree that keeps keys sorted automatically.

Checking for a key with .find()

Using ages["Zoe"] to check whether "Zoe" exists is a trap — if the key isn't there, that syntax silently inserts it with a default value. To check without inserting, use .find().

map_find.cpp
C++
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> stock = {{"apples", 50}, {"bananas", 30}};

    auto it = stock.find("apples");
    if (it != stock.end()) {
        cout << "Found: " << it->first << " -> " << it->second << endl;
    }

    if (stock.find("mangoes") == stock.end()) {
        cout << "mangoes not in stock" << endl;
    }

    return 0;
}

.find(key) returns an iterator to the matching entry, or .end() if the key doesn't exist — it never modifies the map. Both [] access and .find() run in O(log n) on a map, since the tree is balanced.

std::set — Unique, Sorted Values

A set<T> is like a map with only keys and no values — it stores each distinct element exactly once, always in sorted order. Inserting a duplicate is simply ignored.

set_basics.cpp
C++
#include <iostream>
#include <set>
using namespace std;

int main() {
    set<int> ids;

    ids.insert(5);
    ids.insert(2);
    ids.insert(5); // duplicate — silently ignored
    ids.insert(8);

    for (int id : ids) cout << id << " "; // prints sorted: 2 5 8
    cout << endl;

    cout << "Contains 5? " << (ids.find(5) != ids.end() ? "yes" : "no") << endl;

    return 0;
}

Use a set whenever you need to guarantee "no duplicates" and don't need to attach any extra data to each element — deduplicating a list of IDs, tracking which nodes you've already visited in a graph, and so on.

unordered_map & unordered_set — Hash-Based Speed

unordered_map and unordered_set store the exact same kind of data as map and set, but internally use a hash table instead of a balanced tree. The trade-off: you lose sorted iteration order, but lookups, inserts, and erases are average O(1) instead of map's O(log n).

unordered_map_basics.cpp
C++
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;

int main() {
    unordered_map<string, int> wordCount;

    wordCount["cat"]++;
    wordCount["dog"]++;
    wordCount["cat"]++;

    for (const auto& [word, count] : wordCount) {
        cout << word << ": " << count << endl; // order is NOT guaranteed
    }

    return 0;
}
⚠️
Average vs worst case
unordered_map's O(1) is an average, not a guarantee — if many keys hash to the same bucket (a "collision"), a single lookup can degrade toward O(n) in rare pathological cases. map's O(log n) is a firm worst-case guarantee. In everyday code, this rarely matters.
🌳
map / set
Balanced tree. O(log n) lookup. Keys always iterate in sorted order.
unordered_map / unordered_set
Hash table. Average O(1) lookup. No ordering guarantee at all.
🔑
Both require unique keys
Inserting a key that already exists in either family overwrites (map) or is ignored (set) — never duplicated.
🎯
Pick by need, not habit
Need sorted output or a guaranteed worst case? Use map/set. Need raw speed and don't care about order? Use unordered_*.

Lesson Summary

std::map stores unique key-value pairs sorted by key, O(log n) operations.
std::set stores unique sorted values with no attached data.
Use .find() to check for a key without accidentally inserting it via [].
unordered_map/unordered_set trade ordering for average O(1) hash-based lookups.
Structured bindings (auto& [k, v]) make iterating maps clean and readable.
🧩 Knowledge Check — Lesson 16
Answer all 5 questions to test your understanding. Instant feedback on every answer.
1. What's guaranteed about the iteration order of a std::map?
2. What is the typical AVERAGE lookup complexity of std::unordered_map?
3. What is the WORST-CASE lookup complexity of std::map, backed by a balanced tree?
4. Which container stores ONLY unique elements in sorted order, with no attached value?
5. When should you prefer unordered_map over map?
💪
Coding Challenge — Lesson 16
Apply what you learned · Intermediate Level

Now it's your turn to write real code. Complete the challenge below in any C++ compiler.

Challenge: Word Frequency Counter 📊

Write a program that:

1. Stores this list of words in a vector<string>: {"code", "bite", "code", "learn", "bite", "code"}.
2. Uses a map<string, int> to count how many times each word appears.
3. Prints every word with its count, in alphabetical order.
4. Prints which word appeared the MOST times.

Rules: Use map[word]++ to count — it auto-inserts a word with value 0 the first time you touch it, then increments.
💡 Show hints if you're stuck
  • Loop over the vector with for (const string& w : words) counts[w]++;
  • Because map iterates in sorted order automatically, printing with a range-based for loop already gives you alphabetical output — no extra sorting needed.
  • To find the max, track a string bestWord and int bestCount while you iterate, updating whenever you see a higher count.
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 16 Complete!

You can now look up data by key instead of scanning linearly. Next: LIFO/FIFO container adapters — stack, queue, and priority_queue.

Module 16 of 26 Phase 4 — STL