Bài 19.5: Bài tập thực hành Map trong C++

Bài 19.5: Bài tập thực hành Map trong C++
Chào mừng trở lại với chuỗi bài viết hấp dẫn về C++! Sau khi đã cùng nhau tìm hiểu về lý thuyết và cấu trúc của map
- một container cực kỳ mạnh mẽ trong Thư viện Chuẩn C++ (STL) dùng để lưu trữ các cặp key-value (khóa-giá trị), hôm nay chúng ta sẽ cùng nhau bắt tay vào thực hành để thực sự nắm vững cách sử dụng nó trong các tình huống thực tế.
Lý thuyết là quan trọng, nhưng chỉ khi tự mình gõ code và chạy thử, bạn mới thực sự hiểu được sức mạnh và sự linh hoạt của map
. Hãy chuẩn bị trình biên dịch của bạn và chúng ta cùng khám phá nhé!
Nhắc Lại Ngắn Gọn về map
Trước khi đi sâu vào các bài tập, hãy cùng nhắc lại một chút: map
là một associative container lưu trữ các cặp key-value, nơi mỗi key là duy nhất và được dùng để truy cập value tương ứng. Các phần tử trong map
luôn được tự động sắp xếp dựa trên key (theo thứ tự tăng dần mặc định).
Sự tự động sắp xếp này mang lại hiệu quả cao khi tìm kiếm, nhưng đôi khi có thể chậm hơn unordered_map
(sẽ học sau) khi chỉ cần truy cập nhanh mà không quan tâm đến thứ tự. Tuy nhiên, map
vẫn là lựa chọn tuyệt vời cho nhiều bài toán!
Giờ thì, hãy cùng thực hành!
Cùng Thực Hành Sử Dụng map
Chúng ta sẽ đi qua các thao tác cơ bản và phổ biến nhất với map
thông qua các ví dụ code ngắn gọn và dễ hiểu.
Bài Tập 1: Tạo và Thêm Phần Tử vào Map
Một trong những thao tác cơ bản nhất là tạo một map
và thêm các cặp key-value vào đó. Bạn có thể sử dụng toán tử []
hoặc phương thức insert()
.
- Sử dụng toán tử
[]
: Rất tiện lợi, nếu key chưa tồn tại, nó sẽ được thêm vào map cùng với một value được khởi tạo mặc định. Nếu key đã tồn tại, nó sẽ cập nhật giá trị của key đó. - Sử dụng phương thức
insert()
: Phương thức này thêm một cặp key-value mới. Nếu key đã tồn tại, thao tác insert sẽ không thực hiện và trả về thông tin cho biết việc thêm mới không thành công.
Ví dụ Code:
#include <iostream>
#include <map>
#include <string>
int main() {
map<string, int> tuoi;
tuoi["Alice"] = 30;
tuoi["Bob"] = 25;
tuoi["Charlie"] = 35;
tuoi.insert({"David", 28});
tuoi.insert(make_pair("Eve", 22));
auto kq = tuoi.insert({"Bob", 99});
if (!kq.second) {
cout << "*Key 'Bob' da ton tai, insert khong thuc hien." << endl;
}
cout << "\nMap sau khi them cac phan tu:" << endl;
for (const auto& p : tuoi) {
cout << p.first << ": " << p.second << endl;
}
return 0;
}
Output:
*Key 'Bob' da ton tai, insert khong thuc hien.
Map sau khi them cac phan tu:
Alice: 30
Bob: 25
Charlie: 35
David: 28
Eve: 22
Giải thích:
- Chúng ta khai báo
map<string, int> ages;
. Điều này tạo ra một map rỗng, nơi key làstring
và value làint
. ages["Alice"] = 30;
là cách rất trực quan để thêm hoặc cập nhật. Nếu "Alice" chưa có, nó sẽ được thêm vào.ages.insert({"David", 28});
vàages.insert(make_pair("Eve", 22));
là các cách khác sử dụnginsert
.insert
hữu ích khi bạn muốn kiểm tra xem key đó đã tồn tại hay chưa (như trong ví dụ kiểm traresult.second
).- Khi in ra, bạn sẽ thấy các tên được sắp xếp theo thứ tự bảng chữ cái (A, B, C, D, E) nhờ đặc tính của
map
.
Bài Tập 2: Truy Cập và Cập Nhật Giá Trị
Sau khi thêm, việc quan trọng tiếp theo là truy cập hoặc thay đổi giá trị liên kết với một key cụ thể.
- Sử dụng toán tử
[]
: Như đã nói, đây là cách dễ nhất để vừa truy cập vừa có thể cập nhật. Nếu key không tồn tại, nó sẽ tự động thêm key đó với giá trị mặc định (ví dụ: 0 choint
, chuỗi rỗng chostring
). Hãy cẩn thận với hành vi này khi bạn chỉ muốn đọc! - Sử dụng phương thức
at()
: Phương thức này chỉ dùng để truy cập. Nếu key không tồn tại, nó sẽ ném ra một ngoại lệ (out_of_range
), giúp bạn phát hiện lỗi sớm trong quá trình phát triển.
Ví dụ Code:
#include <iostream>
#include <map>
#include <string>
#include <stdexcept>
int main() {
map<string, int> tuoi;
tuoi["Alice"] = 30;
tuoi["Bob"] = 25;
tuoi["Charlie"] = 35;
cout << "Map ban dau:" << endl;
for (const auto& p : tuoi) {
cout << p.first << ": " << p.second << endl;
}
cout << "\nTuoi cua Bob la: " << tuoi["Bob"] << endl;
try {
cout << "Tuoi cua Charlie la (dung at()): " << tuoi.at("Charlie") << endl;
} catch (const out_of_range& e) {
cerr << "Loi: Key khong ton tai: " << e.what() << endl;
}
tuoi["Alice"] = 31;
cout << "\nTuoi cua Alice sau khi cap nhat: " << tuoi["Alice"] << endl;
return 0;
}
Output:
Map ban dau:
Alice: 30
Bob: 25
Charlie: 35
Tuoi cua Bob la: 25
Tuoi cua Charlie la (dung at()): 35
Tuoi cua Alice sau khi cap nhat: 31
Giải thích:
cout << "Tuoi cua Bob la: " << ages["Bob"] << endl;
dùng[]
để lấy giá trị.cout << "Tuoi cua Charlie la (dung at()): " << ages.at("Charlie") << endl;
dùngat()
. Bạn nên dùngat()
khi chắc chắn key tồn tại hoặc khi bạn muốn xử lý trường hợp key không tồn tại bằngtry-catch
.ages["Alice"] = 31;
minh họa cách cập nhật. Vì "Alice" đã tồn tại, giá trị cũ (30) bị ghi đè bởi giá trị mới (31).- Đoạn code bị comment cảnh báo về việc sử dụng
[]
với key không tồn tại - nó sẽ tự động thêm key đó vào map.
Bài Tập 3: Kiểm Tra Sự Tồn Tại Của Key
Trước khi truy cập hoặc xóa một phần tử, bạn thường cần kiểm tra xem key đó có tồn tại trong map hay không để tránh lỗi hoặc ngoại lệ.
- Sử dụng phương thức
count()
: Trả về số lần xuất hiện của key. Vớimap
, key là duy nhất nêncount()
chỉ trả về0
(không tồn tại) hoặc1
(tồn tại). Đây là cách đơn giản nhất để kiểm tra sự tồn tại. - Sử dụng phương thức
find()
: Trả về một iterator trỏ tới phần tử nếu tìm thấy, hoặc trả vềmap.end()
nếu không tìm thấy. Cách này mạnh mẽ hơn vì nếu tìm thấy, bạn có ngay iterator để truy cập key/value hoặc xóa phần tử đó mà không cần tìm lại.
Ví dụ Code:
#include <iostream>
#include <map>
#include <string>
int main() {
map<string, int> tuoi;
tuoi["Alice"] = 31;
tuoi["Bob"] = 25;
string ten1 = "Alice";
if (tuoi.count(ten1)) {
cout << ten1 << " *co ton tai* trong map." << endl;
} else {
cout << ten1 << " *khong ton tai* trong map." << endl;
}
string ten2 = "David";
if (tuoi.count(ten2)) {
cout << ten2 << " *co ton tai* trong map." << endl;
} else {
cout << ten2 << " *khong ton tai* trong map." << endl;
}
string tenTim1 = "Bob";
auto it = tuoi.find(tenTim1);
if (it != tuoi.end()) {
cout << "Tim thay " << tenTim1 << " voi tuoi: " << it->second << endl;
} else {
cout << "Khong tim thay " << tenTim1 << " trong map." << endl;
}
string tenTim2 = "Eve";
it = tuoi.find(tenTim2);
if (it != tuoi.end()) {
cout << "Tim thay " << tenTim2 << " trong map." << endl;
} else {
cout << "Khong tim thay " << tenTim2 << " trong map." << endl;
}
return 0;
}
Output:
Alice *co ton tai* trong map.
David *khong ton tai* trong map.
Tim thay Bob voi tuoi: 25
Khong tim thay Eve trong map.
Giải thích:
ages.count("Alice")
trả về 1 vì "Alice" có trong map.ages.count("David")
trả về 0 vì "David" không có.ages.find("Bob")
trả về một iterator hợp lệ (khácages.end()
). Chúng ta có thể dùngit->second
để truy cập giá trị của "Bob" ngay lập tức.ages.find("Eve")
trả vềages.end()
vì "Eve" không có, cho phép chúng ta xử lý trường hợp không tìm thấy.
Bài Tập 4: Duyệt Qua Các Phần Tử trong Map
Việc duyệt qua tất cả các cặp key-value là rất phổ biến, ví dụ như để in ra nội dung của map. Bạn có thể sử dụng vòng lặp based-range for (từ C++11 trở đi) hoặc sử dụng iterators.
- Range-based for loop: Cách hiện đại và gọn gàng nhất để duyệt. Mỗi phần tử trong map khi duyệt sẽ là một
pair
với.first
là key và.second
là value. - Iterators: Cách truyền thống hơn, cung cấp quyền kiểm soát chi tiết hơn (mặc dù ít dùng khi chỉ cần duyệt đơn giản).
Ví dụ Code:
#include <iostream>
#include <map>
#include <string>
int main() {
map<string, int> tuoi;
tuoi["Alice"] = 31;
tuoi["Charlie"] = 35;
tuoi["Bob"] = 25;
cout << "--- Duyet map su dung Range-based for loop (Recommended) ---" << endl;
for (const auto& p : tuoi) {
cout << "Key: *" << p.first << "*, Value: *" << p.second << "*" << endl;
}
cout << "\n--- Duyet map su dung Iterators (Co dien) ---" << endl;
for (auto it = tuoi.begin(); it != tuoi.end(); ++it) {
cout << "Key: *" << it->first << "*, Value: *" << it->second << "*" << endl;
}
return 0;
}
Output:
--- Duyet map su dung Range-based for loop (Recommended) ---
Key: *Alice*, Value: *31*
Key: *Bob*, Value: *25*
Key: *Charlie*, Value: *35*
--- Duyet map su dung Iterators (Co dien) ---
Key: *Alice*, Value: *31*
Key: *Bob*, Value: *25*
Key: *Charlie*, Value: *35*
Giải thích:
- Vòng lặp
for (const auto& pair : ages)
duyệt qua map theo thứ tự key đã được sắp xếp. Biếnpair
trong mỗi lần lặp là mộtpair<const string, int>
. Chúng ta sử dụngpair.first
để lấy key vàpair.second
để lấy value. Sử dụngconst auto&
là cách hiệu quả để tránh sao chép. - Vòng lặp sử dụng iterators cũng làm điều tương tự.
ages.begin()
trả về iterator tới phần tử đầu tiên,ages.end()
trả về iterator "sentinel" (không hợp lệ, chỉ dùng để so sánh).it->first
vàit->second
truy cập key và value thông qua iterator.
Bài Tập 5: Xóa Phần Tử khỏi Map
Để loại bỏ một cặp key-value khỏi map, bạn sử dụng phương thức erase()
. erase()
có thể nhận:
- Key: Xóa phần tử với key tương ứng (nếu tồn tại).
- Iterator: Xóa phần tử mà iterator đang trỏ tới.
- Phạm vi Iterators: Xóa một dãy các phần tử.
Ví dụ Code:
#include <iostream>
#include <map>
#include <string>
int main() {
map<string, int> tuoi;
tuoi["Alice"] = 31;
tuoi["Bob"] = 25;
tuoi["Charlie"] = 35;
tuoi["David"] = 28;
cout << "Map ban dau:" << endl;
for (const auto& p : tuoi) {
cout << p.first << ": " << p.second << endl;
}
string tenXoa1 = "Bob";
size_t soXoa = tuoi.erase(tenXoa1);
cout << "\nDa xoa " << soXoa << " phan tu voi key '" << tenXoa1 << "'." << endl;
string tenXoa2 = "Eve";
soXoa = tuoi.erase(tenXoa2);
cout << "Da xoa " << soXoa << " phan tu voi key '" << tenXoa2 << "' (key khong ton tai)." << endl;
cout << "\nMap sau khi xoa Bob:" << endl;
for (const auto& p : tuoi) {
cout << p.first << ": " << p.second << endl;
}
auto it = tuoi.find("Charlie");
if (it != tuoi.end()) {
tuoi.erase(it);
cout << "\nDa xoa phan tu dung iterator den 'Charlie'." << endl;
} else {
cout << "\nKhong tim thay Charlie de xoa bang iterator." << endl;
}
cout << "\nMap sau khi xoa Charlie:" << endl;
for (const auto& p : tuoi) {
cout << p.first << ": " << p.second << endl;
}
return 0;
}
Output:
Map ban dau:
Alice: 31
Bob: 25
Charlie: 35
David: 28
Da xoa 1 phan tu voi key 'Bob'.
Da xoa 0 phan tu voi key 'Eve' (key khong ton tai).
Map sau khi xoa Bob:
Alice: 31
Charlie: 35
David: 28
Da xoa phan tu dung iterator den 'Charlie'.
Map sau khi xoa Charlie:
Alice: 31
David: 28
Giải thích:
ages.erase("Bob");
xóa phần tử có key là "Bob". Phương thức này trả về số lượng phần tử đã bị xóa (là 1 nếu tìm thấy, 0 nếu không tìm thấy).ages.find("Charlie")
tìm iterator trỏ đến "Charlie". Sau đó,ages.erase(it);
dùng iterator này để xóa hiệu quả phần tử đó.- Phương thức
clear()
(đang bị comment) sẽ xóa tất cả các phần tử khỏi map, làm cho nó trở thành rỗng.
Bài Tập 6: Map với Các Kiểu Dữ Liệu Khác
Key và value trong map
không nhất thiết phải là string
và int
. Chúng có thể là bất kỳ kiểu dữ liệu nào hỗ trợ toán tử so sánh <
(cho key) và có thể được copy/move.
Ví dụ Code:
#include <iostream>
#include <map>
int main() {
map<int, double> giaSP;
giaSP[101] = 15.50;
giaSP[205] = 99.99;
giaSP[55] = 5.00;
giaSP.insert({310, 45.75});
cout << "Bang gia san pham (sap xep theo ID):" << endl;
for (const auto& p : giaSP) {
cout << "ID San Pham: *" << p.first << "*, Gia: *" << p.second << "*" << endl;
}
if (giaSP.count(205)) {
cout << "\nGia cua san pham ID 205 la: " << giaSP[205] << endl;
}
return 0;
}
Output:
Bang gia san pham (sap xep theo ID):
ID San Pham: *55*, Gia: *5*
ID San Pham: *101*, Gia: *15.5*
ID San Pham: *205*, Gia: *99.99*
ID San Pham: *310*, Gia: *45.75*
Gia cua san pham ID 205 la: 99.99
Giải thích:
- Khai báo
map<int, double> product_prices;
cho thấy key có thể làint
và value làdouble
. - Map vẫn hoạt động tương tự, tự động sắp xếp các key (là các ID sản phẩm) theo thứ tự số học.
- Bạn vẫn sử dụng
[]
,insert
,count
như bình thường.
Bài Tập 7: Ứng Dụng Nhỏ - Đếm Tần Suất
Đây là một bài toán kinh điển mà map
rất phù hợp: đếm số lần xuất hiện của các phần tử (ví dụ: từ trong một câu, ký tự trong một chuỗi, v.v.).
Ví dụ Code:
#include <iostream>
#include <map>
#include <string>
#include <sstream>
#include <cctype>
int main() {
string cau = "Lap trinh C++ la rat hay, lap trinh Python cung rat hay.";
string cauSach;
for (char c : cau) {
if (isalnum(c) || isspace(c)) {
cauSach += tolower(c);
} else if (c == ',') {
cauSach += ' ';
}
}
map<string, int> demTu;
stringstream ss(cauSach);
string tu;
while (ss >> tu) {
if (!tu.empty()) {
demTu[tu]++;
}
}
cout << "Tan suat xuat hien cua cac tu trong cau:" << endl;
for (const auto& p : demTu) {
cout << "'" << p.first << "': *" << p.second << "* lan" << endl;
}
return 0;
}
Output:
Tan suat xuat hien cua cac tu trong cau:
'c': *1* lan
'cung': *1* lan
'hay': *2* lan
'la': *1* lan
'lap': *2* lan
'python': *1* lan
'rat': *2* lan
'trinh': *2* lan
Giải thích:
- Đoạn code đầu tiên thực hiện một bước làm sạch chuỗi đơn giản: chuyển tất cả về chữ thường và loại bỏ/thay thế một số ký tự không phải chữ-số bằng khoảng trắng.
- Chúng ta sử dụng
stringstream
để dễ dàng "đọc" từng từ một từ chuỗi đã làm sạch. - Vòng lặp
while (ss >> word)
sẽ đọc từng từ được phân tách bởi khoảng trắng. - Phần quan trọng nhất là
word_counts[word]++;
.- Nếu
word
chưa có trongword_counts
, toán tử[]
sẽ tự động thêm keyword
vào map và khởi tạo giá trị tương ứng là0
(giá trị mặc định choint
). - Sau đó, toán tử
++
sẽ tăng giá trị đó lên1
. - Nếu
word
đã có trong map,word_counts[word]
sẽ truy cập giá trị hiện tại của nó, và++
sẽ tăng giá trị đó lên 1.
- Nếu
- Kết quả là map
word_counts
sẽ lưu trữ mỗi từ duy nhất như một key và số lần nó xuất hiện như value. Khi in ra, các từ sẽ được sắp xếp theo thứ tự bảng chữ cái.
Comments