Μετάβαση στο περιεχόμενο

Sequence Containers: vector, deque, list, array

Εξερευνήστε τα δοχεία ακολουθίας στο STL — διάνυσμα, deque, list, forward_list και πίνακας. Μάθετε τους συμβιβασμούς απόδοσης, τις διατάξεις μνήμης και τους κανόνες ακύρωσης επαναλήπτη.

Τι είναι τα δοχεία ακολουθίας;

Τα δοχεία ακολουθίας αποθηκεύουν στοιχεία με αυστηρή γραμμική σειρά. Το STL παρέχει πέντε δοχεία ακολουθίας, το καθένα βελτιστοποιημένο για διαφορετικά μοτίβα πρόσβασης και μετάλλαξης:

- std::vector — δυναμικός πίνακας, συνεχόμενη μνήμη
- std::deque — διπλή ουρά, γρήγορη εισαγωγή και στα δύο άκρα
- std::list — λίστα διπλά συνδεδεμένη
- std::forward_list — λίστα μεμονωμένα
- std::array — πίνακας σταθερού μεγέθους, εκχωρημένος σε στοίβα

Η επιλογή του σωστού κοντέινερ είναι μία από τις πιο σημαντικές αποφάσεις απόδοσης που θα λάβετε στο C++. Οι βασικοί παράγοντες είναι μοτίβο πρόσβασης, μοτίβο εισαγωγής/διαγραφής και τοποθεσία προσωρινής μνήμης.

std::vector — Η προεπιλεγμένη επιλογή

Τοstd::vectorείναι το πιο συχνά χρησιμοποιούμενο δοχείο. Αποθηκεύει στοιχεία σε ένα ενιαίο συνεχόμενο μπλοκ μνήμης, παρέχοντάς του εξαιρετική απόδοση κρυφής μνήμης και τυχαία πρόσβαση O(1). Η προσθήκη στοιχείων μεpush_backαποσβένεται O(1) επειδή το διάνυσμα διπλασιάζει τη χωρητικότητά του όταν είναι γεμάτο.

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, list και forward_list

Τοstd::dequeπαρέχει εισαγωγή και διαγραφή O(1) τόσο στο μπροστινό όσο και στο πίσω μέρος, αλλά χρησιμοποιεί μια διάταξη τμηματοποιημένης μνήμης (συστοιχία μπλοκ σταθερού μεγέθους) αντί για μια ενιαία συνεχόμενη κατανομή. Τοstd::listείναι μια διπλά συνδεδεμένη λίστα που προσφέρει εισαγωγή/διαγραφή O(1) οπουδήποτε δίνεται ένας επαναλήπτης, σε βάρος της κακής τοποθεσίας της κρυφής μνήμης. Τοstd::forward_listείναι μια λίστα με μονή σύνδεση που χρησιμοποιεί λιγότερη μνήμη ανά κόμβο, αλλά υποστηρίζει μόνο διέλευση προς τα εμπρός.

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 — Σταθερό μέγεθος, μηδενικό γενικό κόστος

Τοstd::arrayαναδιπλώνει έναν πίνακα τύπου C σε μια σωστή κατηγορία, παρέχοντας.size(),.at(), επαναλήπτες και συμβατότητα με όλους τους αλγόριθμους STL — με μηδενικό κόστος σε σύγκριση με ένα ακατέργαστοT[N].

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);
}

Επιλέγοντας το σωστό κοντέινερ ακολουθίας

Χρησιμοποιήστε αυτόν τον οδηγό απόφασης:

Χρησιμοποιήστε τοstd::vector (η προεπιλογή) όταν χρειάζεστε τυχαία πρόσβαση, επανάληψη φιλική προς την προσωρινή μνήμη ή push/pop στο πίσω μέρος. Η συντριπτική πλειοψηφία του κώδικα θα πρέπει να χρησιμοποιείvector.

Χρησιμοποιήστε τοstd::deque όταν χρειάζεστε συχνή εισαγωγή/αφαίρεση τόσο μπροστά όσο και πίσω (π.χ. συρόμενο παράθυρο, ουρά BFS).

Χρησιμοποιήστε τοstd::list όταν χρειάζεστε εισαγωγή/αφαίρεση O(1) στη μέση δεδομένου ενός επαναλήπτη ή όταν χρειάζεστε τοsplice()για να μετακινήσετε υπολίστες μεταξύ λιστών χωρίς αντιγραφή.

Χρησιμοποιήστε τοstd::forward_list όταν η μνήμη ανά κόμβο έχει σημασία και χρειάζεστε μόνο διέλευση προς τα εμπρός.

Χρησιμοποιήστεstd::array όταν το μέγεθος είναι γνωστό κατά τη στιγμή της μεταγλώττισης και δεν θα αλλάξει ποτέ.

