Bài 20.3: Bài tập thực hành Multiset và Multimap trong C++

Chào mừng trở lại với series C++ của FullhouseDev! Sau khi đã khám phá sâu về setmap, hôm nay chúng ta sẽ tiếp tục hành trình với hai container cực kỳ hữu ích khác trong Thư viện Chuẩn C++ (STL): multisetmultimap.

Nếu bạn đã nắm vững setmap, thì việc tiếp cận multisetmultimap sẽ rất dễ dàng. Điểm khác biệt lớn nhất và cũng là sức mạnh của hai container này nằm ở khả năng lưu trữ các phần tử hoặc key trùng lặp. Điều này mở ra nhiều ứng dụng thú vị trong thực tế mà các phiên bản setmap không thể đáp ứng.

Hãy cùng đi sâu vào từng loại container một nhé!

1. multiset: Tập hợp có thứ tự và cho phép trùng lặp

multiset là một container kết hợp (associative container) lưu trữ các phần tử theo thứ tự được sắp xếp. Giống như set, thứ tự này mặc định là tăng dần dựa trên toán tử < của kiểu dữ liệu, hoặc bạn có thể cung cấp một hàm so sánh tùy chỉnh. Tuy nhiên, điểm làm nên sự khác biệt của multiset là nó cho phép lưu trữ nhiều bản sao của cùng một giá trị.

Các đặc điểm chính của multiset:

  • Có thứ tự: Các phần tử luôn được sắp xếp theo thứ tự.
  • Cho phép trùng lặp: Cùng một giá trị có thể xuất hiện nhiều lần.
  • Truy cập nhanh: Các thao tác tìm kiếm, chèn, xóa có độ phức tạp trung bình là logarithmic (O(log n)), giống như set.
  • Tự quản lý bộ nhớ: Khi bạn chèn hoặc xóa phần tử, multiset sẽ tự động cấp phát/giải phóng bộ nhớ cần thiết.

Khi nào nên dùng multiset? Khi bạn cần lưu trữ một tập hợp các giá trị có thứ tự và bạn cần theo dõi số lượng lần xuất hiện của mỗi giá trị (tức là cho phép trùng lặp).

Các thao tác cơ bản với multiset

Hãy cùng xem một vài ví dụ code để hiểu rõ hơn cách sử dụng multiset.

a) Khởi tạo và chèn phần tử:

Bạn có thể khởi tạo multiset rỗng và sử dụng phương thức insert() để thêm các phần tử.

#include <iostream>
#include <multiset> // Bao gồm header <multiset>

int main() {
    // Khai báo một multiset lưu trữ số nguyên
    multiset<int> myMultiset;

    // Chèn các phần tử
    myMultiset.insert(10);
    myMultiset.insert(30);
    myMultiset.insert(20);
    myMultiset.insert(10); // Chèn giá trị trùng lặp
    myMultiset.insert(30); // Chèn giá trị trùng lặp khác
    myMultiset.insert(20); // Chèn giá trị trùng lặp nữa

    cout << "Cac phan tu trong multiset (theo thu tu):\n";
    // Sử dụng range-based for loop để duyệt multiset
    for (int val : myMultiset) {
        cout << val << " ";
    }
    cout << "\n";

    // Output dự kiến: 10 10 20 20 30 30
    return 0;
}

Giải thích: Trong ví dụ này, chúng ta tạo một multiset chứa các số nguyên. Chúng ta chèn các giá trị, bao gồm cả các giá trị lặp lại như 10, 20, 30. Khi duyệt qua myMultiset, chúng ta thấy rằng tất cả các bản sao của mỗi giá trị đều được giữ lại và được sắp xếp theo thứ tự tăng dần.

b) Tìm kiếm và đếm số lần xuất hiện:

multiset cung cấp các phương thức find()count() để tìm kiếm và đếm số lần xuất hiện của một giá trị.

#include <iostream>
#include <multiset>

