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> v;
        cout << "Kich thuoc ban dau: " << v.size() << endl;
        return 0;
    }
    
    Kich thuoc ban dau: 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> v(10);
        cout << "Kich thuoc ban dau: " << v.size() << endl;
        return 0;
    }
    
    Kich thuoc ban dau: 10

    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> s(5, "chuoi");
        cout << "Phan tu dau tien: " << s[0] << endl;
        cout << "Kich thuoc: " << s.size() << endl;
        return 0;
    }
    
    Phan tu dau tien: chuoi
    Kich thuoc: 5

    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> v = {10, 20, 30, 40, 50};
        cout << "Vector duoc khoi tao: ";
        for (int n : v) {
            cout << n << " ";
        }
        cout << endl;
        cout << "Kich thuoc: " << v.size() << endl;
        return 0;
    }
    
    Vector duoc khoi tao: 10 20 30 40 50 
    Kich thuoc: 5

    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> v;

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

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

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

    cout << "Cac phan tu: ";
    for (int n : v) {
        cout << n << " ";
    }
    cout << endl;

    return 0;
}
Kich thuoc ban dau: 0
Kich thuoc sau khi them: 3
Cac phan tu: 5 10 15

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>
    #include <string>
    
    int main() {
        vector<string> s = {"a", "b", "c"};
    
        cout << "Phan tu dau tien: " << s[0] << endl;
        cout << "Phan tu thu hai: " << s[1] << endl;
        cout << "Phan tu cuoi cung: " << s[2] << endl;
    
        s[1] = "d";
        cout << "Phan tu thu hai sau khi thay doi: " << s[1] << endl;
    
        return 0;
    }
    
    Phan tu dau tien: a
    Phan tu thu hai: b
    Phan tu cuoi cung: c
    Phan tu thu hai sau khi thay doi: d

    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 <stdexcept>
    
    int main() {
        vector<int> v = {100, 200, 300};
    
        try {
            cout << "Phan tu tai chi so 1: " << v.at(1) << endl;
            cout << "Phan tu tai chi so 5: " << v.at(5) << endl;
        } catch (const out_of_range& e) {
            cerr << "Loi truy cap: " << e.what() << endl;
        }
    
        return 0;
    }
    
    Phan tu tai chi so 1: 200
    Loi truy cap: vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)

    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> s = {"do", "xanh", "vang"};
    
        cout << "Cac mau sac trong vector: ";
        for (const string& c : s) {
            cout << c << " ";
        }
        cout << endl;
    
        return 0;
    }
    
    Cac mau sac trong vector: do xanh vang

    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> d = {90, 85, 92, 78};
    
        cout << "Diem so theo chi so:" << endl;
        for (size_t i = 0; i < d.size(); ++i) {
            cout << "Diem[" << i << "] = " << d[i] << endl;
        }
    
        return 0;
    }
    
    Diem so theo chi so:
    Diem[0] = 90
    Diem[1] = 85
    Diem[2] = 92
    Diem[3] = 78

    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> v;

    cout << "Vector rong? " << (v.empty() ? "Co" : "Khong") << endl;
    cout << "Kich thuoc vector: " << v.size() << endl;

    v.push_back(10);
    v.push_back(20);

    cout << "Vector rong sau khi them? " << (v.empty() ? "Co" : "Khong") << endl;
    cout << "Kich thuoc vector sau khi them: " << v.size() << endl;

    return 0;
}
Vector rong? Co
Kich thuoc vector: 0
Vector rong sau khi them? Khong
Kich thuoc vector sau khi them: 2

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> v = {10, 20, 30, 40, 50};
    
        cout << "Vector ban dau: ";
        for (int n : v) cout << n << " ";
        cout << endl;
        cout << "Kich thuoc ban dau: " << v.size() << endl;
    
        v.pop_back();
    
        cout << "Vector sau pop_back(): ";
        for (int n : v) cout << n << " ";
        cout << endl;
        cout << "Kich thuoc sau pop_back(): " << v.size() << endl;
    
        v.pop_back();
    
        cout << "Vector sau pop_back() lan 2: ";
        for (int n : v) cout << n << " ";
        cout << endl;
        cout << "Kich thuoc sau pop_back() lan 2: " << v.size() << endl;
    
        return 0;
    }
    
    Vector ban dau: 10 20 30 40 50 
    Kich thuoc ban dau: 5
    Vector sau pop_back(): 10 20 30 40 
    Kich thuoc sau pop_back(): 4
    Vector sau pop_back() lan 2: 10 20 30 
    Kich thuoc sau pop_back() lan 2: 3

    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.

    #include <vector>
    #include <iostream>
    #include <string>
    
    int main() {
        vector<string> s = {"but", "but chi", "tay", "sach", "thuoc"};
    
        cout << "Vector ban dau: ";
        for (const string& e : s) cout << e << " ";
        cout << endl;
    
        s.erase(s.begin() + 2);
    
        cout << "Vector sau khi xoa phan tu thu 3 (tay): ";
        for (const string& e : s) cout << e << " ";
        cout << endl;
    
        s.erase(s.begin(), s.begin() + 2);
    
        cout << "Vector sau khi xoa pham vi (but va but chi): ";
        for (const string& e : s) cout << e << " ";
        cout << endl;
    
        return 0;
    }
    
    Vector ban dau: but but chi tay sach thuoc 
    Vector sau khi xoa phan tu thu 3 (tay): but but chi sach thuoc 
    Vector sau khi xoa pham vi (but va but chi): sach thuoc

    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> v = {1, 2, 3, 4, 5};
    
        cout << "Kich thuoc truoc khi clear: " << v.size() << endl;
    
        v.clear();
    
        cout << "Kich thuoc sau khi clear: " << v.size() << endl;
        cout << "Vector rong sau khi clear? " << (v.empty() ? "Co" : "Khong") << endl;
    
        return 0;
    }
    
    Kich thuoc truoc khi clear: 5
    Kich thuoc sau khi clear: 0
    Vector rong sau khi clear? Co

    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> v;
    int n;

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

    while (cin >> n && n != 0) {
        v.push_back(n);
    }

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

    if (v.empty()) {
        cout << "Ban chua nhap so nao ngoai so 0." << endl;
    } else {
        for (size_t i = 0; i < v.size(); ++i) {
            cout << "Phan tu thu " << i + 1 << ": " << v.at(i) << endl;
        }

        cout << "\nTong so phan tu: " << v.size() << endl;
        if (!v.empty()) {
             cout << "Phan tu dau tien: " << v.front() << endl;
             cout << "Phan tu cuoi cung: " << v.back() << endl;
        }
    }

    return 0;
}

Input (ví dụ):

Nhap cac so nguyen (nhap 0 de ket thuc):
10
20
30
0

Output cho Input trên:

Cac so ban da nhap la:
Phan tu thu 1: 10
Phan tu thu 2: 20
Phan tu thu 3: 30

Tong so phan tu: 3
Phan tu dau tien: 10
Phan tu cuoi cung: 30

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.