Σημείωση απόδοσης: Λόγω των εφέ κρυφής μνήμης της CPU, τοstd::vectorσυχνά ξεπερνά τοstd::listακόμη και για μεσαίες εισαγωγές σε σύγχρονο υλικό, έως και εκπληκτικά μεγάλα μεγέθη (χιλιάδες στοιχεία). Να μετράτε πάντα πριν επιλέξετε μια συνδεδεμένη λίστα.

Παγίδα

Κάθε κοντέινερ έχει διαφορετικούς κανόνες για το πότε οι επαναλήψεις, οι δείκτες και οι αναφορές καθίστανται άκυρα:

vector — Η εισαγωγή που ενεργοποιεί την ανακατανομή ακυρώνει όλα τους επαναληπτικούς. Η εισαγωγή χωρίς ανακατανομή ακυρώνει τους επαναλήπτες στο ή μετά το σημείο εισαγωγής. Η διαγραφή ακυρώνει τους επαναλήπτες στο ή μετά το σημείο διαγραφής.

deque — Η εισαγωγή στο μπροστινό ή πίσω μέρος ακυρώνει όλους τους επαναληπτικούς αλλά όχι τις αναφορές/δείκτες σε υπάρχοντα στοιχεία. Η εισαγωγή στη μέση ακυρώνει τα πάντα. Η διαγραφή μπροστά/πίσω ακυρώνει μόνο τον επαναλήπτη του διαγραμμένου στοιχείου.

list/forward_list — Η εισαγωγή και η διαγραφή ποτέ ακυρώνουν επαναλήψεις, αναφορές ή δείκτες σε άλλα στοιχεία. Μόνο ο επαναλήπτης του διαγραμμένου στοιχείου είναι άκυρος.

array — Οι επαναληπτικοί δεν ακυρώνονται ποτέ (ο πίνακας δεν μπορεί να αλλάξει μέγεθος).

Ένα κοινό σφάλμα είναι η τροποποίηση ενόςvectorμέσα σε έναν βρόχο for που βασίζεται σε εύρος. Εάν έναpush_backενεργοποιήσει την ανακατανομή, οι κρυφοί επαναλήπτες του βρόχου γίνονται κρέμονται.

Βέλτιστη πρακτική

Εάν γνωρίζετε (ή μπορείτε να υπολογίσετε) πόσα στοιχεία θα περιέχει έναvector, καλέστε τοreserve()πριν το συμπληρώσετε. Αυτό εκχωρεί εκ των προτέρων τη μνήμη και αποφεύγει τις δαπανηρές ανακατανομές:

- Τοreserve(n)αλλάζει χωρητικότητα αλλά όχι μέγεθος — δεν προστίθενται στοιχεία.
- Τοresize(n)αλλάζει μέγεθος — στην πραγματικότητα κατασκευάζει στοιχεία από προεπιλογή.
- Τοshrink_to_fit()είναι ένα μη δεσμευτικό αίτημα για μείωση της χωρητικότητας ώστε να ταιριάζει με το μέγεθος.

Για hot loop που δημιουργούν διανύσματα, μια κλήσηreserve()μπορεί να μειώσει δραματικά την επιβάρυνση της κατανομής. Μετρήστε με μεγάλα σύνολα δεδομένων για να δείτε τη διαφορά.

Βασικά συμπεράσματα
  • Τοstd::vectorείναι η προεπιλεγμένη επιλογή — συνεχόμενη μνήμη σημαίνει επανάληψη φιλική στην κρυφή μνήμη και τυχαία πρόσβαση O(1)
  • Χρησιμοποιήστε τοreserve()για να εκχωρήσετε εκ των προτέρων διανυσματική μνήμη όταν είναι γνωστό το κατά προσέγγιση μέγεθος
  • Τοstd::dequeπαρέχει O(1) push/pop τόσο μπροστά όσο και πίσω, αλλά δεν είναι συνεχόμενο
  • Τοstd::listπροσφέρει εισαγωγή/διαγραφή O(1) με επαναλήπτη και δεν ακυρώνει ποτέ άλλους επαναλήπτες
  • Τοstd::arrayείναι ένα περιτύλιγμα μηδενικής επιβάρυνσης γύρω από συστοιχίες C με πλήρη συμβατότητα STL
  • Οι κανόνες ακύρωσης του Iterator διαφέρουν ανά κοντέινερ — γνωρίζετε τους για να αποφύγετε κρέμονται σφάλματα επαναλήπτη

Κουίζ — Δοκιμάστε τις γνώσεις σας

(15 XP)

1. Ποιο κοντέινερ ακολουθίας παρέχει O(1) αποσβεσμένο`push_back`και διάταξη συνεχούς μνήμης;

2. Τι συμβαίνει σε όλους τους επαναλήπτες διανυσμάτων όταν ένα`push_back`ενεργοποιεί την ανακατανομή;

3. Ποια είναι η διαφορά μεταξύ`reserve(n)`και`resize(n)`σε ένα διάνυσμα;