सामग्री पर जाएं

सीक्वेंस कंटेनर: vector, deque, list, array

STL के सीक्वेंस कंटेनर — vector, deque, list, forward_list और array — को समझें। उनके परफ़ॉर्मेंस ट्रेड-ऑफ़, मेमोरी लेआउट और इटरेटर के अमान्य (invalidate) होने के नियम सीखें।

सीक्वेंस कंटेनर क्या होते हैं?

सीक्वेंस कंटेनर एलिमेंट को सख़्त रैखिक क्रम में स्टोर करते हैं। 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 से बेहतर प्रदर्शन करता है, और यह आश्चर्यजनक रूप से बड़े साइज़ (हज़ारों एलिमेंट) तक सही रहता है। लिंक्ड लिस्ट चुनने से पहले हमेशा मापें (measure)।

सामान्य गलती

हर कंटेनर के अपने अलग नियम हैं कि इटरेटर, पॉइंटर और रेफ़रेंस कब अमान्य (invalid) हो जाते हैं:

vector — जिस इन्सर्शन से रीअलोकेशन होता है, वह सभी इटरेटर को अमान्य कर देता है। रीअलोकेशन के बिना इन्सर्शन, इन्सर्शन बिंदु पर या उसके बाद के इटरेटर को अमान्य करता है। इरेज़ करने पर इरेज़ बिंदु पर या उसके बाद के इटरेटर अमान्य हो जाते हैं।

deque — आगे या पीछे इन्सर्शन सभी इटरेटर को अमान्य करता है, लेकिन मौजूदा एलिमेंट के रेफ़रेंस/पॉइंटर को नहीं। बीच में इन्सर्शन हर चीज़ को अमान्य कर देता है। आगे/पीछे इरेज़ करने पर सिर्फ़ इरेज़ किए गए एलिमेंट का इटरेटर अमान्य होता है।

list / forward_list — इन्सर्शन और इरेज़ दूसरे एलिमेंट के इटरेटर, रेफ़रेंस या पॉइंटर को कभी अमान्य नहीं करते। सिर्फ़ इरेज़ किए गए एलिमेंट का इटरेटर अमान्य होता है।

array — इटरेटर कभी अमान्य नहीं होते (ऐरे का साइज़ बदल नहीं सकता)।

एक आम बग है रेंज-आधारित for लूप के अंदर vector को बदलना। अगर push_back से रीअलोकेशन हो जाए, तो लूप के छिपे हुए इटरेटर डैंगलिंग हो जाते हैं।

सर्वोत्तम अभ्यास

अगर आपको पता है (या अनुमान लगा सकते हैं) कि vector में कितने एलिमेंट होंगे, तो उसे भरने से पहले reserve() कॉल करें। यह मेमोरी पहले से आवंटित कर देता है और महंगे रीअलोकेशन से बचाता है:

- reserve(n) कैपेसिटी बदलता है, साइज़ नहीं — कोई एलिमेंट नहीं जोड़ा जाता।
- resize(n) साइज़ बदलता है — यह वास्तव में एलिमेंट को डिफ़ॉल्ट-कंस्ट्रक्ट करता है।
- shrink_to_fit() कैपेसिटी को साइज़ के बराबर घटाने का एक नॉन-बाइंडिंग (बाध्यकारी नहीं) अनुरोध है।

जिन हॉट लूप में वेक्टर बनाए जाते हैं, वहाँ एक अकेली reserve() कॉल आवंटन का ओवरहेड काफ़ी घटा सकती है। फ़र्क़ देखने के लिए बड़े डेटा सेट के साथ मापें।

मुख्य बातें
  • std::vector डिफ़ॉल्ट विकल्प है — कंटिग्यूअस मेमोरी का मतलब है कैश-फ़्रेंडली इटरेशन और O(1) रैंडम एक्सेस
  • जब अनुमानित साइज़ पता हो, तो वेक्टर की मेमोरी पहले से आवंटित करने के लिए reserve() का उपयोग करें
  • std::deque आगे और पीछे, दोनों जगह O(1) push/pop देता है, लेकिन यह कंटिग्यूअस नहीं है
  • std::list इटरेटर के साथ O(1) insert/erase देती है और दूसरे इटरेटर को कभी अमान्य नहीं करती
  • std::array C ऐरे के ऊपर एक ज़ीरो-ओवरहेड रैपर है, जिसमें पूरी STL कम्पैटिबिलिटी है
  • इटरेटर अमान्य होने के नियम हर कंटेनर में अलग हैं — डैंगलिंग इटरेटर के बग से बचने के लिए उन्हें जानें

क्विज़ — अपना ज्ञान परखें

(15 XP)

1. कौन-सा सीक्वेंस कंटेनर O(1) अमॉर्टाइज़्ड `push_back` और कंटिग्यूअस मेमोरी लेआउट देता है?

2. जब `push_back` से रीअलोकेशन होता है, तो वेक्टर के सभी इटरेटर का क्या होता है?

3. वेक्टर पर `reserve(n)` और `resize(n)` में क्या अंतर है?