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õ codechạ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});ages.insert(make_pair("Eve", 22)); là các cách khác sử dụng insert. 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 tra result.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 cho int, chuỗi rỗng cho string). 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ùng at(). Bạn nên dùng at() 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ằng try-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ới map, key là duy nhất nên count() chỉ trả về 0 (không tồn tại) hoặc 1 (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ác ages.end()). Chúng ta có thể dùng it->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ến pair trong mỗi lần lặp là một pair<const string, int>. Chúng ta sử dụng pair.first để lấy key và pair.second để lấy value. Sử dụng const 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->firstit->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à stringint. 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ấtword_counts[word]++;.
    • Nếu word chưa có trong word_counts, toán tử [] sẽ tự động thêm key word vào map và khởi tạo giá trị tương ứng là 0 (giá trị mặc định cho int).
    • Sau đó, toán tử ++ sẽ tăng giá trị đó lên 1.
    • 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.
  • 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

There are no comments at the moment.