Bài 15.1: Bài tập thực hành Vector cơ bản trong C++

Chào mừng bạn quay trở lại với chuỗi bài học C++ của FullhouseDev!

Trong các bài trước, chúng ta đã làm quen với các khái niệm cơ bản, cấu trúc điều khiển và hàm. Hôm nay, chúng ta sẽ đi sâu vào một trong những cấu trúc dữ liệu cực kỳ quan trọng và được sử dụng rộng rãi trong C++: vector. vector là một phần của Thư viện Chuẩn C++ (Standard Template Library - STL), mang lại sức mạnh và sự linh hoạt của mảng động.

Bài này là bài tập thực hành, giúp bạn làm quen và củng cố kiến thức về các thao tác cơ bản với vector. Hãy cùng bắt tay vào nào!

vector là gì? Tại sao lại cần nó?

Bạn đã từng làm việc với mảng (arrays) trong C++ rồi đúng không? Mảng truyền thống có một nhược điểm lớn: kích thước của nó phải được xác định tại thời điểm biên dịch (compile-time) và không thể thay đổi trong quá trình chạy chương trình (run-time). Điều này gây khó khăn khi bạn không biết trước số lượng phần tử cần lưu trữ.

Đó là lúc vector tỏa sáng! Nó là một loại mảng động (dynamic array), có nghĩa là kích thước của nó có thể tự động thay đổi khi bạn thêm hoặc bớt phần tử trong lúc chương trình đang chạy. vector lưu trữ các phần tử theo trình tự trong các vùng nhớ liên tục (contiguous memory), giống như mảng tĩnh, điều này cho phép truy cập nhanh chóng đến bất kỳ phần tử nào thông qua chỉ số.

Các lợi ích chính:

  • Kích thước động: Tự động co giãn khi cần.
  • Truy cập ngẫu nhiên nhanh: Truy cập phần tử bằng chỉ số chỉ mất thời gian cố định (O(1)).
  • Dễ sử dụng: Cung cấp nhiều phương thức tiện lợi để quản lý dữ liệu.

Để sử dụng vector, bạn cần bao gồm header file <vector>:

#include <vector>

Và để sử dụng các hàm nhập xuất như cout, cin, bạn cần <iostream>:

#include <iostream>

Các Thao Tác Cơ Bản với vector

Chúng ta sẽ thực hành các thao tác phổ biến nhất với vector: tạo, thêm, truy cập, lặp qua, lấy kích thước và xóa phần tử.

1. Khởi tạo vector

Có nhiều cách để tạo một vector:

  • Vector rỗng:

    #include <vector>
    #include <iostream>
    
    int main() {
        vector<int> myVector; // Tạo một vector rỗng chứa số nguyên
        cout << "Kich thuoc ban dau: " << myVector.size() << endl; // Kich thuoc la 0
        return 0;
    }
    

    Giải thích: vector<int> myVector; tạo ra một vector có thể chứa các giá trị kiểu int. Ban đầu nó không chứa phần tử nào, nên kích thước là 0.

  • Vector với kích thước ban đầu:

    #include <vector>
    #include <iostream>
    
    int main() {
        vector<double> prices(10); // Tạo vector 10 phan tu kieu double, gia tri mac dinh (0.0)
        cout << "Kich thuoc ban dau: " << prices.size() << endl;
        // Bạn có thể truy cập các phần tử này ngay lập tức, ví dụ: prices[0], prices[9]
        return 0;
    }
    

    Giải thích: vector<double> prices(10); tạo ra một vector có 10 phần tử ngay từ đầu. Các phần tử này được khởi tạo bằng giá trị mặc định của kiểu dữ liệu (0.0 cho double).

  • Vector với kích thước ban đầu và giá trị khởi tạo:

    #include <vector>
    #include <iostream>
    #include <string>
    
    int main() {
        vector<string> names(5, "unknown"); // Tao vector 5 phan tu kieu string, tat ca deu la "unknown"
        cout << "Phan tu dau tien: " << names[0] << endl;
        cout << "Kich thuoc: " << names.size() << endl;
        return 0;
    }
    

    Giải thích: vector<string> names(5, "unknown"); tạo ra một vector có 5 phần tử kiểu string, và tất cả 5 phần tử đều được khởi tạo với giá trị "unknown".

  • Khởi tạo từ danh sách (Initializer List - C++11 trở lên): Cách này cực kỳ tiện lợi!

    #include <vector>
    #include <iostream>
    
    int main() {
        vector<int> numbers = {10, 20, 30, 40, 50}; // Khoi tao truc tiep voi cac gia tri
        cout << "Vector duoc khoi tao: ";
        for (int num : numbers) {
            cout << num << " ";
        }
        cout << endl;
        cout << "Kich thuoc: " << numbers.size() << endl;
        return 0;
    }
    

    Giải thích: vector<int> numbers = {10, 20, 30, 40, 50}; tạo một vector và điền sẵn các giá trị được liệt kê trong dấu ngoặc nhọn {}. Đây là cách phổ biếndễ đọc khi bạn đã biết trước các giá trị ban đầu.