int main() {
    multiset<int> myMultiset = {10, 20, 10, 30, 20, 10};

    // Đếm số lần xuất hiện của một giá trị
    int valueToCount = 10;
    int count = myMultiset.count(valueToCount);
    cout << "So lan xuat hien cua " << valueToCount << ": " << count << "\n"; // Output: 3

    valueToCount = 25;
    count = myMultiset.count(valueToCount);
    cout << "So lan xuat hien cua " << valueToCount << ": " << count << "\n"; // Output: 0

    // Tìm kiếm (find tra ve iterator toi vi tri dau tien)
    int valueToFind = 20;
    auto it = myMultiset.find(valueToFind);

    if (it != myMultiset.end()) {
        cout << "Tim thay phan tu " << valueToFind << " (lan dau tien): " << *it << "\n"; // Output: 20
    } else {
        cout << "Khong tim thay phan tu " << valueToFind << ".\n";
    }

    valueToFind = 99;
    it = myMultiset.find(valueToFind);
     if (it != myMultiset.end()) {
        cout << "Tim thay phan tu " << valueToFind << ": " << *it << "\n";
    } else {
        cout << "Khong tim thay phan tu " << valueToFind << ".\n"; // Output: Khong tim thay...
    }

    return 0;
}

Giải thích:

  • count(value) trả về tổng số phần tử có giá trị bằng value.
  • find(value) trả về một iterator trỏ đến vị trí của bản sao đầu tiên của value trong multiset. Nếu không tìm thấy, nó trả về myMultiset.end().

c) Xóa phần tử:

Phương thức erase() trong multiset có hai dạng chính: xóa tất cả các bản sao của một giá trị hoặc xóa một phần tử tại vị trí được chỉ định bởi iterator.

#include <iostream>
#include <multiset>

int main() {
    multiset<int> myMultiset = {10, 20, 10, 30, 20, 10}; // Ban đầu: 10 10 10 20 20 30

    cout << "Multiset ban dau: ";
    for (int val : myMultiset) cout << val << " "; cout << "\n";

    // Xóa tat ca cac phan tu co gia tri 10
    size_t num_erased = myMultiset.erase(10); // Tra ve so luong phan tu bi xoa
    cout << "So phan tu 10 da bi xoa: " << num_erased << "\n"; // Output: 3

    cout << "Sau khi xoa tat ca 10: ";
    for (int val : myMultiset) cout << val << " "; cout << "\n"; // Output: 20 20 30

    // Chen lai 20 de minh hoa xoa bang iterator
    myMultiset.insert(20); // Multiset bay gio: 20 20 20 30

    cout << "Multiset sau khi chen lai 20: ";
    for (int val : myMultiset) cout << val << " "; cout << "\n"; // Output: 20 20 20 30

    // Xoa mot phan tu bang iterator (xoa ban sao dau tien cua 20)
    auto it_to_erase = myMultiset.find(20);
    if (it_to_erase != myMultiset.end()) {
        myMultiset.erase(it_to_erase);
    }

    cout << "Sau khi xoa mot phan tu 20 bang iterator: ";
    for (int val : myMultiset) cout << val << " "; cout << "\n"; // Output: 20 20 30 (mot trong ba so 20 da bi xoa)

    return 0;
}

Giải thích:

  • erase(value) sẽ xóa tất cả các phần tử trong multiset có giá trị bằng value. Nó trả về số lượng phần tử đã bị xóa.
  • erase(iterator) sẽ xóa duy nhất một phần tử mà iterator đó đang trỏ tới. Nếu có nhiều bản sao của cùng một giá trị, chỉ bản sao tại vị trí iterator sẽ bị xóa.

d) Tìm kiếm theo khoảng (Range Search):

Với các container có thứ tự và cho phép trùng lặp như multiset, việc tìm kiếm tất cả các phần tử nằm trong một khoảng giá trị nhất định là rất phổ biến. multiset cung cấp các phương thức lower_bound(), upper_bound()equal_range() để thực hiện điều này một cách hiệu quả.

  • lower_bound(value): Trả về iterator đến phần tử đầu tiên không nhỏ hơn value.
  • upper_bound(value): Trả về iterator đến phần tử đầu tiên lớn hơn value.
  • equal_range(value): Trả về một pair chứa hai iterator: iterator đầu tiên là kết quả của lower_bound(value), và iterator thứ hai là kết quả của upper_bound(value). Cặp iterator này định nghĩa một khoảng chứa tất cả các phần tử có giá trị bằng value.
#include <iostream>
#include <multiset>
#include <algorithm> // Doi khi can cho cac thu vien khac, nhung voi iterators thi ko bat buoc

