Chuyển đến nội dung

Vùng chứa kết hợp: bản đồ, bộ, unordered_map

Learn about ordered and unordered associative containers — maps, sets, and their multi- and unordered variants. Understand when to choose trees vs hash tables.

Các vùng chứa kết hợp có thứ tự và không có thứ tự

STL cung cấp hai họ vùng chứa kết hợp:

Đã đặt hàng (được triển khai dưới dạng cây đỏ đen):
-std::map— các cặp khóa-giá trị được sắp xếp, các khóa duy nhất
-std::set— sắp xếp các khóa duy nhất
-std::multimap— các cặp khóa-giá trị được sắp xếp, cho phép khóa trùng lặp
-std::multiset— sắp xếp khóa, cho phép trùng lặp

Không có thứ tự (được triển khai dưới dạng bảng băm):
-std::unordered_map— khóa-giá trị dựa trên hàm băm, khóa duy nhất
-std::unordered_set— khóa duy nhất dựa trên hàm băm
-std::unordered_multimap— khóa trùng lặp, dựa trên hàm băm
-std::unordered_multiset— dựa trên hàm băm, trùng lặp

Các vùng chứa được sắp xếp giữ cho các phần tử được sắp xếp và cung cấp khả năng tra cứu, chèn và xóa O(log n). Các vùng chứa không có thứ tự sử dụng hàm băm cho các hoạt động trong trường hợp trung bình O(1) nhưng trường hợp xấu nhất là O(n) nếu xảy ra nhiều xung đột.

Sử dụng bản đồ và thiết lập

std::maplưu trữ các cặp khóa-giá trị được sắp xếp theo khóa.std::setlưu trữ các giá trị được sắp xếp duy nhất. Cả hai đều được triển khai dưới dạng cây đỏ đen tự cân bằng, đảm bảo hoạt động 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';
}

Vùng chứa không có thứ tự và hàm băm tùy chỉnh

Các vùng chứa không có thứ tự sử dụng bảng băm để tra cứu trường hợp trung bình O(1). Đối với các loại khóa tùy chỉnh, bạn phải cung cấp hàm băm và toán tử đẳng thức.

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 và Multiset

std::multimapvàstd::multisetcho phép khóa trùng lặp. Sử dụngequal_range()để tìm tất cả các mục có khóa nhất định.

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
}

Bộ so sánh tùy chỉnh cho các vùng chứa được đặt hàng

Các vùng chứa được đặt hàng mặc định làstd::less(thứ tự tăng dần). Bạn có thể cung cấp bộ so sánh tùy chỉnh làm đối số mẫu thứ ba:

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<=>) để tự động tạo tất cả toán tử so sánh.

Lỗi thường gặp

Sử dụngoperator[]trênstd::maphoặcstd::unordered_mapchèn giá trị được tạo mặc định nếu khóa không tồn tại. Đây là nguồn phổ biến của các lỗi nhỏ:

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() nếu bạn muốn có ngoại lệ đối với các khóa bị thiếu.

Thực hành tốt

Sử dụngunordered_map/unordered_set khi bạn chỉ cần tra cứu, chèn và xóa — trường hợp trung bình O(1) vượt trội O(log n) cho các bộ sưu tập lớn.

Sử dụngmap/set khi bạn cần:
- Sắp xếp thứ tự lặp lại
- Truy vấn phạm vi (lower_bound,upper_bound,equal_range)
- Đảm bảo trường hợp xấu nhất là O(log n) (không có xung đột băm bệnh lý)

Đối với các bộ sưu tập nhỏ (ít hơn ~100 phần tử),std::vectorđược sắp xếp vớistd::lower_boundcó thể hoạt động tốt hơn cả do vị trí bộ đệm.

Điểm chính
  • std::mapvàstd::setsử dụng cây đỏ đen - thao tác O(log n) với thứ tự được sắp xếp
  • std::unordered_mapvàstd::unordered_setsử dụng bảng băm - trung bình O(1) nhưng yêu cầu hàm băm
  • Sử dụng các liên kết có cấu trúc C++17 (auto& [key, val]) để lặp lại bản đồ rõ ràng
  • operator[]on maps silently inserts default values — usefind()orcontains()for safe lookup
  • Custom types as keys requireoperator<(ordered) or a hash function +operator==(unordered)
  • try_emplace(C++17) tránh xây dựng giá trị nếu khóa đã tồn tại

Câu đố — Kiểm tra kiến thức

(15 XP)

1. What is the average time complexity of lookup in`std::unordered_map`?

2. Điều gì xảy ra khi bạn sử dụng`operator[]`với khóa không tồn tại trong`std::map`?

3. Bạn nên chọn vùng chứa nào nếu cần truy vấn phạm vi VÀ lặp lại được sắp xếp (low_bound/upper_bound)?