Conteneurs associatifs : map, set, unordered_map
Découvrez les conteneurs associatifs ordonnés et non ordonnés — maps, sets et leurs variantes multi- et unordered. Comprenez quand choisir les arbres plutôt que les tables de hachage.
Conteneurs associatifs ordonnés vs non ordonnés
La STL fournit deux familles de conteneurs associatifs :
Ordonnés (implémentés sous forme d'arbres rouge-noir) :
- std::map — paires clé-valeur triées, clés uniques
- std::set — clés uniques triées
- std::multimap — paires clé-valeur triées, clés dupliquées autorisées
- std::multiset — clés triées, doublons autorisés
Non ordonnés (implémentés sous forme de tables de hachage) :
- std::unordered_map — clé-valeur basé sur le hachage, clés uniques
- std::unordered_set — clés uniques basées sur le hachage
- std::unordered_multimap — basé sur le hachage, clés dupliquées
- std::unordered_multiset — basé sur le hachage, doublons
Les conteneurs ordonnés maintiennent les éléments triés et fournissent une recherche, insertion et suppression en O(log n). Les conteneurs non ordonnés utilisent le hachage pour des opérations en O(1) en moyenne mais O(n) dans le pire cas si beaucoup de collisions surviennent.
Utilisation de map et set
std::map stocke des paires clé-valeur triées par clé. std::set stocke des valeurs uniques triées. Les deux sont implémentés sous forme d'arbres rouge-noir auto-équilibrés, garantissant des opérations en O(log n).
#include <map>
#include <set>
#include <string>
#include <iostream>
int main() {
// std::map — sorted key-value pairs
std::map<std::string, int> ages;
ages["Alice"] = 30; // insert or update
ages["Bob"] = 25;
ages.insert({"Charlie", 35}); // insert only if key absent
ages.emplace("Diana", 28); // construct in-place
// Lookup
if (auto it = ages.find("Bob"); it != ages.end()) {
std::cout << it->first << " is " << it->second << '\n';
}
// C++17 structured bindings — elegant map iteration
for (const auto& [name, age] : ages) {
std::cout << name << ": " << age << '\n';
}
// Output is sorted by key: Alice, Bob, Charlie, Diana
// std::set — sorted unique values
std::set<int> primes = {2, 3, 5, 7, 11, 13};
primes.insert(5); // no effect — 5 already present
primes.insert(17); // inserted
if (primes.contains(7)) { // C++20
std::cout << "7 is prime\n";
}
// count() — returns 0 or 1 for set (useful pre-C++20)
if (primes.count(4) == 0) {
std::cout << "4 is not prime\n";
}
// lower_bound / upper_bound — range queries
auto lo = primes.lower_bound(5);
auto hi = primes.upper_bound(11);
for (auto it = lo; it != hi; ++it) {
std::cout << *it << ' '; // 5 7 11
}
std::cout << '\n';
}Conteneurs non ordonnés et fonctions de hachage personnalisées
Les conteneurs non ordonnés utilisent des tables de hachage pour une recherche en O(1) en moyenne. Pour les types de clés personnalisés, vous devez fournir une fonction de hachage et un opérateur d'égalité.
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <iostream>
// Custom type as key
struct Point {
int x, y;
bool operator==(const Point&) const = default; // C++20
};
// Custom hash function
struct PointHash {
std::size_t operator()(const Point& p) const {
// Combine hashes — a common pattern
auto h1 = std::hash<int>{}(p.x);
auto h2 = std::hash<int>{}(p.y);
return h1 ^ (h2 << 1); // simple combine (consider better hashes in production)
}
};
int main() {
// Basic unordered_map — O(1) average lookup
std::unordered_map<std::string, double> prices;
prices["apple"] = 1.50;
prices["banana"] = 0.75;
prices["cherry"] = 3.00;
// try_emplace (C++17) — only constructs value if key is absent
prices.try_emplace("apple", 9.99); // no-op, "apple" exists
prices.try_emplace("date", 4.50); // inserted
// extract and insert (C++17 node API)
auto node = prices.extract("banana");
if (!node.empty()) {
node.key() = "plantain"; // rename key without copy
prices.insert(std::move(node));
}
for (const auto& [fruit, price] : prices) {
std::cout << fruit << ": $" << price << '\n';
}
// Custom type as key
std::unordered_set<Point, PointHash> points;
points.insert({1, 2});
points.insert({3, 4});
points.insert({1, 2}); // duplicate — not inserted
std::cout << "Unique points: " << points.size() << '\n'; // 2
}Multimap et Multiset
std::multimap et std::multiset autorisent les clés dupliquées. Utilisez equal_range() pour trouver toutes les entrées avec une clé donnée.
#include <map>
#include <set>
#include <iostream>
#include <string>
int main() {
// multimap — multiple values per key
std::multimap<std::string, std::string> phone_book;
phone_book.insert({"Alice", "555-0100"});
phone_book.insert({"Alice", "555-0101"}); // duplicate key OK
phone_book.insert({"Bob", "555-0200"});
// equal_range returns a pair of iterators [first, last)
auto [begin, end] = phone_book.equal_range("Alice");
std::cout << "Alice's numbers:\n";
for (auto it = begin; it != end; ++it) {
std::cout << " " << it->second << '\n';
}
// multiset — count duplicates
std::multiset<int> ms = {1, 2, 2, 3, 3, 3};
std::cout << "Count of 3: " << ms.count(3) << '\n'; // 3
}Comparateurs personnalisés pour les conteneurs ordonnés
Les conteneurs ordonnés utilisent par défaut std::less (ordre croissant). Vous pouvez fournir un comparateur personnalisé comme troisième argument template :
std::set<int, std::greater<int>> descending = {1, 5, 3};
// ordre d'itération : 5, 3, 1Pour les types personnalisés, vous pouvez soit définir operator< dans votre classe, soit passer un foncteur comparateur ou une lambda :
auto cmp = [](const Point& a, const Point& b) {
return a.x < b.x || (a.x == b.x && a.y < b.y);
};
std::set<Point, decltype(cmp)> point_set(cmp);En C++20, vous pouvez utiliser l'opérateur spaceship (operator<=>) pour générer automatiquement tous les opérateurs de comparaison.
Utiliser operator[] sur un std::map ou std::unordered_map insère une valeur construite par défaut si la clé n'existe pas. C'est une source courante de bugs subtils :
std::map<std::string, int> m;
int val = m["missing"]; // insère {"missing", 0} dans la map !Utilisez find(), count() ou contains() (C++20) pour la recherche sans modification. Utilisez at() si vous voulez une exception en cas de clé manquante.
Utilisez unordered_map/unordered_set lorsque vous n'avez besoin que de recherche, insertion et suppression — le O(1) en moyenne bat le O(log n) pour les grandes collections.
Utilisez map/set lorsque vous avez besoin de :
- Un ordre d'itération trié
- Des requêtes par plage (lower_bound, upper_bound, equal_range)
- Un O(log n) garanti dans le pire cas (pas de collisions de hachage pathologiques)
Pour les petites collections (moins d'environ 100 éléments), un std::vector trié avec std::lower_bound peut surpasser les deux grâce à la localité de cache.
std::mapetstd::setutilisent des arbres rouge-noir — opérations en O(log n) avec ordre triéstd::unordered_mapetstd::unordered_setutilisent des tables de hachage — O(1) en moyenne mais nécessitent une fonction de hachage- Utilisez les structured bindings C++17 (
auto& [key, val]) pour une itération propre sur les maps operator[]sur les maps insère silencieusement des valeurs par défaut — utilisezfind()oucontains()pour une recherche sûre- Les types personnalisés comme clés nécessitent
operator<(ordonné) ou une fonction de hachage +operator==(non ordonné) try_emplace(C++17) évite de construire la valeur si la clé existe déjà
Quiz — Testez vos connaissances
(15 XP)1. Quelle est la complexité temporelle moyenne d'une recherche dans `std::unordered_map` ?
2. Que se passe-t-il lorsque vous utilisez `operator[]` avec une clé qui n'existe pas dans un `std::map` ?
3. Quel conteneur devriez-vous choisir si vous avez besoin d'une itération triée ET de requêtes par plage (lower_bound/upper_bound) ?