int main() {
    multiset<int> myMultiset = {10, 20, 10, 30, 20, 10, 40, 20}; // 10 10 10 20 20 20 30 40

    cout << "Multiset: ";
    for (int val : myMultiset) cout << val << " "; cout << "\n";

    // Tìm kiếm tất cả các phần tử có giá trị bằng 20
    auto range = myMultiset.equal_range(20);

    cout << "Cac phan tu co gia tri 20:\n";
    for (auto it = range.first; it != range.second; ++it) {
        cout << *it << " ";
    }
    cout << "\n"; // Output: 20 20 20

    // Tìm kiếm các phần tử trong khoảng [20, 40)
    auto lower = myMultiset.lower_bound(20); // Iterator tới 20 đầu tiên
    auto upper = myMultiset.upper_bound(30); // Iterator tới phần tử đầu tiên > 30 (tức là 40)

    cout << "Cac phan tu trong khoang [lower_bound(20), upper_bound(30)):\n";
    for (auto it = lower; it != upper; ++it) {
        cout << *it << " ";
    }
    cout << "\n"; // Output: 20 20 20 30

    return 0;
}

Giải thích: equal_range(value) rất tiện lợi khi bạn muốn xử lý tất cả các bản sao của một giá trị cụ thể. Nó trả về một cặp iterator, xác định chính xác đoạn trong multiset chứa các phần tử có giá trị đó. Bạn có thể duyệt qua đoạn này bằng một vòng lặp từ range.first đến range.second. Tương tự, lower_boundupper_bound giúp bạn định nghĩa các khoảng tùy ý.

2. multimap: Ánh xạ có thứ tự theo key và cho phép trùng lặp key

multimap là một container kết hợp khác lưu trữ các cặp key-value. Giống như map, các cặp này được sắp xếp dựa trên key. Tuy nhiên, điểm làm cho multimap khác biệt là nó cho phép nhiều cặp key-value khác nhau có cùng một key.

Các đặc điểm chính của multimap:

  • Lưu trữ cặp Key-Value: Mỗi phần tử là một pair<const Key, Value>.
  • Có thứ tự theo Key: Các cặp được sắp xếp theo thứ tự tăng dần của key (hoặc theo hàm so sánh tùy chỉnh).
  • Cho phép trùng lặp Key: Cùng một key có thể xuất hiện nhiều lần, mỗi lần map tới một value khác nhau (hoặc giống nhau).
  • Truy cập nhanh: Các thao tác tìm kiếm, chèn, xóa (bằng iterator hoặc range) có độ phức tạp trung bình là logarithmic (O(log n)). Xóa bằng key có thể là O(log n + k) trong đó k là số lượng phần tử bị xóa.
  • Tự quản lý bộ nhớ.

Khi nào nên dùng multimap? Khi bạn cần lưu trữ các mối quan hệ one-to-many (một key liên kết với nhiều giá trị) hoặc simply khi bạn cần một ánh xạ có thứ tự và cho phép các key trùng lặp. Ví dụ: lưu trữ tất cả các đơn hàng của một khách hàng (ID khách hàng là key, thông tin đơn hàng là value), lưu trữ các điểm số khác nhau mà một học sinh đạt được trong các bài kiểm tra khác nhau (tên học sinh là key, điểm số là value).

Các thao tác cơ bản với multimap

Hãy xem các ví dụ code cho multimap.

a) Khởi tạo và chèn cặp Key-Value:

Bạn sử dụng phương thức insert() để thêm các cặp key-value. Lưu ý rằng multimap không hỗ trợ truy cập bằng toán tử [] (myMap[key]) như map vì một key có thể có nhiều value, nên việc trả về một reference duy nhất là không rõ ràng.

#include <iostream>
#include <multimap>
#include <string> // Sử dụng kiểu string cho key

int main() {
    // Khai báo một multimap lưu trữ key là string, value là int
    multimap<string, int> studentScores;

    // Chèn các cặp key-value
    studentScores.insert({"Alice", 95});
    studentScores.insert({"Bob", 80});
    studentScores.insert({"Alice", 100}); // Key "Alice" trùng lặp
    studentScores.insert({"Charlie", 90});
    studentScores.insert({"Bob", 85});   // Key "Bob" trùng lặp khác
    studentScores.insert({"Alice", 98}); // Thêm một bản sao khác cho "Alice"

    cout << "Cac cap key-value trong multimap (sap xep theo key):\n";
    // Sử dụng range-based for loop để duyệt multimap
    for (const auto& pair : studentScores) {
        cout << pair.first << ": " << pair.second << "\n";
    }

    /* Output dự kiến:
    Alice: 95
    Alice: 100
    Alice: 98
    Bob: 80
    Bob: 85
    Charlie: 90
    (Thứ tự các value với cùng một key có thể phụ thuộc vào thứ tự chèn hoặc trình biên dịch, nhưng các cặp sẽ đứng cạnh nhau và key được sắp xếp.)
    */
    return 0;
}

