STL Containers Overview

1. Sequence Containers

Sequence containers are data structures that allow elements to be accessed in a sequential manner. They are essentially what we traditionally think of as 'lists' of elements, but can differ in how elements are stored, accessed, and manipulated.

1.1. std::array

Description:

A container that encapsulates fixed-size arrays.

Properties and characteristics:

  • Fixed size
  • Stack-allocated
  • Contiguous memory

Common operations:

  • size()
  • front()
  • back()
  • operator[]

Example usage:

#include <array>
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << "First element: " << arr.front() << std::endl;
std::cout << "Third element: " << arr[2] << std::endl;

1.2. std::vector

Description:
Dynamic array implementation, automatically resizing when needed.

Properties and characteristics:

  • Dynamic size
  • Contiguous memory
  • Fast random access

Common operations and member functions:

  • push_back()
  • pop_back()
  • size()
  • capacity()
  • operator[]

Example usage:

#include <vector>
std::vector<int> vec = {1, 2, 3};
vec.push_back(4);
std::cout << "Vector size: " << vec.size() << std::endl;

1.3. std::deque

Description:
Double-ended queue that supports insertion and deletion at both ends efficiently.

Properties and characteristics:

  • Dynamic size
  • Non-contiguous memory
  • Fast random access

Common operations:

  • push_front()
  • push_back()
  • pop_front()
  • pop_back()

Example usage:

#include <deque>
std::deque<int> deq = {2, 3, 4};
deq.push_front(1);
deq.push_back(5);
std::cout << "Front element: " << deq.front() << std::endl;

1.4. std::list

Description:
Doubly linked list implementation.

Properties and characteristics:

  • Dynamic size
  • Non-contiguous memory
  • No direct access to the individual elements

Common operations:

  • push_front()
  • push_back()
  • pop_front()
  • pop_back()

Example usage:

#include <list>
std::list<int> lst = {1, 2, 3};
lst.push_back(4);
std::cout << "First element: " << lst.front() << std::endl;

1.5. std::forward_list

Description:
Singly linked list implementation.

Properties and characteristics:

  • Dynamic size
  • Non-contiguous memory
  • No direct access to the individual elements
  • More memory efficient than std::list due to singly-linked nature

Common operations:

  • push_front()
  • pop_front()

Example usage:

#include <forward_list>
std::forward_list<int> flist = {1, 2, 3};
flist.push_front(0);
std::cout << "First element: " << flist.front() << std::endl;

Summary of Sequence Containers

Operation array vector deque list forward_list Description of Operation
at() O(1) O(1) O(1) std::advance(l.begin(), i): O(n) std::advance(fl.begin(), i): O(n) Direct access to the specified element with bounds-checking.
operator[] O(1) O(1) O(1) std::advance(l.begin(), i): O(n) std::advance(fl.begin(), i): O(n) Direct access to the specified element.
front() O(1) O(1) O(1) O(1) O(1) Access the first element.
back() O(1) O(1) O(1) O(1) - Access the last element.
push_front() - v.insert(v.begin(), val): O(n) O(1) O(1) O(1) Insert element at the beginning.
pop_front() - v.erase(v.begin()): O(n) O(1) O(1) O(1) Remove first element.
push_back() - O(1)* O(1)* O(1) O(1) Insert element at the end.
pop_back() - O(1) O(1) O(1) - Remove last element.
insert() - O(n) O(n) O(1) O(1) Insert elements at the specified location.
erase() - O(n) O(n) O(1) O(1) Erase elements from the specified location.
clear() O(n) O(n) O(n) O(n) O(n) Remove all elements from the container.
resize() - O(n) O(n) O(n) - Change the number of elements.
emplace_front() - v.emplace(v.begin(), args...): O(n) O(1) O(1) O(1) Construct and insert element at the beginning.
emplace_back() - O(1)* O(1)* O(1) O(1) Construct and insert element at the end.
emplace() - O(n) O(n) O(1) O(1) Construct and insert element at the specified location.
data() O(1) O(1) - - - Direct access to the underlying array, which is sometimes required by legacy APIs.

For push_back() and emplace_back(), the average-case time complexity is O(1) with an occasional worst-case of O(n) when resizing is required (indicated as O(1)*).


2. Associative Containers

Associative containers are specialized containers designed for fast element lookup using keys. They are generally implemented using binary search trees or other balanced tree structures, ensuring logarithmic complexity for search operations.

