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() {
// Khai báo một map với key là string (ví dụ: tên), value là int (ví dụ: tuổi)
map<string, int> ages;
// --- Thêm phần tử sử dụng toán tử [] ---
// Nếu "Alice" chưa tồn tại, thêm "Alice" với tuổi 30. Nếu đã tồn tại, cập nhật tuổi thành 30.
ages["Alice"] = 30;
ages["Bob"] = 25;
ages["Charlie"] = 35;
// --- Thêm phần tử sử dụng phương thức insert() ---
// insert nhận một pair hoặc một initializer list {}
ages.insert({"David", 28});
ages.insert(make_pair("Eve", 22)); // Một cách khác để tạo pair
// Thử insert một key đã tồn tại - sẽ không có gì thay đổi cho key "Bob"
auto result = ages.insert({"Bob", 99});
if (!result.second) {
cout << "*Key 'Bob' da ton tai, insert khong thuc hien." << endl;
}
// In ra map để kiểm tra (chú ý thứ tự các phần tử sẽ được sắp xếp theo key)
cout << "\nMap sau khi them cac phan tu:" << endl;
for (const auto& pair : ages) {
cout << pair.first << ": " << pair.second << endl;
}
return 0;
}
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> // De bat ngoai le
int main() {
map<string, int> ages;
ages["Alice"] = 30;
ages["Bob"] = 25;
ages["Charlie"] = 35;
cout << "Map ban dau:" << endl;
for (const auto& pair : ages) {
cout << pair.first << ": " << pair.second << endl;
}
// --- Truy cập giá trị ---
cout << "\nTuoi cua Bob la: " << ages["Bob"] << endl;
// Sử dụng at() - an toàn hơn khi chỉ muốn đọc
try {
cout << "Tuoi cua Charlie la (dung at()): " << ages.at("Charlie") << endl;
// cout << "Tuoi cua David la (dung at()): " << ages.at("David") << endl; // Dong nay se gay ra ngoai le
} catch (const out_of_range& e) {
cerr << "Loi: Key khong ton tai: " << e.what() << endl;
}
// --- Cập nhật giá trị ---
ages["Alice"] = 31; // Alice vua them 1 tuoi!
cout << "\nTuoi cua Alice sau khi cap nhat: " << ages["Alice"] << endl;
// Can than khi dung [] de truy cap mot key chua ton tai
// Dong duoi se tao mot entry moi "David": 0 trong map
// int david_age = ages["David"];
// cout << "Tuoi cua David (vua duoc them boi []): " << david_age << endl;
// cout << "So luong phan tu trong map sau khi dung []: " << ages.size() << endl;
return 0;
}
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> ages;
ages["Alice"] = 31;
ages["Bob"] = 25;
// --- Kiểm tra sự tồn tại sử dụng count() ---
string name_to_check_1 = "Alice";
if (ages.count(name_to_check_1)) {
cout << name_to_check_1 << " *co ton tai* trong map." << endl;
} else {
cout << name_to_check_1 << " *khong ton tai* trong map." << endl;
}
string name_to_check_2 = "David";
if (ages.count(name_to_check_2)) {
cout << name_to_check_2 << " *co ton tai* trong map." << endl;
} else {
cout << name_to_check_2 << " *khong ton tai* trong map." << endl;
}
// --- Kiểm tra sự tồn tại sử dụng find() ---
string name_to_find_1 = "Bob";
auto it = ages.find(name_to_find_1);
if (it != ages.end()) {
cout << "Tim thay " << name_to_find_1 << " voi tuoi: " << it->second << endl;
// it tro den pair<const string, int>, nen it->first la key, it->second la value
} else {
cout << "Khong tim thay " << name_to_find_1 << " trong map." << endl;
}
string name_to_find_2 = "Eve";
it = ages.find(name_to_find_2); // it duoc gan lai ket qua tim kiem moi
if (it != ages.end()) {
cout << "Tim thay " << name_to_find_2 << " trong map." << endl;
} else {
cout << "Khong tim thay " << name_to_find_2 << " trong map." << endl;
}
return 0;
}
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> ages;
ages["Alice"] = 31;
ages["Charlie"] = 35;
ages["Bob"] = 25; // Key "Bob" se nam giua "Alice" va "Charlie" khi duyet do sap xep
cout << "--- Duyet map su dung Range-based for loop (Recommended) ---" << endl;
// const auto& pair : ages => Duyet tung phan tu, moi phan tu la mot cap key-value
for (const auto& pair : ages) {
cout << "Key: *" << pair.first << "*, Value: *" << pair.second << "*" << endl;
}
cout << "\n--- Duyet map su dung Iterators (Co dien) ---" << endl;
// ages.begin(): iterator tro den phan tu dau tien
// ages.end(): iterator tro den vi tri SAU phan tu cuoi cung
for (auto it = ages.begin(); it != ages.end(); ++it) {
cout << "Key: *" << it->first << "*, Value: *" << it->second << "*" << endl;
}
return 0;
}
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> ages;
ages["Alice"] = 31;
ages["Bob"] = 25;
ages["Charlie"] = 35;
ages["David"] = 28;
cout << "Map ban dau:" << endl;
for (const auto& pair : ages) {
cout << pair.first << ": " << pair.second << endl;
}
// --- Xóa phần tử sử dụng key ---
string name_to_erase_1 = "Bob";
size_t num_erased = ages.erase(name_to_erase_1); // erase() bang key tra ve so luong phan tu da xoa (0 hoac 1)
cout << "\nDa xoa " << num_erased << " phan tu voi key '" << name_to_erase_1 << "'." << endl;
string name_to_erase_invalid = "Eve";
num_erased = ages.erase(name_to_erase_invalid);
cout << "Da xoa " << num_erased << " phan tu voi key '" << name_to_erase_invalid << "' (key khong ton tai)." << endl;
cout << "\nMap sau khi xoa Bob:" << endl;
for (const auto& pair : ages) {
cout << pair.first << ": " << pair.second << endl;
}
// --- Xóa phần tử sử dụng iterator ---
auto it = ages.find("Charlie"); // Tim iterator den Charlie
if (it != ages.end()) {
ages.erase(it); // Xoa phan tu ma it tro den
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& pair : ages) {
cout << pair.first << ": " << pair.second << endl;
}
// --- Xóa tất cả các phần tử ---
// ages.clear();
// cout << "\nMap sau khi xoa tat ca (clear()): So luong phan tu = " << ages.size() << endl;
return 0;
}
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 voi key la int (ID san pham), value la double (gia tien)
map<int, double> product_prices;
product_prices[101] = 15.50;
product_prices[205] = 99.99;
product_prices[55] = 5.00; // Key 55 se dung dau tien do map sap xep theo int
// Thêm sản phẩm khác
product_prices.insert({310, 45.75});
cout << "Bang gia san pham (sap xep theo ID):" << endl;
for (const auto& pair : product_prices) {
cout << "ID San Pham: *" << pair.first << "*, Gia: *" << pair.second << "*" << endl;
}
// Truy cap gia san pham co ID 205
if (product_prices.count(205)) {
cout << "\nGia cua san pham ID 205 la: " << product_prices[205] << endl;
}
return 0;
}
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> // De tach chuoi thanh tung tu
#include <cctype> // De xu ly ky tu
int main() {
string sentence = "Lap trinh C++ la rat hay, lap trinh Python cung rat hay.";
// Chuan hoa chuoi don gian: chuyen ve chu thuong va loai bo dau cau (gan dung)
string cleaned_sentence;
for (char c : sentence) {
if (isalnum(c) || isspace(c)) { // Giu lai chu cai, so va khoang trang
cleaned_sentence += tolower(c); // Chuyen ve chu thuong
} else if (c == ',') {
cleaned_sentence += ' '; // Thay dau phay bang khoang trang de tach tu dung
}
}
map<string, int> word_counts; // Map de luu tu va so lan xuat hien
stringstream ss(cleaned_sentence); // Dung stringstream de tach cac tu
string word;
// Doc tung tu mot tu stringstream
while (ss >> word) {
if (!word.empty()) { // Dam bao tu khong rong (do xu ly khoang trang thua)
// Tang so dem cho tu hien tai
// Neu 'word' chua co trong map, ages[word] se tao moi entry voi value 0
// Sau do, ++ se tang value do len 1
word_counts[word]++;
}
}
cout << "Tan suat xuat hien cua cac tu trong cau:" << endl;
// Duyet map de in ket qua
for (const auto& pair : word_counts) {
cout << "'" << pair.first << "': *" << pair.second << "* lan" << endl;
}
return 0;
}
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.
Đây là một minh họa tuyệt vời cho thấy map
có thể đơn giản hóa các bài toán đếm và thống kê như thế nào.
Comments