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
, orstd::vector
(default isstd::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 bestd::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) orstd::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:
-
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.
-
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).
-
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.
-
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.
-
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:
-
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).
-
std::list:
- When you require frequent insertions or deletions in the middle.
- Not ideal for random access.
-
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.
-
Associative containers:
- When you want elements to be always in a sorted order (set, map).
- For quick look-ups, insertions, and deletions (logarithmic time).
-
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, whilestd::vector
might require moving a lot of elements.
- Random access:
-
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. Instd::list
, iterators are not invalidated unless the corresponding element is deleted.
- Iterators for
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:
-
The time complexities mentioned are average complexities. In some cases, worst-case scenarios can differ.
-
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). -
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.
-
std::stack
andstd::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::set
s, 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.