Associative Containers: map, set, unordered_map
درباره کانتینرهای انجمنی مرتب و نامرتب بیاموزید — mapها، setها و انواع multi- و unordered آنها. بفهمید چه زمانی درختها و چه زمانی جداول هش را انتخاب کنید.
کانتینرهای انجمنی مرتب در مقابل نامرتب
STL دو خانواده از کانتینرهای انجمنی (Associative Containers) ارائه میدهد:
مرتب (پیادهسازی شده با درختهای قرمز-سیاه):
- std::map — جفتهای کلید-مقدار مرتب، کلیدهای یکتا
- std::set — کلیدهای یکتای مرتب
- std::multimap — جفتهای کلید-مقدار مرتب، کلیدهای تکراری مجاز
- std::multiset — کلیدهای مرتب، تکراری مجاز
نامرتب (پیادهسازی شده با جداول هش):
- std::unordered_map — کلید-مقدار مبتنی بر هش، کلیدهای یکتا
- std::unordered_set — کلیدهای یکتای مبتنی بر هش
- std::unordered_multimap — مبتنی بر هش، کلیدهای تکراری
- std::unordered_multiset — مبتنی بر هش، تکراری
کانتینرهای مرتب عناصر را مرتب نگه میدارند و جستجو، درج و حذف O(log n) ارائه میدهند. کانتینرهای نامرتب از هش برای عملیات O(1) میانگین استفاده میکنند اما بدترین حالت O(n) است اگر برخوردهای زیادی رخ دهد.
استفاده از map و set
std::map جفتهای کلید-مقدار را به ترتیب کلید ذخیره میکند. std::set مقادیر یکتای مرتب را ذخیره میکند. هر دو به عنوان درختهای قرمز-سیاه خودمتعادلکننده پیادهسازی شدهاند و عملیات 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';
}کانتینرهای نامرتب و توابع هش سفارشی
کانتینرهای نامرتب از جداول هش برای جستجوی O(1) میانگین استفاده میکنند. برای نوعهای کلید سفارشی، باید یک تابع هش و یک عملگر برابری فراهم کنید.
#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 و Multiset
std::multimap و std::multiset اجازه کلیدهای تکراری را میدهند. از equal_range() برای یافتن تمام مدخلها با یک کلید مشخص استفاده کنید.
#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
}مقایسهکنندههای سفارشی برای کانتینرهای مرتب
کانتینرهای مرتب به طور پیشفرض از std::less (ترتیب صعودی) استفاده میکنند. میتوانید یک مقایسهکننده سفارشی به عنوان سومین آرگومان قالب ارائه دهید:
std::set<int, std::greater<int>> descending = {1, 5, 3};
// iteration order: 5, 3, 1برای نوعهای سفارشی، میتوانید operator< را در کلاس خود تعریف کنید یا یک فانکتور یا لامبدا مقایسهکننده بدهید:
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);در C++20، میتوانید از عملگر فضاپیما (operator<=>) برای تولید خودکار تمام عملگرهای مقایسه استفاده کنید.
استفاده از operator[] روی std::map یا std::unordered_map یک مقدار ساختهشده با پیشفرض را درج میکند اگر کلید وجود نداشته باشد. این منبع رایج باگهای ظریف است:
std::map<std::string, int> m;
int val = m["missing"]; // inserts {"missing", 0} into the map!برای جستجو بدون تغییر از find()، count() یا contains() (C++20) استفاده کنید. اگر میخواهید در صورت نبودن کلید استثنا پرتاب شود از at() استفاده کنید.
از unordered_map/unordered_set استفاده کنید وقتی فقط به جستجو، درج و حذف نیاز دارید — حالت میانگین O(1) برای مجموعههای بزرگ از O(log n) بهتر است.
از map/set استفاده کنید وقتی نیاز دارید به:
- ترتیب مرتب پیمایش
- پرسوجوهای بازهای (lower_bound، upper_bound، equal_range)
- تضمین O(log n) بدترین حالت (بدون برخوردهای هش آسیبشناختی)
برای مجموعههای کوچک (کمتر از حدود ۱۰۰ عنصر)، یک std::vector مرتبشده با std::lower_bound میتواند به دلیل موقعیتمندی حافظه نهان از هر دو بهتر عمل کند.
std::mapوstd::setاز درختهای قرمز-سیاه استفاده میکنند — عملیات O(log n) با ترتیب مرتبstd::unordered_mapوstd::unordered_setاز جداول هش استفاده میکنند — O(1) میانگین اما نیاز به تابع هش دارند- از structured bindings سیپلاسپلاس ۱۷ (
auto& [key, val]) برای پیمایش تمیز map استفاده کنید operator[]روی mapها بیصدا مقادیر پیشفرض درج میکند — برای جستجوی امن ازfind()یاcontains()استفاده کنید- نوعهای سفارشی به عنوان کلید نیاز به
operator<(مرتب) یا تابع هش +operator==(نامرتب) دارند try_emplace(C++17) از ساخت مقدار جلوگیری میکند اگر کلید قبلاً وجود داشته باشد
آزمون — دانش خود را بسنجید
(15 امتیاز)1. پیچیدگی زمانی میانگین جستجو در `std::unordered_map` چیست؟
2. وقتی از `operator[]` با کلیدی که در `std::map` وجود ندارد استفاده کنید چه اتفاقی میافتد؟
3. اگر به پیمایش مرتب و پرسوجوهای بازهای (lower_bound/upper_bound) نیاز دارید کدام کانتینر را باید انتخاب کنید؟