C++ Standard Template Library

🧰 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.

vectorMảng động, truy cập O(1)
pairGom đúng hai giá trị
tupleGom từ ba giá trị
sortSắp xếp từ điển

🎯 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, reverse và 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 đề

Bài toán mở đầu.
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ầuCách thủ côngSTL phù hợp
Dãy có kích thước thay đổiMảng lớn + biến đếmvector<T>
Gom hai giá trịHai biến riêngpair<A,B>
Gom nhiều giá trịNhiều biến riêngtuple<A,B,C,...>
Kết luận: vector quản lý số lượng phần tử; pair/tuple quản lý cấu trúc của từng phần tử.

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

size khác capacity: size là số phần tử đang có; capacity là số ô đã cấp phát. Khi hết capacity, vector có thể tái cấp phát vùng nhớ mới.
Thao tácĐộ phức tạpGhi chú
Truy cập a[i]O(1)Không tự kiểm tra biên.
push_backO(1) trung bình khấu haoĐôi lúc O(n) khi tái cấp phát.
pop_backO(1)Không trả về phần tử bị xóa.
Chèn/xóa giữaO(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

Mục tiêu: vector còn hỗ trợ chèn, xóa theo vị trí, xóa một đoạn, thay đổi kích thước, tìm kiếm và đảo thứ tự.
Hàm / thuật toánVí dụTác dụngĐộ phức tạp
inserta.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ạna.erase(a.begin()+L,a.begin()+R);Xóa đoạn [L,R).O(n)
findfind(a.begin(),a.end(),x)Tìm lần xuất hiện đầu tiên.O(n)
remove + erasea.erase(remove(a.begin(),a.end(),x),a.end());Xóa mọi phần tử bằng x.O(n)
resizea.resize(n,0);Đổi size; phần tử mới nhận 0.O(|size mới-size cũ|)
assigna.assign(n,value);Thay toàn bộ vector.O(n)
reversereverse(a.begin(),a.end());Đảo thứ tự.O(n)
lower_boundlower_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)
swapa.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

Đoạn xóa là nửa mở: erase(begin()+L, begin()+R) xóa từ L đến R−1, không xóa R.
Kiểm tra biên: insert cần 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}
Ghi nhớ: 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.first
  • p.second
  • auto [x,y]=p; từ C++17

Ứng dụng

  • {weight,vertex}.
  • {score,id}.
  • {value,index}.

4.1. Sắp xếp vector<pair>

Quy tắc: so sánh first trước; nếu bằng nhau mới so sánh second.

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.
Nên dùng struct nếu dữ liệu có ý nghĩa lâu dài hoặc nhiều trường, vì tên trường dễ đọc hơn get<i>.

5.1. Sắp xếp tuple

6. Chọn vector, pair, tuple hay struct?

Công cụMục đíchƯu điểmHạn chế
vectorLưu nhiều phần tử cùng kiểuKích thước độngChèn/xóa giữa chậm
pairGom hai trườngNgắn gọnfirst/second thiếu ý nghĩa
tupleGom nhiều trường nhỏStructured binding tiệnKhó đọc khi nhiều trường
structDữ liệu có tên trường rõDễ đọc, dễ mở rộngDà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 insert hoặc erase với vị trí vượt biên.
  • Nhầm đoạn xóa [L,R) thành [L,R].
  • Dùng remove nhưng quên gọi erase.
  • 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

Câu 1. Hàm thêm cuối vector?
Câu 2. Pair được so sánh thế nào?
Câu 3. reserve(100) làm gì?
Câu 4. a.erase(a.begin()+1,a.begin()+4) xóa các chỉ số nào?
Câu 5. Muốn xóa mọi phần tử bằng x khỏi vector, cách chuẩn là gì?

Cơ bản

  1. Nhập vector, in ngược.
  2. Tìm max/min bằng STL.
  3. Sắp xếp tăng/giảm.
  4. Chèn x vào vị trí pos.
  5. Xóa phần tử hoặc đoạn chỉ số.
  6. Xóa mọi phần tử bằng x.

Trung bình

  1. Sắp xếp vector<pair>.
  2. Lưu cạnh đồ thị.
  3. Trả về min/max/sum bằng tuple.