2.1. std::set

Description:
An ordered collection of unique elements.

Properties and characteristics:

  • Elements are always arranged in order (according to a specified comparator or the default < operator).
  • Does not allow duplicate elements.
  • Insertion, removal, and search operations have logarithmic complexity.

Common operations:

  • insert()
  • erase()
  • find()

Example usage:

#include <set>
std::set<int> s = {1, 2, 3};
s.insert(4);
if (s.find(3) != s.end()) {
    std::cout << "3 is in the set!" << std::endl;
}

2.2. std::multiset

Description:
Similar to std::set, but allows duplicate elements.

Properties and characteristics:

  • Elements are ordered.
  • Allows multiple equivalent elements.

Common operations:

  • insert()
  • erase()
  • find()
  • count()

Example usage:

#include <set>
std::multiset<int> ms = {1, 1, 2, 2, 3};
ms.insert(1);
std::cout << "Number of 1s: " << ms.count(1) << std::endl;

2.3. std::map

Description:
An ordered collection of key-value pairs, where each key is unique.

Properties and characteristics:

  • Elements are always arranged in order of the key.
  • Direct access to values using keys.
  • Logarithmic complexity for search operations.

Common operations:

  • insert()
  • erase()
  • find()
  • operator[]

Example usage:

#include <map>
std::map<char, int> m = {{'a', 1}, {'b', 2}};
m['c'] = 3;
if (m.find('a') != m.end()) {
    std::cout << "'a' maps to " << m['a'] << std::endl;
}

2.4. std::multimap

Description:
Similar to std::map, but multiple keys can map to different values.

Properties and characteristics:

  • Elements are ordered by key.
  • Allows multiple key-value pairs with equivalent keys.

Common operations:

  • insert()
  • erase()
  • find()
  • count()

Example usage:

#include <map>
std::multimap<char, int> mm = {{'a', 1}, {'a', 2}};
mm.insert({'b', 1});
std::cout << "Number of pairs with 'a' as key: " << mm.count('a') << std::endl;

Summary of Associative Containers

Operation set/multiset map/multimap Description of Operation
begin() O(1) O(1) Returns an iterator to the beginning.
end() O(1) O(1) Returns an iterator to the end.
rbegin() O(1) O(1) Returns a reverse iterator to the beginning.
rend() O(1) O(1) Returns a reverse iterator to the end.
find() O(log n) O(log n) Finds an element with a specific key.
count() O(log n) + k O(log n) + k Counts elements with a specific key (k is number of key duplicates).
lower_bound() O(log n) O(log n) Returns an iterator to the lower bound.
upper_bound() O(log n) O(log n) Returns an iterator to the upper bound.
equal_range() O(log n) O(log n) Returns a range of elements matching a specific key.
insert() O(log n) O(log n) Inserts elements into the container.
erase() O(log n) O(log n) Erases elements from the container.
clear() O(n) O(n) Removes all elements from the container.
emplace() O(log n) O(log n) Constructs and inserts element into the container.
emplace_hint() O(1) amortized O(1) amortized Constructs and inserts with a hint.
key_comp() O(1) O(1) Returns the key compare function.
value_comp() O(1) O(1) Returns the value compare function (map/multimap only).

3. Unordered Associative Containers

Unordered associative containers are similar to their ordered counterparts but use hashing to achieve average constant-time complexity for most operations. They don't maintain any order among elements, leading to faster performance at the cost of non-sequential access.

3.1. std::unordered_set

Description: A collection of unique elements, where the position of each element is determined by its value.

Properties and characteristics:

  • Elements are not ordered.
  • Does not allow duplicate elements.
  • Uses hashing for average constant-time complexity for search operations.

Common operations:

  • insert()
  • erase()
  • find()

Example usage:

#include <unordered_set>
std::unordered_set<int> uset = {1, 2, 3};
uset.insert(4);
if (uset.find(3) != uset.end()) {
    std::cout << "3 is in the unordered set!" << std::endl;
}

3.2. std::unordered_multiset

Description: Like std::unordered_set but allows duplicate elements.

Properties and characteristics:

  • Elements are not ordered.
  • Allows multiple equivalent elements.

Common operations:

  • insert()
  • erase()
  • find()
  • count()

Example usage:

#include <unordered_set>
std::unordered_multiset<int> ums = {1, 1, 2, 2, 3};
ums.insert(1);
std::cout << "Number of 1s: " << ums.count(1) << std::endl;

3.3. std::unordered_map

Description: A collection of key-value pairs, where the position of each pair is determined by its key's value. Each key is unique.

Properties and characteristics:

  • Elements are not ordered by key.
  • Direct access to values using keys.
  • Uses hashing mechanism.

Common operations:

  • insert()
  • erase()
  • find()
  • operator[]

Example usage:

#include <unordered_map>
std::unordered_map<char, int> um = {{'a', 1}, {'b', 2}};
um['c'] = 3;
if (um.find('a') != um.end()) {
    std::cout << "'a' maps to " << um['a'] << std::endl;
}

3.4. std::unordered_multimap

Description: Like std::unordered_map, but multiple keys can map to different values.

Properties and characteristics:

  • Elements are not ordered by key.
  • Allows multiple key-value pairs with equivalent keys.

Common operations:

  • insert()
  • erase()
  • find()
  • count()

Example usage:

#include <unordered_map>
std::unordered_multimap<char, int> umm = {{'a', 1}, {'a', 2}};
umm.insert({'b', 1});
std::cout << "Number of pairs with 'a' as key: " << umm.count('a') << std::endl;

Summary of Unordered Associative Containers

Operation unordered_set/multiset unordered_map/multimap Description of Operation
begin() O(1) O(1) Returns an iterator to the beginning.
end() O(1) O(1) Returns an iterator to the end.
find() O(1) avg, O(n) worst O(1) avg, O(n) worst Finds an element with a specific key.
count() O(1) avg, O(n) worst O(1) avg, O(n) worst Counts elements with a specific key.
insert() O(1) avg, O(n) worst O(1) avg, O(n) worst Inserts elements into the container.
erase() O(1) avg, O(n) worst O(1) avg, O(n) worst Erases elements from the container.
clear() O(n) O(n) Removes all elements from the container.
emplace() O(1) avg, O(n) worst O(1) avg, O(n) worst Constructs and inserts element into the container.
emplace_hint() O(1) avg, O(n) worst O(1) avg, O(n) worst Constructs and inserts with a hint.
bucket_count() O(1) O(1) Returns the number of buckets in the container.
max_bucket_count() O(1) O(1) Returns the maximum number of buckets.
bucket_size(n) O(1) avg, O(n) worst O(1) avg, O(n) worst Returns the number of elements in bucket n.
bucket(k) O(1) avg, O(n) worst O(1) avg, O(n) worst Returns the bucket for a specific key.
load_factor() O(1) O(1) Returns the load factor.
max_load_factor() O(1) O(1) Returns the maximum load factor.
rehash(n) O(n) O(n) Rehashes the container to have at least n buckets.
reserve(n) O(n) O(n) Sets the number of elements for which space is reserved.

4. Container Adaptors

Container adaptors are not standalone containers. Instead, they provide a modified interface to other containers. Think of them as wrappers that restrict or augment the functionalities of underlying containers, often to suit specific data structure requirements.

4.1. std::stack

Description:
A container adaptor that provides a Last-In-First-Out (LIFO) data structure.

Properties and characteristics:

  • Elements are inserted and removed from the top.
  • Underlying container can be std::deque, std::list, or std::vector (default is std::deque).
  • Does not support iteration.

Common operations:

  • push()
  • pop()
  • top()
  • empty()
  • size()

Example usage:

#include <stack>
std::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
std::cout << "Top element: " << st.top() << std::endl; // Outputs: 3
st.pop();
std::cout << "Top element after pop: " << st.top() << std::endl; // Outputs: 2

4.2. std::queue

Description:
A container adaptor that provides a First-In-First-Out (FIFO) data structure.

Properties and characteristics:

  • Elements are inserted at the end and removed from the front.
  • Default underlying container is std::deque but can also be std::list.
  • Doesn't support random access.

Common operations:

  • push()
  • pop()
  • front()
  • back()
  • empty()
  • size()

Example usage:

#include <queue>
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
std::cout << "Front element: " << q.front() << std::endl; // Outputs: 1
q.pop();
std::cout << "Front element after pop: " << q.front() << std::endl; // Outputs: 2

4.3. std::priority_queue

Description:
A container adaptor that provides elements in priority order.

