Chuyển đến nội dung

Vùng chứa trình tự: vector, deque, list, array

Khám phá các vùng chứa trình tự trong STL - vector, deque, list, Forward_list và mảng. Tìm hiểu sự cân bằng hiệu suất, bố cục bộ nhớ và quy tắc vô hiệu hóa vòng lặp của chúng.

Vùng chứa trình tự là gì?

Sequence container lưu trữ các phần tử theo thứ tự tuyến tính nghiêm ngặt. STL cung cấp năm vùng chứa trình tự, mỗi vùng được tối ưu hóa cho các kiểu truy cập và đột biến khác nhau:

- std::vector — mảng động, bộ nhớ liền kề
- std::deque — hàng đợi hai đầu, chèn nhanh ở cả hai đầu
- std::list — danh sách liên kết đôi
- std::forward_list — danh sách liên kết đơn
- std::array — mảng có kích thước cố định, được phân bổ theo ngăn xếp

Chọn vùng chứa phù hợp là một trong những quyết định về hiệu suất có ảnh hưởng nhất mà bạn sẽ thực hiện trong C++. Các yếu tố chính là mẫu truy cập, mẫu chèn/xóa và vị trí bộ đệm.

std::vector — Lựa chọn mặc định

std::vectorlà vùng chứa được sử dụng phổ biến nhất. Nó lưu trữ các phần tử trong một khối bộ nhớ liền kề, mang lại hiệu suất bộ đệm tuyệt vời và khả năng truy cập ngẫu nhiên O(1). Việc thêm các phần tử cópush_backđược khấu hao O(1) vì vectơ tăng gấp đôi công suất khi đầy.

vector_basics.cpp
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums;          // empty vector
    nums.reserve(100);              // pre-allocate for 100 elements (no reallocation)

    for (int i = 0; i < 10; ++i) {
        nums.push_back(i * i);      // amortized O(1)
    }

    // Size vs capacity
    std::cout << "Size:     " << nums.size()     << '\n';  // 10
    std::cout << "Capacity: " << nums.capacity() << '\n';  // >= 100

    // Random access — O(1)
    std::cout << "Element 3: " << nums[3] << '\n';         // 9
    std::cout << "Element 3: " << nums.at(3) << '\n';      // 9 (bounds-checked)

    // Erase-remove idiom (pre-C++20)
    nums.erase(std::remove_if(nums.begin(), nums.end(),
                              [](int x) { return x > 50; }),
               nums.end());

    // C++20: std::erase_if (cleaner)
    std::erase_if(nums, [](int x) { return x > 30; });

    // Range-based for loop
    for (int n : nums) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}

deque, danh sách và Forward_list

std::dequecung cấp tính năng chèn và xóa O(1) ở cả mặt trước và mặt sau, nhưng sử dụng bố cục bộ nhớ được phân đoạn (mảng các khối có kích thước cố định) thay vì phân bổ liền kề.std::listlà một danh sách liên kết đôi cung cấp tính năng chèn/xóa O(1) ở bất kỳ đâu được cung cấp một trình vòng lặp, với cái giá phải trả là vị trí bộ nhớ đệm kém.std::forward_listlà danh sách liên kết đơn sử dụng ít bộ nhớ hơn cho mỗi nút nhưng chỉ hỗ trợ truyền tải về phía trước.

deque_list.cpp
#include <deque>
#include <list>
#include <forward_list>
#include <iostream>

int main() {
    // deque — O(1) push_front and push_back
    std::deque<int> dq;
    dq.push_front(1);   // O(1) — vector cannot do this efficiently
    dq.push_back(2);    // O(1)
    dq.push_front(0);   // O(1)
    // dq: {0, 1, 2}

    // list — O(1) insert/erase at any position with iterator
    std::list<std::string> names = {"Alice", "Charlie", "Eve"};
    auto it = std::next(names.begin());  // points to "Charlie"
    names.insert(it, "Bob");             // O(1) insert before "Charlie"
    names.erase(it);                     // O(1) erase "Charlie"
    // names: {"Alice", "Bob", "Eve"}

    // list has splice — O(1) move of entire sublists
    std::list<std::string> more = {"Frank", "Grace"};
    names.splice(names.end(), more);     // moves all of 'more' into 'names'
    // names: {"Alice", "Bob", "Eve", "Frank", "Grace"}
    // more is now empty

    // forward_list — singly linked, no size(), push_front only
    std::forward_list<int> fl = {3, 1, 4, 1, 5};
    fl.push_front(0);
    fl.sort();           // member sort — O(n log n), stable
    fl.unique();         // remove consecutive duplicates
    // fl: {0, 1, 3, 4, 5}

    for (int v : fl) std::cout << v << ' ';
    std::cout << '\n';
}

std::array — Kích thước cố định, không cần chi phí

std::arraybao bọc một mảng kiểu C trong một lớp thích hợp, cung cấp.size(),.at(), các trình vòng lặp và khả năng tương thích với tất cả các thuật toán STL — với chi phí bằng 0 so vớiT[N]thô.

std_array.cpp
#include <array>
#include <algorithm>
#include <iostream>

