📦 Phase 4 · STL 🟡 Intermediate MODULE 15

Vector, List & Deque

⏱️ 24 min read
📖 Theory + Code
🧩 5 Quiz Questions
🏗️ 1 Challenge
Your progress in Phase 4 — STL20%
🎯 What you'll learn: How std::vector works as C++'s default dynamic array, when std::list's doubly-linked structure beats it, and when std::deque's two-ended access is exactly what you need.

Why Not Just Use Arrays?

Back in Lesson 6 you learned raw C++ arrays: int arr[10];. They work, but they have a big limitation — their size is fixed at compile time. You can't add an 11th element to a 10-element array. If you don't know how many items you'll need ahead of time, a raw array is the wrong tool.

In Lesson 9 you saw the manual fix: allocate with new, track the size yourself, and remember to delete[] it when you're done — or leak memory. The C++ Standard Template Library (STL) gives you container classes that do all of that bookkeeping automatically and safely. The first and most important one is std::vector.

💡
A vector is a self-managing dynamic array
Internally, std::vector allocates a block of memory with new on your behalf. When it runs out of room, it automatically allocates a bigger block, copies the old elements over, and frees the old block — all behind the scenes. You never call new or delete yourself.

std::vector — Your Default Container

A vector<T> stores elements of type T in one contiguous block of memory, just like an array — which means indexing with [] is just as fast as a raw array (O(1)). Unlike an array, it can grow.

vector_basics.cpp
C++
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> scores; // empty vector, grows automatically

    scores.push_back(90);
    scores.push_back(85);
    scores.push_back(77);

    cout << "Number of scores: " << scores.size() << endl;

    for (auto s : scores) { // range-based for loop
        cout << s << " ";
    }
    cout << endl;

    scores.pop_back(); // removes the last element (77)
    cout << "After pop_back, size = " << scores.size() << endl;

    return 0;
}

Notice scores started completely empty — no size given up front. .push_back() adds an element to the end (amortized O(1) — occasionally the vector has to grow and copy everything, but averaged over many pushes, each one is effectively constant time). .pop_back() removes the last element in O(1). .size() always tells you exactly how many elements are currently stored.

Indexing: [] vs .at()

Just like an array, you can read or write any element with []. Vector also gives you .at(), which does the same thing but checks the index is valid first and throws an exception if it isn't.

vector_indexing.cpp
C++
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {10, 20, 30, 40, 50};

    cout << "nums[2] = " << nums[2] << endl;       // fast, no bounds check
    cout << "nums.at(2) = " << nums.at(2) << endl;  // safe, checked

    nums[0] = 100; // elements are mutable through []

    try {
        cout << nums.at(10) << endl; // index out of range!
    } catch (const std::out_of_range& e) {
        cout << "Caught error: " << e.what() << endl;
    }

    return 0;
}
⚠️
[] is fast but unsafe, .at() is safe but slightly slower
Use [] in hot loops where you're certain the index is valid. Use .at() when the index comes from user input or untrusted data — a caught exception beats a silent crash or corrupted memory.
O(1) Random Access
Because storage is contiguous, vec[i] jumps straight to the element — no walking through the list.
📈
Amortized O(1) push_back
Growing occasionally costs O(n) to copy, but spread across many pushes it averages out to constant time.
🐢
O(n) Middle Insert
Inserting or erasing in the middle shifts every element after it — slow for large vectors.
🧹
Auto Memory Management
No new/delete needed — the vector's destructor frees its memory automatically when it goes out of scope.

std::list — The Doubly-Linked List

std::list<T> stores elements as separate nodes, each holding a value plus pointers to the next and previous node — a doubly-linked list. Elements are scattered across memory, not contiguous.