Properties and characteristics:

  • By default, the largest element is considered the highest priority (though this can be changed with a custom comparator).
  • Underlying container can be std::vector (default) or std::deque.
  • Implemented using a binary heap.

Common operations:

  • push()
  • pop()
  • top()
  • empty()
  • size()

Example usage:

#include <queue>
std::priority_queue<int> pq;
pq.push(1);
pq.push(3);
pq.push(2);
std::cout << "Top element: " << pq.top() << std::endl; // Outputs: 3
pq.pop();
std::cout << "Top element after pop: " << pq.top() << std::endl; // Outputs: 2

Summary of Container Adaptors

Operation stack queue priority_queue Description of Operation
push() O(1) O(1) O(log n) Inserts an element at the top/front.
pop() O(1) O(1) O(log n) Removes the top/front element.
top() O(1) O(1) Accesses the top element (does not remove).
front() O(1) Accesses the first element (does not remove).
back() O(1) Accesses the last element (does not remove).
empty() O(1) O(1) O(1) Checks whether the container is empty.
size() O(1) O(1) O(1) Returns the number of elements in the container.
emplace() O(1) O(1) O(log n) Constructs an element in-place in the container.
swap(container&) O(1) O(1) O(1) Swaps the contents of the container with those of another.

It's important to remember that stack and queue are implemented using underlying container types (typically deque, but can also be list or vector), and their time complexities generally reflect the operations on these containers. On the other hand, priority_queue is typically implemented using a binary heap via a vector.


5. Memory and Performance Considerations

Choosing the right container is not just about the functionality it provides, but also about how efficiently it manages memory and how well it performs in different scenarios.

5.1 How Different Containers Manage Memory:

  1. std::vector:

    • Uses a dynamic array internally.
    • Requires contiguous memory.
    • When resizing, new memory is allocated, and elements are copied or moved. Excess capacity might be allocated to avoid frequent reallocations.
  2. std::list:

    • Uses a doubly-linked list.
    • Elements are not stored in contiguous memory.
    • Each element requires additional memory for two pointers (previous and next).
  3. std::deque:

    • Maintains multiple blocks of arrays, rather than a single block like vector.
    • Provides fast insertion at both ends.
    • Not contiguous, but offers almost similar random access speed as a vector.
  4. Associative containers (set, map, multiset, multimap):

    • Typically implemented as balanced binary search trees (like red-black trees).
    • Requires pointers to maintain the tree structure, leading to higher memory overhead than a vector or an array.
  5. Unordered associative containers (unordered_set, unordered_map, etc.):

    • Implemented using hash tables.
    • Memory overhead for managing buckets and handling collisions.

5.2 When to Use Which Container for Performance:

  1. std::vector:

    • When you need to access elements by index frequently.
    • If most of the insertions are at the end.
    • Minimum overhead in terms of memory (compared to list or deque).
  2. std::list:

    • When you require frequent insertions or deletions in the middle.
    • Not ideal for random access.
  3. std::deque:

    • If you need fast insertion or deletion at both ends.
    • When you require almost random access performance, but with the flexibility of insertion at both ends.
  4. Associative containers:

    • When you want elements to be always in a sorted order (set, map).
    • For quick look-ups, insertions, and deletions (logarithmic time).
  5. Unordered associative containers:

    • When you don't need sorted order but want quick average constant-time look-ups.

5.3 Trade-offs between containers (e.g., std::vector vs. std::list):

  • Memory:

    • std::vector is more memory-efficient since it just has an array with size and capacity information. std::list, on the other hand, has memory overhead for its next and previous pointers.
  • Performance:

    • Random access: std::vector excels due to contiguous memory, making cache locality better. std::list performs poorly due to its node-based nature.
    • Insertion/Deletion: std::list is faster for insertions or deletions in the middle as it only involves pointer changes, while std::vector might require moving a lot of elements.
  • Reallocation:

    • std::vector might need to reallocate and copy its elements to a bigger memory region when it grows. This can be a costly operation. std::list doesn't have this issue, as new elements are simply new nodes linked into the list.
  • Iterators:

    • Iterators for std::vector can be invalidated upon resizing. In std::list, iterators are not invalidated unless the corresponding element is deleted.

6. Summary