int main() {
    std::array<int, 5> arr = {5, 2, 8, 1, 9};

    std::sort(arr.begin(), arr.end());       // works with STL algorithms

    std::cout << "Size: " << arr.size() << '\n';   // constexpr — known at compile time
    std::cout << "Front: " << arr.front() << '\n';  // 1
    std::cout << "Back:  " << arr.back()  << '\n';  // 9

    // Bounds-checked access
    try {
        arr.at(10);  // throws std::out_of_range
    } catch (const std::out_of_range& e) {
        std::cout << "Out of range: " << e.what() << '\n';
    }

    // Compile-time size via std::tuple_size
    constexpr std::size_t n = std::tuple_size_v<decltype(arr)>;  // 5
    static_assert(n == 5);
}

Chọn vùng chứa trình tự phù hợp

Sử dụng hướng dẫn quyết định này:

Sử dụngstd::vector (mặc định) khi bạn cần truy cập ngẫu nhiên, lặp lại thân thiện với bộ nhớ đệm hoặc đẩy/bật ở phía sau. Phần lớn mã nên sử dụngvector.

Sử dụngstd::deque khi bạn cần chèn/xóa thường xuyên ở cả mặt trước và mặt sau (ví dụ: cửa sổ trượt, hàng đợi BFS).

Sử dụngstd::list khi bạn cần chèn/xóa O(1) ở giữa với một trình vòng lặp hoặc khi bạn cầnsplice()để di chuyển danh sách con giữa các danh sách mà không cần sao chép.

Sử dụngstd::forward_list khi bộ nhớ trên mỗi nút có vấn đề và bạn chỉ cần truyền tải về phía trước.

Sử dụngstd::array khi kích thước được biết tại thời điểm biên dịch và sẽ không bao giờ thay đổi.

Lưu ý về hiệu suất: Do hiệu ứng bộ nhớ đệm của CPU,std::vectorthường hoạt động tốt hơnstd::listngay cả đối với các phần chèn ở giữa trên phần cứng hiện đại, có kích thước lớn đến mức đáng kinh ngạc (hàng nghìn phần tử). Luôn đo lường trước khi chọn danh sách liên kết.

Lỗi thường gặp

Mỗi vùng chứa có các quy tắc khác nhau khi các trình lặp, con trỏ và tham chiếu trở nên không hợp lệ:

vector — Việc chèn kích hoạt phân bổ lại sẽ làm mất hiệu lực tất cả các trình vòng lặp. Việc chèn mà không phân bổ lại sẽ làm mất hiệu lực các trình vòng lặp tại hoặc sau điểm chèn. Việc xóa làm mất hiệu lực các trình vòng lặp tại hoặc sau điểm xóa.

deque — Việc chèn ở phía trước hoặc phía sau sẽ vô hiệu hóa tất cả các trình lặp nhưng không làm mất hiệu lực các tham chiếu/con trỏ đến các phần tử hiện có. Chèn vào giữa làm mất hiệu lực mọi thứ. Việc xóa ở phía trước/phía sau chỉ làm mất hiệu lực trình vòng lặp của phần tử bị xóa.

list/forward_list — Chèn và xóa không bao giờ làm mất hiệu lực các trình vòng lặp, tham chiếu hoặc con trỏ tới các phần tử khác. Chỉ trình vòng lặp của phần tử bị xóa mới bị vô hiệu.

array — Các trình vòng lặp không bao giờ bị vô hiệu (mảng không thể thay đổi kích thước).

Một lỗi phổ biến là sửa đổivectorbên trong vòng lặp for dựa trên phạm vi. Nếupush_backkích hoạt việc tái phân bổ, các vòng lặp ẩn của vòng lặp sẽ bị treo lơ lửng.

Thực hành tốt

Nếu bạn biết (hoặc có thể ước tính) số lượng phần tửvectorsẽ chứa, hãy gọireserve()trước khi điền vào. Điều này phân bổ trước bộ nhớ và tránh việc phân bổ lại tốn kém:

-reserve(n)thay đổi dung lượng nhưng không thay đổi kích thước — không có phần tử nào được thêm vào.
-resize(n)thay đổi kích thước — nó thực sự là các phần tử cấu trúc mặc định.
-shrink_to_fit()là một yêu cầu không ràng buộc nhằm giảm dung lượng để phù hợp với kích thước.

Đối với các vòng lặp nóng xây dựng vectơ, một lệnh gọireserve()có thể cắt giảm đáng kể chi phí phân bổ. Đo lường bằng tập dữ liệu lớn để thấy sự khác biệt.

Điểm chính
  • std::vectorlà lựa chọn mặc định - bộ nhớ liền kề có nghĩa là phép lặp thân thiện với bộ đệm và truy cập ngẫu nhiên O(1)
  • Usereserve()to pre-allocate vector memory when the approximate size is known
  • std::dequeprovides O(1) push/pop at both front and back, but is not contiguous
  • std::listoffers O(1) insert/erase with an iterator and never invalidates other iterators
  • std::arrayis a zero-overhead wrapper around C arrays with full STL compatibility
  • Iterator invalidation rules differ per container — know them to avoid dangling iterator bugs

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Which sequence container provides O(1) amortized`push_back`and contiguous memory layout?

2. What happens to all vector iterators when a`push_back`triggers reallocation?

3. What is the difference between`reserve(n)`and`resize(n)`on a vector?