انتقل إلى المحتوى

الحاويات الترابطية: خريطة، مجموعة، خريطة غير مرتبة

تعرف على الحاويات الترابطية المرتبة وغير المرتبة - الخرائط والمجموعات ومتغيراتها المتعددة وغير المرتبة. افهم متى تختار الأشجار مقابل جداول التجزئة.

الحاويات الترابطية المرتبة مقابل غير المرتبة

توفر المحكمة الخاصة بلبنان عائلتين من الحاويات الترابطية:

مرتبة (يتم تنفيذها كأشجار حمراء وسوداء):
-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) في حالة حدوث العديد من الاصطدامات.

باستخدام الخريطة وتعيين

يقوم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
}

الخرائط المتعددة والمجموعات المتعددة

يسمح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

For custom types, you can either define operator< in your class or pass a comparator functor or 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);

In C++20, you can use the spaceship operator (operator<=>) لإنشاء جميع عوامل المقارنة تلقائيًا.

خطأ شائع

يؤدي استخدامoperator[]علىstd::mapأوstd::unordered_map إلى إدراج قيمة تم إنشاؤها افتراضيًا في حالة عدم وجود المفتاح. هذا مصدر شائع للأخطاء الدقيقة:

std::map<std::string, int> m;
int val = m["missing"]; // inserts {"missing", 0} into the map!

Use find(), count(), or contains() (C++20) for lookup without modification. Use at() إذا كنت تريد استثناءً للمفاتيح المفقودة.

أفضل ممارسة

استخدمunordered_map/unordered_set عندما تحتاج فقط إلى البحث والإدراج والحذف - تتفوق الحالة المتوسطة O(1) على O(log n) للمجموعات الكبيرة.

استخدمmap/set عندما تحتاج إلى:
- ترتيب التكرار مرتبة
- استعلامات النطاق (lower_bound،upper_bound،equal_range)
- أسوأ حالة مضمونة لـ O(log n) (لا توجد تصادمات تجزئة مرضية)

بالنسبة للمجموعات الصغيرة (أقل من 100 عنصر تقريبًا)، يمكن أن يتفوق أداءstd::vectorمعstd::lower_boundعلى كليهما بسبب منطقة ذاكرة التخزين المؤقت.

النقاط الرئيسية
  • يستخدمstd::mapوstd::setالأشجار ذات اللون الأحمر والأسود — عمليات O(log n) بترتيب مفروز
  • يستخدمstd::unordered_mapوstd::unordered_setجداول التجزئة - متوسط ​​O(1) ولكنهما يتطلبان وظيفة تجزئة
  • استخدم الارتباطات الهيكلية C++17 (auto& [key, val]) لتكرار الخريطة النظيفة
  • يُدرجoperator[]على الخرائط القيم الافتراضية بصمت — استخدمfind()أوcontains()للبحث الآمن
  • تتطلب الأنواع المخصصة كمفاتيحoperator<(مرتبة) أو وظيفة التجزئة +operator==(غير مرتبة)
  • يتجنبtry_emplace(C++17) إنشاء القيمة إذا كان المفتاح موجودًا بالفعل

اختبار — اختبر معرفتك

(15 نقطة خبرة)

1. ما هو متوسط ​​التعقيد الزمني للبحث في`std::unordered_map`؟

2. ماذا يحدث عند استخدام`operator[]`مع مفتاح غير موجود في`std::map`؟

3. ما هي الحاوية التي يجب أن تختارها إذا كنت بحاجة إلى تكرار مصنف واستعلامات نطاق (lower_bound/upper_bound)؟