Giải thích: Chúng ta chèn các cặp {key, value} vào multimap. Các key "Alice" và "Bob" xuất hiện nhiều lần. multimap tự động sắp xếp các cặp dựa trên key và giữ lại tất cả các cặp, kể cả khi key bị trùng lặp. Khi duyệt, các cặp có cùng key sẽ nằm liền kề nhau, và các nhóm key được sắp xếp theo thứ tự.

b) Tìm kiếm và đếm số lần xuất hiện Key:

Giống như multiset, multimapfind()count(). count() trả về số lượng cặp có key nhất định. find() trả về iterator tới cặp đầu tiên có key đó.

#include <iostream>
#include <multimap>
#include <string>

int main() {
    multimap<string, int> studentScores = {
        {"Alice", 95}, {"Bob", 80}, {"Alice", 100}, {"Charlie", 90}, {"Bob", 85}, {"Alice", 98}
    };

    // Đếm số lần xuất hiện của một key
    string keyToCount = "Alice";
    size_t count = studentScores.count(keyToCount);
    cout << "So lan xuat hien cua key \"" << keyToCount << "\": " << count << "\n"; // Output: 3

    keyToCount = "David";
    count = studentScores.count(keyToCount);
    cout << "So lan xuat hien cua key \"" << keyToCount << "\": " << count << "\n"; // Output: 0

    // Tìm kiếm (find tra ve iterator toi cap dau tien voi key do)
    string keyToFind = "Bob";
    auto it = studentScores.find(keyToFind);

    if (it != studentScores.end()) {
        cout << "Tim thay cap voi key \"" << keyToFind << "\" (lan dau tien): "
                  << it->first << ": " << it->second << "\n"; // Output: Bob: 80
    } else {
        cout << "Khong tim thay key \"" << keyToFind << "\".\n";
    }

    keyToFind = "David";
    it = studentScores.find(keyToFind);
     if (it != studentScores.end()) {
        cout << "Tim thay cap voi key \"" << keyToFind << "\": "
                  << it->first << ": " << it->second << "\n";
    } else {
        cout << "Khong tim thay key \"" << keyToFind << "\".\n"; // Output: Khong tim thay...
    }

    return 0;
}

Giải thích: count(key) trả về số lượng cặp trong multimap có key bằng key. find(key) trả về iterator trỏ đến cặp đầu tiên tìm thấy có key đó.

c) Xóa cặp Key-Value:

Phương thức erase() trong multimap cũng cho phép xóa tất cả các cặp với một key nhất định hoặc xóa một cặp tại vị trí iterator.

#include <iostream>
#include <multimap>
#include <string>

int main() {
    multimap<string, int> studentScores = {
        {"Alice", 95}, {"Bob", 80}, {"Alice", 100}, {"Charlie", 90}, {"Bob", 85}, {"Alice", 98}
    };
    // Ban đầu (sắp xếp): Alice: 95, Alice: 98, Alice: 100, Bob: 80, Bob: 85, Charlie: 90

    cout << "Multimap ban dau:\n";
    for (const auto& pair : studentScores) cout << pair.first << ": " << pair.second << "\n";

    // Xóa tat ca cac cap co key "Alice"
    size_t num_erased = studentScores.erase("Alice"); // Tra ve so luong cap bi xoa
    cout << "\nSo cap voi key \"Alice\" da bi xoa: " << num_erased << "\n"; // Output: 3

    cout << "Sau khi xoa tat ca key \"Alice\":\n";
    for (const auto& pair : studentScores) cout << pair.first << ": " << pair.second << "\n";
    /* Output:
    Bob: 80
    Bob: 85
    Charlie: 90
    */

    // Chen lai Bob de minh hoa xoa bang iterator
    studentScores.insert({"Bob", 80}); // Multimap bay gio (sap xep): Bob: 80, Bob: 80, Bob: 85, Charlie: 90

    cout << "\nMultimap sau khi chen lai Bob:\n";
     for (const auto& pair : studentScores) cout << pair.first << ": " << pair.second << "\n";

    // Xoa mot cap bang iterator (xoa cap dau tien voi key "Bob")
    auto it_to_erase = studentScores.find("Bob");
    if (it_to_erase != studentScores.end()) {
        studentScores.erase(it_to_erase);
    }

    cout << "Sau khi xoa mot cap \"Bob\" bang iterator:\n";
    for (const auto& pair : studentScores) cout << pair.first << ": " << pair.second << "\n";
    /* Output:
    Bob: 80
    Bob: 85
    Charlie: 90
    (Mot trong hai cap Bob: 80 da bi xoa)
    */

    return 0;
}

