Skip to content

std list

std::list

std::list is a doubly-linked list container that provides fast insertion and deletion at any position, but trades this flexibility for poor cache locality and no random access. Understanding when to use std::list vs std::vector is crucial for making informed design decisions.

What is std::list?

std::list is a sequence container implemented as a doubly-linked list. Each element contains:

  • Data: The actual value
  • Next pointer: Points to the next element
  • Previous pointer: Points to the previous element

This structure provides:

  • Fast insertion/deletion: O(1) at any position
  • Stable iterators: Iterators remain valid during modifications
  • No memory reallocation: Elements are never moved in memory
  • Bidirectional iteration: Can iterate forward and backward

Basic Usage

Creating and Initializing Lists

cpp
#include <list>
#include <iostream>

// Empty list
std::list<int> empty_list;

// List with initial size
std::list<int> sized_list(5);  // 5 elements, all 0

// List with initial size and value
std::list<int> filled_list(3, 42);  // 3 elements, all 42

// List from initializer list
std::list<int> init_list = {1, 2, 3, 4, 5};

// List from another list
std::list<int> copy_list(init_list);

Adding and Removing Elements

cpp
std::list<int> my_list = {1, 2, 3};

// Add elements at the beginning
my_list.push_front(0);        // my_list = {0, 1, 2, 3}
my_list.emplace_front(-1);    // my_list = {-1, 0, 1, 2, 3}

// Add elements at the end
my_list.push_back(4);         // my_list = {-1, 0, 1, 2, 3, 4}
my_list.emplace_back(5);      // my_list = {-1, 0, 1, 2, 3, 4, 5}

// Insert elements at specific positions
auto it = std::next(my_list.begin(), 2);  // Iterator to position 2
my_list.insert(it, 10);       // my_list = {-1, 0, 10, 1, 2, 3, 4, 5}

// Remove elements
my_list.pop_front();           // my_list = {0, 10, 1, 2, 3, 4, 5}
my_list.pop_back();            // my_list = {0, 10, 1, 2, 3, 4}

// Remove specific elements
my_list.remove(10);            // my_list = {0, 1, 2, 3, 4}

// Clear all elements
my_list.clear();               // my_list = {}

Performance Characteristics

Time Complexity

OperationTime ComplexityNotes
Insert/Delete at BeginningO(1)push_front, pop_front
Insert/Delete at EndO(1)push_back, pop_back
Insert/Delete in MiddleO(1)Once you have an iterator
Random AccessO(n)Must iterate to position
Search by ValueO(n)Linear search required

Memory Layout and Cache Performance

cpp
std::list<int> data_list = {1, 2, 3, 4, 5};

// Memory layout (conceptual):
// [1] -> [2] -> [3] -> [4] -> [5]
//   |      |      |      |      |
//   v      v      v      v      v
// [prev] [prev] [prev] [prev] [prev]

// Poor cache performance - elements scattered in memory
for (auto it = data_list.begin(); it != data_list.end(); ++it) {
    std::cout << *it << " ";  // Cache misses: elements not adjacent
}

Key Insight: Each element can be anywhere in memory, causing cache misses during iteration.

Common Operations

Accessing Elements

cpp
std::list<int> my_list = {10, 20, 30, 40, 50};

// Front and back access (O(1))
int front = my_list.front();  // 10
int back = my_list.back();    // 50

// NO random access - this won't compile:
// int third = my_list[2];  // Error: no operator// Must iterate to access middle elements
auto it = my_list.begin();
std::advance(it, 2);         // Move iterator 2 positions forward
int third = *it;              // 30

// Or use std::next (C++11)
int third_alt = *std::next(my_list.begin(), 2);  // 30

Iterating Over Lists

cpp
std::list<int> my_list = {1, 2, 3, 4, 5};

// Forward iteration
for (auto it = my_list.begin(); it != my_list.end(); ++it) {
    std::cout << *it << " ";  // 1 2 3 4 5
}

// Range-based for loop (C++11)
for (const auto& item : my_list) {
    std::cout << item << " ";  // 1 2 3 4 5
}

// Reverse iteration
for (auto it = my_list.rbegin(); it != my_list.rend(); ++it) {
    std::cout << *it << " ";  // 5 4 3 2 1
}

// Bidirectional iteration
auto it = my_list.begin();
std::cout << *it << " ";      // 1
++it;                         // Move forward
std::cout << *it << " ";      // 2
--it;                         // Move backward
std::cout << *it << " ";      // 1

Finding and Searching

cpp
#include <algorithm>

std::list<int> my_list = {10, 20, 30, 40, 50};