Container Underlying Data Structure Time Complexity (Avg.) Ordered/Unordered Iterator Types Contiguous Memory? Memory Location Fixed/Dynamic Size
std::vector Dynamic Array Access: O(1)
Insert/Delete End: O(1)
Insert/Delete Middle: O(n)
Ordered Random Access Yes Heap Dynamic
std::list Doubly-Linked List Access: O(n)
Insert/Delete: O(1)
Ordered Bidirectional No Heap Dynamic
std::deque Block of Arrays Access: O(1)
Insert/Delete Front/End: O(1)
Ordered Random Access No (but blocks are) Heap Dynamic
std::forward_list Singly-Linked List Access: O(n)
Insert/Delete: O(1)
Ordered Forward No Heap Dynamic
std::array Fixed Size Array Access: O(1)
Insert/Delete: Not applicable
Ordered Random Access Yes Stack or Heap (based on declaration) Fixed
std::set/multiset Balanced BST (e.g., Red-Black Tree) Access: O(log n)
Insert/Delete: O(log n)
Ordered Bidirectional No Heap Dynamic
std::map/multimap Balanced BST (e.g., Red-Black Tree) Access: O(log n)
Insert/Delete: O(log n)
Ordered Bidirectional No Heap Dynamic
std::unordered_set/multiset Hash Table Access: O(1)
Insert/Delete: O(1) amortized
Unordered Forward No (buckets can be, but not elements) Heap Dynamic
std::unordered_map/multimap Hash Table Access: O(1)
Insert/Delete: O(1) amortized
Unordered Forward No (buckets can be, but not elements) Heap Dynamic
std::stack Depends on underlying container (e.g., vector, list) Push/Pop/Top: O(1) N/A Depends on underlying container Depends on underlying container Depends on underlying container Depends on underlying container
std::queue Depends on underlying container (e.g., deque, list) Push/Pop/Front: O(1) N/A Depends on underlying container Depends on underlying container Depends on underlying container Depends on underlying container
std::priority_queue Binary Max Heap Insert: O(log n)
Delete Max: O(1)
Max Element: O(1)
N/A N/A Typically Yes (e.g., when backed by a vector) Heap Dynamic

Please note the following:

  1. The time complexities mentioned are average complexities. In some cases, worst-case scenarios can differ.

  2. In the case of hash tables (unordered_ containers), while the average case for operations like insert and delete is constant time, the worst case can be linear (especially when there are too many collisions and the table needs to be rehashed).

  3. The iterator type mentioned for each container is the most powerful iterator it supports. For instance, while a vector supports random access iterators, it can also be used with bidirectional or forward iterators.

  4. std::stack and std::queue can be based on different underlying containers, so their complexities and iterator types can vary accordingly.


7. Q&A

Question 1: You have a std::vector<int> nums with some integers. Describe a way to remove all duplicate integers while maintaining the original order using STL containers and algorithms.

Answer 1: To remove all duplicate integers while maintaining the original order, one can use the combination of std::unique and std::vector::erase:

std::sort(nums.begin(), nums.end());
auto last = std::unique(nums.begin(), nums.end());
nums.erase(last, nums.end());

The std::unique function removes consecutive duplicate elements in the vector and returns an iterator pointing just past the last unique element. Then, std::vector::erase is used to remove the redundant elements from the end.


Question 2: When would you prefer to use a std::list over a std::vector, and why?

Answer 2: std::list should be preferred over std::vector in scenarios where: (1) Frequent insertions and deletions in the middle of the sequence are common, as these operations are O(1) for std::list (provided you have an iterator to the position), whereas they can take up to O(n) for std::vector. (2) If your operations don't need to randomly access elements by their index, but you'll mostly be working with iterators. (3) In environments where it's undesirable for memory to be reallocated and elements to be moved, like when holding references or pointers to the contained objects.


Question 3: You are given a task to store a collection of unique student IDs (represented as integers) where the order of insertion is irrelevant, but you need efficient look-up times. Which STL container would be the most appropriate choice and why?

Answer 3: For efficient look-up times, a std::unordered_set would be the most appropriate choice. It offers O(1) average-time complexity for look-ups as it uses hashing internally. The elements in an unordered_set are unique, ensuring that each student ID is stored only once.


Question 4: Explain the main difference between a std::map and a std::unordered_map. What are the trade-offs between using one over the other?

Answer 4: The primary difference is in how they internally manage elements: - std::map stores its elements in a balanced binary search tree (like a Red-Black Tree), which means the elements are always ordered. - std::unordered_map uses a hash table, so the elements are not ordered.