Giải thích:

  • erase(key) sẽ xóa tất cả các cặp trong multimap có key bằng key. Nó trả về số lượng cặp đã bị xóa.
  • erase(iterator) sẽ xóa duy nhất một cặp mà iterator đó đang trỏ tới.

d) Tìm kiếm theo khoảng Key (Range Search):

Tương tự như multiset, multimaplower_bound(), upper_bound()equal_range() để tìm kiếm các cặp dựa trên key. equal_range(key) trả về một pair chứa hai iterator định nghĩa khoảng chứa tất cả các cặp có key bằng key.

#include <iostream>
#include <multimap>
#include <string>
#include <algorithm>

int main() {
    multimap<string, int> studentScores = {
        {"Alice", 95}, {"Bob", 80}, {"Alice", 100}, {"Charlie", 90}, {"Bob", 85}, {"Alice", 98}
    };
    // Sap xep: Alice: 95, Alice: 98, Alice: 100, Bob: 80, Bob: 85, Charlie: 90

    cout << "Multimap:\n";
     for (const auto& pair : studentScores) cout << pair.first << ": " << pair.second << "\n";

    // Tìm kiếm tất cả các cặp có key "Alice"
    auto range = studentScores.equal_range("Alice");

    cout << "\nCac cap voi key \"Alice\":\n";
    for (auto it = range.first; it != range.second; ++it) {
        cout << it->first << ": " << it->second << "\n";
    }
    /* Output:
    Alice: 95
    Alice: 98
    Alice: 100
    */

    // Tìm kiếm tất cả các cặp có key "Bob"
    auto bob_range = studentScores.equal_range("Bob");

    cout << "\nCac cap voi key \"Bob\":\n";
    for (auto it = bob_range.first; it != bob_range.second; ++it) {
        cout << it->first << ": " << it->second << "\n";
    }
    /* Output:
    Bob: 80
    Bob: 85
    */


    // Tìm kiếm các cặp trong khoảng key ['B', 'C')
    // upper_bound('C') sẽ trỏ đến phần tử đầu tiên >= 'C'
    auto lower_key = studentScores.lower_bound("B");
    auto upper_key = studentScores.lower_bound("C"); // Hoac studentScores.upper_bound("B") neu chi muon key < 'C'

    cout << "\nCac cap voi key trong khoang ['B', 'C'):\n";
     for (auto it = lower_key; it != upper_key; ++it) {
        cout << it->first << ": " << it->second << "\n";
    }
    /* Output:
    Bob: 80
    Bob: 85
    */


    return 0;
}

Giải thích: equal_range(key) trả về một cặp iterator xác định đoạn trong multimap chứa tất cả các cặp có key bằng key. lower_boundupper_bound hoạt động tương tự multiset nhưng áp dụng cho key.

3. Tóm tắt và Ứng dụng

multisetmultimap là những công cụ mạnh mẽ trong STL khi bạn cần lưu trữ dữ liệu có thứ tự và quan trọng là cho phép các phần tử hoặc key bị trùng lặp.

  • Sử dụng multiset khi bạn cần một bộ sưu tập các giá trị có thứ tự và bạn cần quản lý các bản sao của cùng một giá trị (ví dụ: theo dõi tần suất xuất hiện của các mục).
  • Sử dụng multimap khi bạn cần một ánh xạ có thứ tự theo key, trong đó một key có thể liên kết với nhiều giá trị khác nhau (ví dụ: lịch sử giao dịch của khách hàng, điểm số nhiều lần thi của sinh viên).

So với setmap, chúng linh hoạt hơn ở khía cạnh chấp nhận trùng lặp, nhưng đổi lại bạn cần cẩn thận hơn khi thao tác (ví dụ: erase(value) trong multiset xóa tất cả, find() chỉ trả về iterator đầu tiên). Các phương thức như count()equal_range() trở nên đặc biệt hữu ích để làm việc với các phần tử/key trùng lặp này.

Nắm vững multisetmultimap sẽ giúp bạn giải quyết hiệu quả nhiều bài toán thực tế trong lập trình C++.

Comments

There are no comments at the moment.