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

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ểuint
. 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
chodouble
).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ểustring
, 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ến và dễ đọ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ốitry-catch
để xử lý lỗi một cách gracefully. Sử dụng.at()
là 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 đại và dễ đọ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ếncolor
sẽ lần lượt nhận giá trị của mỗi phần tử. Sử dụngconst 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 đếnscores.size() - 1
. Kiểu dữ liệusize_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ơnpop_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:
- Chúng ta khởi tạo một vector
userNumbers
rỗng. - 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 đọcnum
không phải là 0. - 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()
. - Sau khi người dùng nhập 0, vòng lặp kết thúc.
- Chúng ta kiểm tra xem vector có rỗng không bằng
empty()
. - 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. - Cuối cùng, chúng ta in ra tổng số phần tử bằng
size()
và sử dụng.front()
và.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
new
vàdelete
). - 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àng và an 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