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

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ề set
và map
, 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): multiset
và multimap
.
Nếu bạn đã nắm vững set
và map
, thì việc tiếp cận multiset
và multimap
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 set
và map
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()
và 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ằngvalue
.find(value)
trả về một iterator trỏ đến vị trí của bản sao đầu tiên củavalue
trongmultiset
. 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ử trongmultiset
có giá trị bằngvalue
. 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()
và 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ơnvalue
.upper_bound(value)
: Trả về iterator đến phần tử đầu tiên lớn hơnvalue
.equal_range(value)
: Trả về mộtpair
chứa hai iterator: iterator đầu tiên là kết quả củalower_bound(value)
, và iterator thứ hai là kết quả củaupper_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ằngvalue
.
#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_bound
và upper_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
, multimap
có find()
và 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 trongmultimap
có key bằngkey
. 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
, multimap
có lower_bound()
, upper_bound()
và 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_bound
và upper_bound
hoạt động tương tự multiset
nhưng áp dụng cho key.
3. Tóm tắt và Ứng dụng
multiset
và multimap
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 set
và map
, 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()
và 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 multiset
và multimap
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