2. Thêm phần tử (push_back)

Phương thức quan trọng nhất để thêm phần tử vào cuối vector là push_back(). Vector sẽ tự động quản lý bộ nhớ để có chỗ cho phần tử mới.

#include <vector>
#include <iostream>

int main() {
    vector<int> myDynamicVector;

    cout << "Kich thuoc ban dau: " << myDynamicVector.size() << endl;

    myDynamicVector.push_back(5);
    myDynamicVector.push_back(10);
    myDynamicVector.push_back(15);

    cout << "Kich thuoc sau khi them: " << myDynamicVector.size() << endl;

    cout << "Cac phan tu: ";
    for (int num : myDynamicVector) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

Giải thích: Chúng ta bắt đầu với một vector rỗng. Mỗi lần gọi push_back(), một phần tử mới được thêm vào cuối vector và kích thước vector tăng lên 1. Vector tự động điều chỉnh bộ nhớ cần thiết.

3. Truy cập phần tử

Có hai cách chính để truy cập các phần tử của vector dựa trên chỉ số (index), bắt đầu từ 0:

  • Sử dụng toán tử []: Giống như mảng tĩnh. Rất nhanh.

    #include <vector>
    #include <iostream>
    
    int main() {
        vector<string> fruits = {"apple", "banana", "cherry"};
    
        cout << "Phan tu dau tien: " << fruits[0] << endl;   // apple
        cout << "Phan tu thu hai: " << fruits[1] << endl;  // banana
        cout << "Phan tu cuoi cung: " << fruits[2] << endl; // cherry
    
        // Có thể thay đổi giá trị phần tử
        fruits[1] = "grape";
        cout << "Phan tu thu hai sau khi thay doi: " << fruits[1] << endl; // grape
    
        return 0;
    }
    

    Giải thích: fruits[0] truy cập phần tử đầu tiên. Bạn có thể sử dụng toán tử [] cả để đọc và ghi giá trị. Tuy nhiên, hãy cẩn thận: toán tử [] không kiểm tra xem chỉ số có hợp lệ hay không. Nếu bạn truy cập một chỉ số nằm ngoài phạm vi hợp lệ (từ 0 đến size-1), chương trình sẽ gặp lỗi không xác định (undefined behavior), có thể dẫn đến crash.

  • Sử dụng phương thức .at(): Cung cấp tính năng kiểm tra phạm vi. An toàn hơn!

    #include <vector>
    #include <iostream>
    #include <string>
    #include <stdexcept> // Can cai nay cho exception
    
    int main() {
        vector<int> data = {100, 200, 300};
    
        try {
            cout << "Phan tu tai chi so 1: " << data.at(1) << endl; // 200
            // Thu truy cap chi so khong hop le
            cout << "Phan tu tai chi so 5: " << data.at(5) << endl;
        } catch (const out_of_range& oor) {
            // at() se nem ra ngoai le out_of_range neu chi so khong hop le
            cerr << "Loi truy cap: " << oor.what() << endl;
        }
    
        return 0;
    }
    

    Giải thích: .at(1) truy cập phần tử tại chỉ số 1 một cách an toàn. Khi chúng ta cố gắng truy cập .at(5) (mà chỉ số hợp lệ chỉ từ 0 đến 2), .at() phát hiện ra điều đó và ném ra một ngoại lệ (out_of_range). Bạn có thể bắt ngoại lệ này bằng khối try-catch để xử lý lỗi một cách gracefully. Sử dụng .at()cách an toàn hơn khi bạn không chắc chắn về tính hợp lệ của chỉ số.

4. Lặp qua các phần tử

Bạn có thể duyệt qua tất cả các phần tử trong vector theo nhiều cách:

  • Vòng lặp dựa trên phạm vi (Range-based for loop - C++11 trở lên): Cách hiện đạidễ đọc nhất cho việc duyệt đơn giản.

    #include <vector>
    #include <iostream>
    #include <string>
    
    int main() {
        vector<string> colors = {"red", "green", "blue", "yellow"};
    
        cout << "Cac mau sac trong vector: ";
        for (const string& color : colors) { // Dung const reference de hieu qua hon
            cout << color << " ";
        }
        cout << endl;
    
        return 0;
    }
    

    Giải thích: Vòng lặp này sẽ tự động lặp qua từng phần tử trong vector colors. Biến color sẽ lần lượt nhận giá trị của mỗi phần tử. Sử dụng const string& thay vì string giúp tránh việc sao chép phần tử, hiệu quả hơn đối với các kiểu dữ liệu lớn như string.

  • Vòng lặp dựa trên chỉ số (Index-based for loop): Cách truyền thống, hữu ích khi bạn cần biết chỉ số của phần tử.

    #include <vector>
    #include <iostream>
    
    int main() {
        vector<int> scores = {90, 85, 92, 78};
    
        cout << "Diem so theo chi so:" << endl;
        for (size_t i = 0; i < scores.size(); ++i) {
            cout << "Scores[" << i << "] = " << scores[i] << endl;
        }
    
        return 0;
    }
    

    Giải thích: Chúng ta sử dụng biến i làm chỉ số, bắt đầu từ 0 và tăng dần cho đến scores.size() - 1. Kiểu dữ liệu size_t là kiểu unsigned integer phù hợp để lưu trữ kích thước và chỉ số của các container như vector.

5. Lấy kích thước vector
  • Phương thức .size() trả về số lượng phần tử hiện có trong vector.
  • Phương thức .empty() trả về true nếu vector rỗng (kích thước bằng 0), ngược lại trả về false.
#include <vector>
#include <iostream>

int main() {
    vector<int> data; // Rỗng

    cout << "Data rong? " << (data.empty() ? "Yes" : "No") << endl; // Yes
    cout << "Kich thuoc Data: " << data.size() << endl; // 0

    data.push_back(10);
    data.push_back(20);

    cout << "Data rong sau khi them? " << (data.empty() ? "Yes" : "No") << endl; // No
    cout << "Kich thuoc Data sau khi them: " << data.size() << endl; // 2

    return 0;
}

Giải thích: size() là cách chính xác để biết vector có bao nhiêu phần tử. empty() là một cách hiệu quả hơn để kiểm tra xem vector có rỗng hay không so với việc kiểm tra size() == 0.

6. Xóa phần tử

Có một vài cách để xóa phần tử khỏi vector:

  • Xóa phần tử cuối cùng (pop_back): Loại bỏ phần tử ở cuối vector. Nhanh chóng.

    #include <vector>
    #include <iostream>
    
    int main() {
        vector<int> numbers = {10, 20, 30, 40, 50};
    
        cout << "Vector ban dau: ";
        for (int num : numbers) cout << num << " ";
        cout << endl;
        cout << "Kich thuoc ban dau: " << numbers.size() << endl; // 5
    
        numbers.pop_back(); // Xoa 50
    
        cout << "Vector sau pop_back(): ";
        for (int num : numbers) cout << num << " ";
        cout << endl;
        cout << "Kich thuoc sau pop_back(): " << numbers.size() << endl; // 4
    
        numbers.pop_back(); // Xoa 40
    
        cout << "Vector sau pop_back() lan 2: ";
        for (int num : numbers) cout << num << " ";
        cout << endl;
        cout << "Kich thuoc sau pop_back() lan 2: " << numbers.size() << endl; // 3
    
        return 0;
    }
    

    Giải thích: pop_back() đơn giản là loại bỏ phần tử cuối cùng. Nó không trả về giá trị của phần tử bị xóa.

  • Xóa phần tử tại vị trí cụ thể hoặc một phạm vi (erase): Sử dụng iterator để chỉ định vị trí cần xóa. Phức tạp hơn pop_back() và có thể chậm hơn nếu xóa ở đầu hoặc giữa vector vì các phần tử sau đó cần được di chuyển. ```cpp #include <vector> #include <iostream> #include <string>

    int main() {

    vector<string> items = {"pen", "pencil", "eraser", "book", "ruler"};
    
    cout << "Vector ban dau: ";
    for (const string& item : items) cout << item << " ";
    cout << endl;
    
    // Xoa phan tu tai chi so 2 ("eraser")
    // items.begin() la iterator den phan tu dau tien
    // items.begin() + 2 la iterator den phan tu tai chi so 2
    items.erase(items.begin() + 2);
    
    cout << "Vector sau khi xoa phan tu thu 3 (eraser): ";
    for (const string& item : items) cout << item << " ";
    cout << endl;
    
    // Xoa mot pham vi phan tu (tu chi so 0 den chi so 1 - tuc la pen va pencil)
    // erase nhan vao 2 iterator: begin (bao gom) va end (khong bao gom)
    // items.begin() la iterator den pen
    // items.begin() + 2 la iterator den eraser (vi tri sau pencil)
    items.erase(items.begin(), items.begin() + 2);
    cout << "Vector sau khi xoa pham vi (pen va pencil): ";
    for (const string& item : items) cout << item << " ";
    cout << endl;

    return 0;
}
```
*Giải thích:* `erase()` là phương thức *mạnh mẽ* hơn nhưng yêu cầu bạn làm việc với *iterator*. `items.begin()` trả về một iterator trỏ đến phần tử đầu tiên. `items.begin() + index` trả về iterator trỏ đến phần tử tại vị trí `index`. Bạn có thể xóa một phần tử hoặc một phạm vi các phần tử (bắt đầu từ iterator đầu tiên đến *trước* iterator thứ hai).
  • Xóa tất cả các phần tử (clear): Làm cho vector trở thành rỗng.

    #include <vector>
    #include <iostream>
    
    int main() {
        vector<int> numbers = {1, 2, 3, 4, 5};
    
        cout << "Kich thuoc truoc khi clear: " << numbers.size() << endl; // 5
    
        numbers.clear(); // Xoa het
    
        cout << "Kich thuoc sau khi clear: " << numbers.size() << endl; // 0
        cout << "Vector rong sau khi clear? " << (numbers.empty() ? "Yes" : "No") << endl; // Yes
    
        return 0;
    }
    

    Giải thích: clear() là cách nhanh nhất để xóa tất cả nội dung của vector.

Ví dụ thực hành tổng hợp: Đọc input và xử lý

Hãy kết hợp một vài thao tác cơ bản vào một ví dụ thực tế hơn: đọc một danh sách các số từ người dùng và lưu chúng vào vector.

#include <vector>
#include <iostream>

int main() {
    vector<int> userNumbers;
    int num;

    cout << "Nhap cac so nguyen (nhap 0 de ket thuc):" << endl;

    // Doc so tu nguoi dung cho den khi nhap 0
    while (cin >> num && num != 0) {
        userNumbers.push_back(num); // Them so vao cuoi vector
    }

    cout << "\nCac so ban da nhap la:" << endl;

    // Kiem tra xem vector co rong khong truoc khi in
    if (userNumbers.empty()) {
        cout << "Ban chua nhap so nao ngoai so 0." << endl;
    } else {
        // In cac so trong vector
        for (size_t i = 0; i < userNumbers.size(); ++i) {
            // Dung at() de truy cap an toan hon trong truong hop nay (du vong lap da dam bao chi so hop le)
            cout << "Phan tu thu " << i + 1 << ": " << userNumbers.at(i) << endl;
        }

        // Mot vai thong tin khac
        cout << "\nTong so phan tu: " << userNumbers.size() << endl;
        // Truy cap phan tu dau va cuoi (can dam bao vector khong rong)
        if (!userNumbers.empty()) {
             cout << "Phan tu dau tien: " << userNumbers.front() << endl; // Hoac userNumbers[0] hoac userNumbers.at(0)
             cout << "Phan tu cuoi cung: " << userNumbers.back() << endl;  // Hoac userNumbers[userNumbers.size() - 1] hoac userNumbers.at(userNumbers.size() - 1)
        }
    }

    return 0;
}

Giải thích:

  1. Chúng ta khởi tạo một vector userNumbers rỗng.
  2. Sử dụng vòng lặp while, chúng ta đọc số nguyên từ cin. Vòng lặp tiếp tục chừng nào việc đọc thành công (cin >> num) và số vừa đọc num không phải là 0.
  3. Với mỗi số đọc được (khác 0), chúng ta thêm nó vào cuối vector bằng push_back().
  4. Sau khi người dùng nhập 0, vòng lặp kết thúc.
  5. Chúng ta kiểm tra xem vector có rỗng không bằng empty().
  6. Nếu không rỗng, chúng ta dùng vòng lặp dựa trên chỉ số để in ra từng phần tử. Lưu ý dùng .at(i) để truy cập an toàn.
  7. Cuối cùng, chúng ta in ra tổng số phần tử bằng size() và sử dụng .front().back() (các phương thức tiện lợi để truy cập phần tử đầu và cuối mà không cần dùng chỉ số, nhưng cũng cần kiểm tra vector không rỗng trước).

Ví dụ này cho thấy cách vector rất hữu ích khi bạn cần thu thập dữ liệu từ người dùng mà không biết trước họ sẽ nhập bao nhiêu.

Tại sao vector tốt hơn mảng tĩnh?

Sau khi thực hành, bạn có thể thấy rõ các ưu điểm của vector so với mảng tĩnh:

  • Linh hoạt về kích thước: Điều chỉnh kích thước tự động, loại bỏ nhu cầu khai báo kích thước cố định hoặc phải tự quản lý bộ nhớ động (với newdelete).
  • An toàn hơn (với .at()): Ngăn chặn lỗi truy cập ngoài phạm vi (out-of-bounds access) nếu bạn sử dụng phương thức .at().
  • Nhiều tiện ích tích hợp: Các phương thức như push_back, pop_back, size, empty, clear, erase giúp thao tác với dữ liệu dễ dàngan toàn hơn.

Sử dụng vector là một thực hành tốt trong lập trình C++ hiện đại cho hầu hết các trường hợp cần mảng động.

Comments

There are no comments at the moment.