🧰 STL cơ bản: vector, pair và tuple
Ba công cụ nền tảng giúp lưu dãy động và gom nhiều dữ liệu liên quan thành một đối tượng. Bài học tập trung vào cách chọn kiểu dữ liệu, thao tác thường dùng, sắp xếp và structured binding.
🎯 Mục tiêu bài học
Cần hiểu
- Vì sao cần vector thay cho mảng cố định.
- Các thao tác thêm, chèn, xóa, truy cập, duyệt và sắp xếp.
- Biết dùng
insert,erase,find,resize,reversevà remove–erase. - Khi nào dùng pair và tuple.
- Cách dùng first, second, get, tie và structured binding.
Ứng dụng
- Lưu danh sách cạnh đồ thị.
- Lưu điểm và mã học sinh.
- Priority queue chứa khoảng cách và đỉnh.
- Trả về nhiều kết quả từ một hàm.
1. Đặt vấn đề
Chương trình đọc số lượng phần tử chưa biết trước. Mỗi phần tử có thể là một số, một cặp
(điểm, mã) hoặc một bộ ba (điểm, thời gian, mã). Cần thêm dữ liệu trong lúc chạy, sắp xếp và truy cập từng trường.1.1. Hạn chế của mảng thường
Mảng như int a[1000] cần biết trước giới hạn. Khi dữ liệu thay đổi, ta phải tự quản lý số phần tử và dễ vượt kích thước.
1.2. Hạn chế của nhiều mảng song song
Nếu một bản ghi có nhiều trường, dùng nhiều mảng riêng dễ làm dữ liệu lệch nhau khi sắp xếp.
| Nhu cầu | Cách thủ công | STL phù hợp |
|---|---|---|
| Dãy có kích thước thay đổi | Mảng lớn + biến đếm | vector<T> |
| Gom hai giá trị | Hai biến riêng | pair<A,B> |
| Gom nhiều giá trị | Nhiều biến riêng | tuple<A,B,C,...> |
2. vector – mảng động
Khai báo
vector<int> a;
vector<int> a(n);
vector<int> a(n,0);
vector<vector<int>> adj(n+1);
Thao tác chính
push_back(x),emplace_back(...).pop_back(),clear().size(),empty().front(),back(),a[i].sort(a.begin(),a.end()).
2.1. Mô phỏng vector
Phần tử
Capacity minh họa
| Thao tác | Độ phức tạp | Ghi chú |
|---|---|---|
| Truy cập a[i] | O(1) | Không tự kiểm tra biên. |
| push_back | O(1) trung bình khấu hao | Đôi lúc O(n) khi tái cấp phát. |
| pop_back | O(1) | Không trả về phần tử bị xóa. |
| Chèn/xóa giữa | O(n) | Các phần tử phía sau phải dịch. |
3. Các hàm thường dùng với vector
| Hàm / thuật toán | Ví dụ | Tác dụng | Độ phức tạp |
|---|---|---|---|
insert | a.insert(a.begin()+pos,x); | Chèn x trước vị trí pos. | O(n) |
erase một phần tử | a.erase(a.begin()+pos); | Xóa phần tử tại pos. | O(n) |
erase một đoạn | a.erase(a.begin()+L,a.begin()+R); | Xóa đoạn [L,R). | O(n) |
find | find(a.begin(),a.end(),x) | Tìm lần xuất hiện đầu tiên. | O(n) |
remove + erase | a.erase(remove(a.begin(),a.end(),x),a.end()); | Xóa mọi phần tử bằng x. | O(n) |
resize | a.resize(n,0); | Đổi size; phần tử mới nhận 0. | O(|size mới-size cũ|) |
assign | a.assign(n,value); | Thay toàn bộ vector. | O(n) |
reverse | reverse(a.begin(),a.end()); | Đảo thứ tự. | O(n) |
lower_bound | lower_bound(a.begin(),a.end(),x) | Tìm phần tử đầu tiên ≥ x; vector phải tăng dần. | O(log n) |
swap | a.swap(b); | Đổi nội dung hai vector. | Thường O(1) |
3.1. Mô phỏng insert, erase, resize, reverse và find
Vector đang mô phỏng
erase(begin()+L, begin()+R) xóa từ L đến R−1, không xóa R.0 ≤ pos ≤ size; erase cần 0 ≤ pos < size.3.2. Ví dụ xóa theo giá trị
vector<int> a = {2, 5, 2, 7, 2, 9};
int x = 2;
a.erase(
remove(a.begin(), a.end(), x),
a.end()
);
// a = {5, 7, 9}
remove chỉ dồn phần tử, không làm giảm size(). Phải kết hợp với erase.4. pair – gom hai giá trị
Truy cập
p.firstp.secondauto [x,y]=p;từ C++17
Ứng dụng
{weight,vertex}.{score,id}.{value,index}.
4.1. Sắp xếp vector<pair>
5. tuple – gom nhiều giá trị
Truy cập
get<0>(t),get<1>(t).auto [a,b,c]=t;tie(a,b,c)=t;ignoređể bỏ qua trường.
Khi nào dùng?
- Dữ liệu nhỏ, dùng cục bộ.
- Trả về nhiều giá trị.
- State 3–4 trường.
- Sắp xếp nhiều khóa.
5.1. Sắp xếp tuple
6. Chọn vector, pair, tuple hay struct?
| Công cụ | Mục đích | Ưu điểm | Hạn chế |
|---|---|---|---|
| vector | Lưu nhiều phần tử cùng kiểu | Kích thước động | Chèn/xóa giữa chậm |
| pair | Gom hai trường | Ngắn gọn | first/second thiếu ý nghĩa |
| tuple | Gom nhiều trường nhỏ | Structured binding tiện | Khó đọc khi nhiều trường |
| struct | Dữ liệu có tên trường rõ | Dễ đọc, dễ mở rộng | Dài hơn |
7. Code mẫu
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> a;
a.reserve(100);
a.push_back(5);
a.push_back(2);
a.emplace_back(8);
sort(a.begin(), a.end());
for (int x : a) cout << x << ' ';
if (!a.empty()) a.pop_back();
}#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> a = {10, 20, 30, 40, 50};
a.insert(a.begin() + 2, 99);
// {10, 20, 99, 30, 40, 50}
a.erase(a.begin() + 3);
// {10, 20, 99, 40, 50}
a.erase(a.begin() + 1, a.begin() + 3);
// Xóa đoạn [1,3): {10, 40, 50}
a.erase(
remove(a.begin(), a.end(), 40),
a.end()
);
// {10, 50}
auto it = find(a.begin(), a.end(), 50);
if (it != a.end()) {
int position = it - a.begin();
cout << position;
}
}vector<pair<int,string>> students;
students.push_back({9, "An"});
students.emplace_back(7, "Binh");
students.emplace_back(9, "Chi");
sort(students.begin(), students.end());
for (const auto& [score, name] : students) {
cout << score << ' ' << name << '\n';
}tuple<int,int,int> getMinMaxSum(const vector<int>& a) {
int mn = *min_element(a.begin(), a.end());
int mx = *max_element(a.begin(), a.end());
int sum = accumulate(a.begin(), a.end(), 0);
return {mn, mx, sum};
}
auto [mn, mx, sum] = getMinMaxSum(a);sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
if (a.score != b.score) return a.score > b.score;
if (a.name != b.name) return a.name < b.name;
return a.id < b.id;
}
);8. Giải thích code
8.1. Vì sao push_back là O(1) trung bình?
Phần lớn lần thêm chỉ ghi vào ô tiếp theo. Khi hết capacity, vector cấp phát vùng mới và chuyển các phần tử, nên có lần O(n). Tính trung bình nhiều lần là O(1) khấu hao.
8.2. reserve khác resize
reserve(n) chỉ tăng capacity, không tạo n phần tử. resize(n) thay đổi size và tạo/xóa phần tử.8.3. Vì sao dùng const auto&?
Tránh sao chép phần tử và không cho phép sửa dữ liệu gốc.
8.4. Structured binding có sao chép không?
auto [x,y]=p tạo bản sao. Muốn tham chiếu, dùng auto& [x,y]=p hoặc const auto& [x,y]=p.
8.5. emplace_back có luôn nhanh hơn?
Không. Nó hữu ích khi dựng đối tượng trực tiếp từ đối số. Với kiểu đơn giản như int, khác biệt không đáng kể.
8.6. insert và erase ảnh hưởng đến phần tử phía sau thế nào?
Vector lưu dữ liệu liên tiếp. Khi chèn hoặc xóa ở giữa, các phần tử phía sau phải dịch chuyển, nên thao tác thường là O(n).
8.7. Vì sao erase nhận iterator?
Muốn xóa vị trí pos, chuyển chỉ số thành iterator bằng a.begin()+pos.
8.8. erase trả về gì?
erase trả về iterator tới phần tử đứng ngay sau phần tử hoặc đoạn vừa bị xóa. Điều này hữu ích khi vừa duyệt vừa xóa.
8.9. Khi nào dùng lower_bound?
Đoạn dữ liệu phải được sắp xếp theo cùng thứ tự so sánh. Nếu vector chưa sắp xếp, kết quả không có ý nghĩa như tìm kiếm nhị phân.
9. Lỗi thường gặp
- Truy cập a[0] hoặc pop_back khi vector rỗng.
- Duyệt
i <= a.size(). - Nhầm reserve với resize.
- Giữ pointer/reference rồi push_back làm vector tái cấp phát.
- Dùng
auto [x,y]rồi tưởng sửa được pair gốc. - Nhớ sai thứ tự trường trong tuple.
- Dùng tuple quá dài thay vì struct.
- Gọi
inserthoặcerasevới vị trí vượt biên. - Nhầm đoạn xóa
[L,R)thành[L,R]. - Dùng
removenhưng quên gọierase. - Tiếp tục dùng iterator/reference cũ sau khi insert hoặc erase làm chúng mất hiệu lực.
10. Quiz và bài tập
a.erase(a.begin()+1,a.begin()+4) xóa các chỉ số nào?Cơ bản
- Nhập vector, in ngược.
- Tìm max/min bằng STL.
- Sắp xếp tăng/giảm.
- Chèn x vào vị trí pos.
- Xóa phần tử hoặc đoạn chỉ số.
- Xóa mọi phần tử bằng x.
Trung bình
- Sắp xếp vector<pair>.
- Lưu cạnh đồ thị.
- Trả về min/max/sum bằng tuple.
💳 Quét mã ủng hộ tuỳ tâm nhé!