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> 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ể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> 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
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> 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ể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> 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ế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> 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ố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> 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ế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> 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 đế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> 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ơ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.#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:
- 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