Bài 18.2: Multiset và Unordered_set trong C++

Bài 18.2: Multiset và Unordered_set trong C++
Chào mừng trở lại với chuỗi bài blog về C++! Hôm nay, chúng ta sẽ "lặn sâu" vào thế giới của hai cấu trúc dữ liệu cực kỳ hữu ích trong Thư viện Chuẩn C++ (STL): multiset
và unordered_set
. Nếu bạn đã quen thuộc với set
, thì multiset
và unordered_set
là những "anh em" của nó, mỗi loại mang một đặc tính riêng biệt để giải quyết các bài toán cụ thể.
Chúng ta sẽ tìm hiểu xem chúng là gì, cách sử dụng chúng, và quan trọng nhất là sự khác biệt cốt lõi giúp bạn đưa ra lựa chọn phù hợp cho từng tình huống lập trình.
multiset
: Khi thứ tự quan trọng và trùng lặp được chấp nhận
multiset
là một container (vùng chứa) trong STL, lưu trữ các phần tử theo một thứ tự được sắp xếp. Điểm đặc biệt làm nên tên tuổi của nó (multi) chính là khả năng cho phép các phần tử trùng lặp.
Nghĩ về nó như một tập hợp các số mà bạn luôn muốn giữ chúng theo thứ tự từ nhỏ đến lớn, và nếu có nhiều số giống nhau, bạn muốn giữ lại tất cả chúng.
Về mặt kỹ thuật, multiset
thường được triển khai dựa trên cấu trúc cây cân bằng (như Red-Black Tree), đảm bảo các thao tác cơ bản như thêm, xóa, tìm kiếm có độ phức tạp thời gian là O(log N), trong đó N là số lượng phần tử trong multiset.
Các thao tác cơ bản với multiset
Hãy cùng xem một số ví dụ minh họa cách sử dụng multiset
.
1. Khởi tạo và thêm phần tử (insert
)
#include <iostream>
#include <multiset>
#include <string>
int main() {
// Khởi tạo một multiset rỗng chứa số nguyên
multiset<int> myMultiset;
// Thêm các phần tử
myMultiset.insert(10);
myMultiset.insert(5);
myMultiset.insert(15);
myMultiset.insert(5); // Chèn thêm một giá trị 5 (trùng lặp)
myMultiset.insert(10); // Chèn thêm một giá trị 10 (trùng lặp)
// In ra kích thước
cout << "Kich thuoc myMultiset: " << myMultiset.size() << endl; // Output: 5
// Duyệt qua các phần tử (sẽ theo thứ tự tăng dần)
cout << "Cac phan tu trong myMultiset: ";
for (int num : myMultiset) {
cout << num << " "; // Output: 5 5 10 10 15
}
cout << endl;
return 0;
}
Giải thích: Đoạn code này tạo một multiset<int>
, chèn các số vào. Bạn có thể thấy rằng các giá trị 5 và 10 được thêm nhiều lần và đều được lưu giữ. Khi duyệt qua myMultiset
, các phần tử được in ra theo thứ tự tăng dần, bất kể thứ tự chèn vào ban đầu.
2. Tìm kiếm phần tử (find
, count
)
Bạn có thể tìm kiếm một phần tử cụ thể và đếm số lần xuất hiện của nó.
#include <iostream>
#include <multiset>
int main() {
multiset<int> numbers = {10, 5, 15, 5, 20, 10}; // {5, 5, 10, 10, 15, 20}
// Tìm kiếm giá trị 10
auto it_find = numbers.find(10);
if (it_find != numbers.end()) {
cout << "Tim thay so 10 trong multiset." << endl; // Tìm thấy
// Lưu ý: find() tra ve iterator toi LAN XUAT HIEN DAU TIEN theo thu tu sap xep
cout << "Gia tri tai iterator find: " << *it_find << endl;
} else {
cout << "Khong tim thay so 10." << endl;
}
// Tìm kiếm giá trị 100 (không tồn tại)
it_find = numbers.find(100);
if (it_find != numbers.end()) {
cout << "Tim thay so 100 trong multiset." << endl;
} else {
cout << "Khong tim thay so 100." << endl; // Không tìm thấy
}
// Dem so lan xuat hien cua mot gia tri
cout << "So lan xuat hien cua 5: " << numbers.count(5) << endl; // Output: 2
cout << "So lan xuat hien cua 10: " << numbers.count(10) << endl; // Output: 2
cout << "So lan xuat hien cua 20: " << numbers.count(20) << endl; // Output: 1
cout << "So lan xuat hien cua 100: " << numbers.count(100) << endl; // Output: 0
return 0;
}
Giải thích: Phương thức find(value)
trả về một iterator trỏ đến lần xuất hiện đầu tiên của value
trong multiset (theo thứ tự đã sắp xếp). Nếu không tìm thấy, nó trả về numbers.end()
. Phương thức count(value)
thì cực kỳ tiện lợi, nó trả về tổng số lần value
xuất hiện trong multiset.
3. Xóa phần tử (erase
)
Xóa phần tử trong multiset
có thể hơi phức tạp hơn so với set
vì có thể có trùng lặp.
#include <iostream>
#include <multiset>
int main() {
multiset<int> numbers = {10, 5, 15, 5, 20, 10}; // {5, 5, 10, 10, 15, 20}
cout << "Multiset ban dau: ";
for (int num : numbers) cout << num << " ";
cout << endl;
// Xoa mot LAN xuat hien cua gia tri 5 bang iterator
auto it_erase = numbers.find(5); // Tìm lần xuất hiện đầu tiên
if (it_erase != numbers.end()) {
numbers.erase(it_erase);
}
cout << "Sau khi xoa mot so 5: ";
for (int num : numbers) cout << num << " "; // Output: 5 10 10 15 20
cout << endl;
// Xoa TAT CA cac lan xuat hien cua gia tri 10 bang gia tri
// Phuong thuc erase(value) tra ve so phan tu bi xoa
size_t removed_count = numbers.erase(10);
cout << "So phan tu bi xoa (gia tri 10): " << removed_count << endl; // Output: 2
cout << "Sau khi xoa tat ca so 10: ";
for (int num : numbers) cout << num << " "; // Output: 5 15 20
cout << endl;
// Xoa tat ca phan tu trong mot khoang (vi du: tat ca phan tu > 10)
// lower_bound(value) tra ve iterator toi phan tu >= value dau tien
// upper_bound(value) tra ve iterator toi phan tu > value dau tien
numbers.erase(numbers.upper_bound(15), numbers.end()); // Xoa 20
cout << "Sau khi xoa phan tu > 15: ";
for (int num : numbers) cout << num << " "; // Output: 5 15
cout << endl;
return 0;
}
Giải thích:
erase(iterator)
: Xóa phần tử mà iterator đó đang trỏ tới. Nếu bạn chỉ muốn xóa một trong số các phần tử trùng lặp, bạn cần tìm đúng iterator của phần tử đó (thường là dùngfind
hoặc duyệt).erase(value)
: Xóa tất cả các lần xuất hiện củavalue
trong multiset và trả về số lượng phần tử đã bị xóa.erase(first_iterator, last_iterator)
: Xóa tất cả các phần tử trong phạm vi từfirst_iterator
đếnlast_iterator
(không bao gồmlast_iterator
). Kết hợp vớilower_bound
vàupper_bound
rất hữu ích.
unordered_set
: Khi tốc độ là ưu tiên và chỉ cần duy nhất
Trái ngược với multiset
(và set
), unordered_set
là một container lưu trữ các phần tử theo một thứ tự không được đảm bảo (unordered). Nó dựa trên cấu trúc bảng băm (hash table).
Điểm mấu chốt của unordered_set
là:
- Nó không cho phép các phần tử trùng lặp.
- Các thao tác cơ bản như thêm, xóa, tìm kiếm có độ phức tạp thời gian trung bình là O(1) (thời gian hằng số). Trong trường hợp xấu nhất (xảy ra va chạm băm nhiều), độ phức tạp có thể lên tới O(N), nhưng điều này khá hiếm với hàm băm tốt.
Nghĩ về nó như một cái "túi" chứa các vật duy nhất. Bạn bỏ vật vào, chúng nằm lộn xộn trong túi, nhưng bạn có thể nhanh chóng kiểm tra xem một vật cụ thể có trong túi hay không.
Các thao tác cơ bản với unordered_set
Hãy xem unordered_set
hoạt động như thế nào.
1. Khởi tạo và thêm phần tử (insert
)
#include <iostream>
#include <unordered_set>
#include <string>
int main() {
// Khởi tạo một unordered_set rỗng chứa số nguyên
unordered_set<int> myUnorderedSet;
// Thêm các phần tử
myUnorderedSet.insert(10);
myUnorderedSet.insert(5);
myUnorderedSet.insert(15);
auto result_insert_5 = myUnorderedSet.insert(5); // Thêm giá trị 5 (đã có)
auto result_insert_10 = myUnorderedSet.insert(10); // Thêm giá trị 10 (đã có)
// Kết quả insert tra ve pair<iterator, bool>, bool = true neu chen thanh cong, false neu phan tu da ton tai
cout << "Insert 5 lan 2 " << (result_insert_5.second ? "thanh cong" : "that bai") << endl; // Output: that bai
cout << "Insert 10 lan 2 " << (result_insert_10.second ? "thanh cong" : "that bai") << endl; // Output: that bai
// In ra kích thước
cout << "Kich thuoc myUnorderedSet: " << myUnorderedSet.size() << endl; // Output: 3
// Duyệt qua các phần tử (thứ tự không đảm bảo)
cout << "Cac phan tu trong myUnorderedSet: ";
for (int num : myUnorderedSet) {
cout << num << " "; // Output order is unpredictable (e.g., 15 5 10)
}
cout << endl;
return 0;
}
Giải thích: Khi bạn cố gắng chèn một phần tử đã tồn tại vào unordered_set
, thao tác insert
sẽ trả về một pair
với phần tử second
là false
, và phần tử đó sẽ không được thêm vào lại. Kích thước của tập hợp vẫn là 3 (5
, 10
, 15
). Khi duyệt, thứ tự in ra có thể khác nhau mỗi lần chạy.
2. Tìm kiếm phần tử (find
, count
)
Tìm kiếm trong unordered_set
rất nhanh, trung bình là O(1).
#include <iostream>
#include <unordered_set>
int main() {
unordered_set<int> numbers = {10, 5, 15, 20};
// Tìm kiếm giá trị 10
auto it_find = numbers.find(10);
if (it_find != numbers.end()) {
cout << "Tim thay so 10 trong unordered_set." << endl; // Tim thay
} else {
cout << "Khong tim thay so 10." << endl;
}
// Tìm kiếm giá trị 100 (không tồn tại)
it_find = numbers.find(100);
if (it_find != numbers.end()) {
cout << "Tim thay so 100 trong unordered_set." << endl;
} else {
cout << "Khong tim thay so 100." << endl; // Khong tim thay
}
// Dem so lan xuat hien cua mot gia tri
// Luu y: Voi unordered_set (khong co trung lap), count chi co the tra ve 0 hoac 1
cout << "So lan xuat hien cua 5: " << numbers.count(5) << endl; // Output: 1
cout << "So lan xuat hien cua 100: " << numbers.count(100) << endl; // Output: 0
return 0;
}
Giải thích: Phương thức find(value)
hoạt động tương tự như trong multiset
và set
, trả về iterator trỏ đến phần tử nếu tìm thấy, hoặc end()
nếu không. Phương thức count(value)
trả về số lần xuất hiện. Tuy nhiên, vì unordered_set
không cho phép trùng lặp, count
luôn chỉ trả về 0 (nếu không tìm thấy) hoặc 1 (nếu tìm thấy).
3. Xóa phần tử (erase
)
Xóa phần tử trong unordered_set
cũng rất nhanh.
#include <iostream>
#include <unordered_set>
int main() {
unordered_set<int> numbers = {10, 5, 15, 20};
cout << "Unordered_set ban dau: ";
for (int num : numbers) cout << num << " ";
cout << endl;
// Xoa phan tu co gia tri 15 bang gia tri
// Phuong thuc erase(value) tra ve so phan tu bi xoa (0 hoac 1)
size_t removed_count = numbers.erase(15);
cout << "So phan tu bi xoa (gia tri 15): " << removed_count << endl; // Output: 1
cout << "Sau khi xoa 15: ";
for (int num : numbers) cout << num << " "; // Thu tu co the thay doi
cout << endl;
// Xoa phan tu khong ton tai
removed_count = numbers.erase(100);
cout << "So phan tu bi xoa (gia tri 100): " << removed_count << endl; // Output: 0
// Xoa phan tu bang iterator
auto it_erase = numbers.find(10);
if(it_erase != numbers.end()) {
numbers.erase(it_erase);
}
cout << "Sau khi xoa 10: ";
for (int num : numbers) cout << num << " "; // Thu tu co the thay doi
cout << endl;
return 0;
}
Giải thích: Phương thức erase(value)
sẽ tìm và xóa phần tử có giá trị đó (nếu tồn tại) và trả về 1 nếu xóa thành công, 0 nếu không tìm thấy. Phương thức erase(iterator)
xóa phần tử mà iterator đó trỏ tới. Cả hai đều có độ phức tạp trung bình O(1).
Multiset vs. Unordered_set: Lựa chọn nào cho bài toán của bạn?
Sau khi tìm hiểu về cả hai, đây là bảng tóm tắt sự khác biệt chính để giúp bạn quyết định nên dùng container nào:
Đặc điểm | multiset |
unordered_set |
---|---|---|
Trùng lặp | Cho phép các phần tử trùng lặp | Không cho phép các phần tử trùng lặp |
Thứ tự | Các phần tử được sắp xếp theo thứ tự (tăng dần theo mặc định) | Thứ tự các phần tử không được đảm bảo (dựa vào hàm băm) |
Cấu trúc dữ liệu | Thường là cây cân bằng (như Red-Black Tree) | Thường là bảng băm (Hash Table) |
Độ phức tạp TB | O(log N) cho insert, find, erase, count | O(1) cho insert, find, erase, count (count trả về 0 hoặc 1) |
Độ phức tạp WW | O(log N) | O(N) (trong trường hợp va chạm băm nghiêm trọng) |
Khi nào nên sử dụng multiset
?
- Khi bạn cần lưu trữ một tập hợp mà thứ tự của các phần tử là quan trọng.
- Khi bạn cần lưu trữ các phần tử có thể trùng lặp.
- Khi bạn cần thực hiện các thao tác như tìm kiếm phạm vi (
lower_bound
,upper_bound
). - Khi độ phức tạp O(log N) là chấp nhận được.
Ví dụ: Theo dõi danh sách điểm thi của học sinh (có thể có điểm trùng nhau) và muốn xem danh sách điểm theo thứ tự tăng dần.
Khi nào nên sử dụng unordered_set
?
- Khi bạn chỉ cần lưu trữ các phần tử duy nhất.
- Khi thứ tự của các phần tử không quan trọng.
- Khi bạn ưu tiên tốc độ truy cập trung bình O(1) cho các thao tác thêm, xóa, tìm kiếm.
Ví dụ: Lưu trữ danh sách các từ duy nhất xuất hiện trong một văn bản, hoặc kiểm tra sự tồn tại nhanh chóng của một phần tử trong một tập lớn.
Comments