Trade-offs: - std::map: Slower average access/insert/delete times (O(log n)), but it maintains order. - std::unordered_map: Faster average access/insert/delete times (O(1) on average), but consumes more memory and does not maintain order.


Question 5: You have a std::deque<int> numbers. Describe an STL method to insert a number at both the beginning and the end of this deque. Additionally, explain when you might prefer using a std::deque over other sequence containers.

Answer 5: You can use the push_front and push_back member functions of std::deque:

numbers.push_front(10); // Inserts 10 at the beginning.
numbers.push_back(20);  // Inserts 20 at the end.

A std::deque might be preferred over other sequence containers like std::vector or std::list when you need fast insertions/removals at both the beginning and end of the container. While a std::vector provides fast insertions/removals at the end, its operations at the beginning are slow. On the other hand, a std::list offers fast operations at both ends but lacks cache locality, making element access slower than std::deque.


Question 6: How would you insert multiple values in the middle of a std::vector<int> at once? Provide an example using STL methods.

Answer 6: To insert multiple values in the middle of a std::vector<int>, you can use the insert member function with an iterator to the desired position and two iterators denoting the range of values to insert. Example:

std::vector<int> vec = {1, 2, 5, 6};
std::vector<int> to_insert = {3, 4};
vec.insert(vec.begin() + 2, to_insert.begin(), to_insert.end());
// vec now holds {1, 2, 3, 4, 5, 6}

Question 7: What is the primary reason behind the difference in time complexity when accessing an element in a std::list versus a std::vector?

Answer 7: The primary reason is the underlying data structure. A std::vector is based on a dynamic array, allowing direct access to its elements, which results in O(1) time complexity for accessing an element. On the other hand, a std::list is implemented as a doubly-linked list where elements are stored in nodes. To access an element, you must traverse the list from the beginning (or the end), leading to O(n) time complexity.


Question 8: Explain the significance of bucket-related functions in std::unordered_map and why they might be useful.

Answer 8: std::unordered_map is implemented using a hash table. In this context, a bucket is a slot in the hash table's array, potentially holding multiple key-value pairs due to hash collisions. Bucket-related functions allow developers to: - Inspect the number of buckets (bucket_count). - Check the number of elements in a specific bucket (bucket_size). - Find the bucket for a specific key (bucket).

They're useful to analyze and optimize the performance of the hash table by understanding its distribution and collision rate. For instance, if many elements are clustered in a few buckets (leading to longer linked lists), you might want to use a better hash function or resize the container.


Question 9: Given two std::set<int> named setA and setB, how can you get the intersection of these sets using STL functions?

Answer 9: To get the intersection of two std::sets, you can use the std::set_intersection algorithm:

std::set<int> setA = {1, 2, 3, 4};
std::set<int> setB = {3, 4, 5, 6};
std::set<int> result;

std::set_intersection(setA.begin(), setA.end(), setB.begin(), setB.end(), std::inserter(result, result.begin()));
// result now holds {3, 4}

Question 10: Why can't you use square brackets [] to access elements in a std::list? How would you access the nth element?

Answer 10: The square brackets operator [] relies on direct access, suitable for continuous memory data structures like arrays (and thus, std::vector). Since std::list is a doubly-linked list, direct access isn't efficient. To access the nth element, you'd typically use a loop or std::advance with an iterator:

std::list<int>::iterator it = myList.begin();
std::advance(it, n); // move the iterator n positions forward
int value = *it;

Note: Accessing the nth element in a list is an O(n) operation.


Question 11: Describe the differences between a std::stack and a std::queue in terms of underlying data structures and primary operations.

Answer 11: Both std::stack and std::queue are container adaptors, not containers themselves. They can use different underlying containers like std::vector, std::deque, or std::list, though the default is std::deque. - A std::stack supports LIFO (Last-In-First-Out) semantics. Its primary operations are push (to add an element to the top), pop (to remove the top element), and top (to access the top element without popping it). - A std::queue supports FIFO (First-In-First-Out) semantics. Its primary operations are push (to add an element to the back), pop (to remove the front element), and front (to access the front element without popping it).


Question 12: How would you determine the size of a std::array<int, 5> and check if it's empty?

Answer 12: For a std::array<int, 5>, its size is compile-time fixed and is 5. You can get the size using the size() member function:

