رفتن به محتوا

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) را تضمین می‌کنند.

map_set.cpp
#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) میانگین استفاده می‌کنند. برای نوع‌های کلید سفارشی، باید یک تابع هش و یک عملگر برابری فراهم کنید.

unordered_custom.cpp
#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() برای یافتن تمام مدخل‌ها با یک کلید مشخص استفاده کنید.

multimap_multiset.cpp
#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) نیاز دارید کدام کانتینر را باید انتخاب کنید؟