// Find element (O(n) - must iterate)
auto it = std::find(my_list.begin(), my_list.end(), 30);
if (it != my_list.end()) {
    std::cout << "Found 30" << std::endl;
}

// Find element with custom predicate
auto even_it = std::find_if(my_list.begin(), my_list.end(), 
                           (int x) { return x % 2 == 0; });
if (even_it != my_list.end()) {
    std::cout << "First even number: " << *even_it << std::endl;
}

// Count occurrences
int count = std::count(my_list.begin(), my_list.end(), 20);  // 1

// Check if element exists
bool has_30 = std::find(my_list.begin(), my_list.end(), 30) != my_list.end();

Advanced Features

List-Specific Operations

cpp
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};

// Splice: move elements from one list to another
auto pos = std::next(list1.begin(), 1);  // Position after first element
list1.splice(pos, list2);                // list1 = {1, 4, 5, 6, 2, 3}, list2 = {}

// Merge: merge two sorted lists
std::list<int> sorted1 = {1, 3, 5};
std::list<int> sorted2 = {2, 4, 6};
sorted1.merge(sorted2);                   // sorted1 = {1, 2, 3, 4, 5, 6}, sorted2 = {}

// Sort: sort the list (uses merge sort internally)
std::list<int> unsorted = {3, 1, 4, 1, 5, 9, 2, 6};
unsorted.sort();                          // unsorted = {1, 1, 2, 3, 4, 5, 6, 9}

// Unique: remove consecutive duplicates
std::list<int> duplicates = {1, 1, 2, 2, 3, 3};
duplicates.unique();                      // duplicates = {1, 2, 3}

// Reverse: reverse the list
std::list<int> forward = {1, 2, 3, 4, 5};
forward.reverse();                         // forward = {5, 4, 3, 2, 1}

Iterator Stability

cpp
std::list<int> my_list = {1, 2, 3, 4, 5};

// Iterators remain valid during modifications
auto it1 = my_list.begin();               // Points to 1
auto it2 = std::next(my_list.begin(), 2); // Points to 3

// Insert element between it1 and it2
my_list.insert(it2, 10);                  // my_list = {1, 2, 10, 3, 4, 5}

// Iterators are still valid!
std::cout << *it1 << std::endl;           // Still prints 1
std::cout << *it2 << std::endl;           // Still prints 3

// Only iterators to modified elements become invalid
auto it3 = my_list.begin();
my_list.erase(it3);                       // Remove first element
// it3 is now invalid - don't use it!

When to Use std::list

Use std::list when:

  • You frequently insert/delete elements in the middle
  • You need stable iterators during modifications
  • You don't need random access to elements
  • You're implementing algorithms that benefit from O(1) insertion/deletion
  • Memory fragmentation is a concern

Consider alternatives when:

  • You need fast random access: Use std::vector
  • You mostly append at the end: Use std::vector
  • Cache performance is critical: Use std::vector
  • Memory usage is critical: Use std::vector (less overhead per element)
  • You need to sort frequently: Consider std::vector with std::sort

Summary

std::list is a powerful container that excels at:

  • Fast insertion/deletion at any position
  • Stable iterators during modifications
  • Efficient splicing and merging operations
  • Built-in sorting and deduplication

However, it comes with trade-offs:

  • Poor cache locality due to scattered memory
  • No random access - must iterate to reach elements
  • Higher memory overhead per element
  • Slower iteration compared to contiguous containers

Choose std::list when you need the flexibility of O(1) insertion/deletion and stable iterators, but be aware of the performance implications for sequential access. For most use cases where you're primarily iterating through data, std::vector will provide better performance due to superior cache locality.

Questions

Q: What is the time complexity of inserting an element in the middle of a std::list?

Inserting in the middle of a std::list is O(1) once you have an iterator to the position. This is because you only need to update a few pointers, unlike std::vector which requires shifting elements.

Q: What is the main disadvantage of std::list compared to std::vector?

The main disadvantage is poor cache locality. Elements are scattered in memory, causing cache misses when iterating. This can make std::list significantly slower than std::vector for sequential access, even though insertion/deletion is O(1).

Q: Which operation is NOT O(1) for std::list?

Finding an element by value is O(n) because std::list doesn't support random access. You must iterate through the list sequentially to find a specific value, unlike std::vector which can use algorithms like std::find more efficiently.

Q: When should you prefer std::list over std::vector?

std::list is better when you frequently insert/delete in the middle because it's O(1) vs O(n) for std::vector. However, if you mostly append at the end or need random access, std::vector is usually better.

Q: What happens to iterators when you insert/delete elements in std::list?

std::list iterators remain valid unless they point to the element being modified. This is a key advantage over std::vector, where insertions/deletions can invalidate many iterators due to memory reallocation.