std::array<int, 5> arr;
size_t s = arr.size(); // s will be 5

To check if it's empty, use the empty() member function. However, for a std::array, this function always returns false because the size is fixed and non-zero:

bool is_empty = arr.empty(); // is_empty will be false

Question 13: What makes std::forward_list different from std::list, and in what scenarios might you prefer to use one over the other?

Answer 13: The primary difference is that std::forward_list is a singly-linked list, whereas std::list is a doubly-linked list. This means std::forward_list only supports forward traversal. Because std::forward_list doesn't store previous pointers, it's more memory-efficient than std::list. You might prefer std::forward_list when: - You only need forward traversal. - Memory efficiency is a concern. - You don't require operations that need backward traversal or bidirectional iterators.


Question 14: Explain when and why you'd want to use std::multiset or std::multimap instead of their non-multi counterparts.

Answer 14: std::multiset and std::multimap allow multiple entries with equivalent keys, unlike std::set and std::map which enforce unique keys. You'd use the multi variants when: - You need to store multiple values under an equivalent key. - Your application requires a sorted container where duplicates are permissible. For instance, if you're cataloging books in a library where multiple copies of the same book (with the same ISBN) can exist, std::multimap would be suitable.


Question 15: How does the efficiency of insertion and deletion in a std::vector compare to a std::deque? Give scenarios where one might be preferred over the other.

Answer 15: - std::vector: Insertion/deletion at the end is O(1) (amortized). However, insertion/deletion at the beginning or in the middle is O(n) because it might require shifting elements. - std::deque: Insertion/deletion at both the beginning and the end are fast operations, typically O(1). Insertion/deletion in the middle can be O(n) but can be faster than std::vector depending on the position due to its segmented memory.

Scenarios: - Prefer std::vector when most insertions/deletions are at the end, or when you need data to be stored contiguously for operations like interfacing with APIs that require raw pointers. - Prefer std::deque when you have frequent insertions/deletions at both the beginning and the end, or when memory efficiency isn't the primary concern.


Question 16: Why would someone choose to use a std::array over a C-style array?

Answer 16: There are several advantages of std::array over C-style arrays: 1. Type Safety: std::array provides type safety, ensuring no out-of-bounds access. 2. Fixed Size: The size of std::array is known at compile-time, making it safer than dynamic arrays. 3. STL Compatibility: Being an STL container, std::array can be used seamlessly with STL algorithms and iterators. 4. Utility Functions: std::array provides handy member functions like at(), size(), and fill().


Question 17: In what situations would using a std::set be more appropriate than using a std::vector?

Answer 17: std::set is a sorted associative container that contains unique elements. You would prefer a std::set over a std::vector when: 1. Uniqueness: You need to ensure all elements are unique. 2. Ordered Data: You want data to be automatically sorted. 3. Efficient Lookups: You need efficient lookups, insertions, and deletions, irrespective of the data size. std::set typically provides logarithmic time complexities for these operations.


Question 18: How does the underlying data structure of a std::stack differ from that of a std::queue?

Answer 18: The underlying data structures of both std::stack and std::queue can be std::deque, std::list, or even std::vector (though not typically used). The main distinction is not the underlying data structure but the access pattern: - std::stack provides LIFO (Last-In-First-Out) access, so elements are both inserted and removed from the same end. - std::queue provides FIFO (First-In-First-Out) access, where elements are inserted at the back and removed from the front.


Question 19: What is the primary advantage of using a std::unordered_set over a std::set?

Answer 19: The primary advantage is in terms of time complexity for common operations. For a std::unordered_set: - Insertion, deletion, and lookups have average constant time complexity O(1) (though worst-case can be O(n) in situations like hash collisions). This efficiency is because std::unordered_set is implemented using a hash table. On the other hand, std::set uses a balanced binary tree, resulting in logarithmic time complexities. However, it's worth noting that std::set maintains order, while std::unordered_set does not.


Question 20: How does a std::priority_queue determine the priority of its elements?

Answer 20: A std::priority_queue is a container adaptor that provides constant time retrieval and logarithmic time deletion of the highest (or lowest, based on the provided criteria) element. By default, it uses std::less for comparison, meaning it will be a max-heap (highest element has the priority). However, you can change its behavior by providing a different comparator. The underlying data structure is usually a std::vector, but it can also be a std::deque. The essential part is that it satisfies the properties of a heap.