That trade-off flips vector's strengths and weaknesses: list can't be indexed with [] at all (there's no operator for it — reaching element 5 means walking 5 nodes from the start, O(n)). But once you already have an iterator pointing at a position, inserting or removing there is O(1) — no shifting, just re-linking a couple of pointers.

list_basics.cpp
C++
#include <iostream>
#include <list>
#include <string>
using namespace std;

int main() {
    list<string> names = {"Alice", "Bob", "Charlie"};

    auto it = names.begin();
    ++it; // now points at "Bob"
    names.insert(it, "Zoe"); // O(1) — no shifting needed

    for (const string& n : names) {
        cout << n << " ";
    }
    cout << endl; // Alice Zoe Bob Charlie

    return 0;
}
When to reach for list
Choose list when your program does a lot of inserting/removing in the middle of a sequence (e.g. a playlist you constantly reorder) and rarely needs to jump to "the 47th item" by index. If you mostly read by index or add to the end, vector wins.

std::deque — Double-Ended Queue

std::deque<T> (pronounced "deck") behaves like a vector — indexable with [] — but adds one superpower: it can grow or shrink efficiently from both ends, not just the back.

deque_basics.cpp
C++
#include <iostream>
#include <deque>
using namespace std;

int main() {
    deque<int> dq;

    dq.push_back(2);
    dq.push_back(3);
    dq.push_front(1);
    dq.push_front(0);

    for (int n : dq) cout << n << " ";
    cout << endl; // 0 1 2 3

    dq.pop_front();
    dq.pop_back();

    cout << "front = " << dq.front() << ", back = " << dq.back() << endl;

    return 0;
}

A vector's push_front() doesn't even exist — inserting at position 0 would mean shifting every other element down, an O(n) operation, so the STL deliberately doesn't offer a misleadingly-named fast method for it. A deque is built specifically to make front and back operations both O(1), which makes it the natural choice for things like a sliding-window buffer or a task queue processed from either end.

Choosing the Right Container

Container Random Access Insert/Erase Middle Push Front Best For
vectorO(1)O(n)O(n)Default choice, general use
listO(n)O(1)*O(1)Frequent middle insert/remove
dequeO(1)O(n)O(1)Need both ends fast

* O(1) once you already hold an iterator to the position — finding that position by walking the list is still O(n).

🎯
Rule of thumb
Start every new sequence with vector. Only switch to list or deque once you've identified a specific access pattern that vector handles poorly. In real codebases, vector is used roughly ten times more often than list and deque combined.

Lesson Summary

std::vector is a self-growing array — contiguous memory, O(1) indexing, and it manages its own new/delete internally.
.push_back()/.pop_back() work on the end in amortized O(1); [] is fast but unchecked, .at() is checked.
std::list is a doubly-linked list — no [], but O(1) insert/erase once you have a position.
std::deque supports fast push/pop on both ends, unlike vector.
Default to vector unless you have a concrete reason to reach for list or deque.
🧩 Knowledge Check — Lesson 15
Answer all 5 questions to test your understanding. Instant feedback on every answer.
1. Which STL container should usually be your DEFAULT choice for a general-purpose resizable sequence?
2. What is the time complexity of accessing vec[i] on a std::vector?
3. You need to insert/remove items in the MIDDLE of a large sequence very frequently. Which container fits best?
4. Which vector method safely accesses an element and throws an exception if the index is out of range?
5. Which container lets you push and pop efficiently from BOTH the front and the back?
💪
Coding Challenge — Lesson 15
Apply what you learned · Intermediate Level

Now it's your turn to write real code. Complete the challenge below in any C++ compiler.

Challenge: Deduplicate & Summarize 🔢

Write a program that:

1. Fills a vector<int> with the numbers {4, 8, 4, 15, 16, 8, 23, 42, 15, 4}.
2. Removes duplicate values so each number appears only once (order doesn't matter).
3. Prints the resulting unique numbers.
4. Prints the sum and the average of the unique numbers.

Rules: Use only containers/algorithms covered so far (a std::set is a very effective way to deduplicate — you'll meet it properly in Lesson 16, but nothing stops you from trying it early).
💡 Show hints if you're stuck
  • The simplest dedupe trick: loop through the vector and only push_back() a value into a new vector if it's not already in there (check with a nested loop, or a std::set for speed).
  • To compute the average, divide the sum (as a double) by the count — watch out for integer division truncating the result!
  • You can loop with for (int n : uniqueNums) to both print and sum in the same pass.
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 15 Complete!

You now know the three core sequence containers. Next up: key-value lookups with map and set.

Module 15 of 26 Phase 4 — STL