Hi everyone 
In this tutorial we will talk about Set
So, What is Set actually?
Set is an associative container of C++ STL that stores only unique elements. Each element inside a set is a key — there are no associated values (that is the job of map). The elements are always kept sorted in ascending order automatically.
Think of set as map without the values — just the keys. Or think of it as a sorted container that silently drops duplicates for us.
Since set stores only unique keys, it does not have the [ ] subscript operator or the at() method. Set is not about looking up values by keys — it is about answering the question “is this element present?” or iterating through the elements in sorted order.
Set’s underlying data structure is a Self-Balancing Binary Search Tree (usually a Red-Black Tree). That is why all its major operations — insertion, deletion, search — take O(log n) time, and that is also why the elements stay sorted automatically.
Header File Inclusion for set
#include <set>
Declaration & Initialization
Declaring an empty set —
set <int> numbers;
The set is empty right now.
Declaring a set with values —
From C++11 onwards, we can initialize a set with values using curly braces —
set <int> numbers = { 50, 20, 40, 10, 30 };
After Initialization numbers will look like this —
| 10 | 20 | 30 | 40 | 50 |
Notice that even though we inserted the values in the order 50, 20, 40, 10, 30, set has sorted them in ascending order.
Declaring a set with duplicate values —
If we try to initialize a set with duplicate values, the duplicates are silently dropped —
set <int> numbers = { 10, 20, 30, 20, 40, 10, 50 };
After Initialization numbers will look like this —
| 10 | 20 | 30 | 40 | 50 |
The duplicate 10 and 20 were ignored. This is one of the most common uses of set — to deduplicate a collection of values.
Declaring sets of different types —
Element type can be anything that supports the < operator —
set <double> scores;
set <string> nameList;
set <char> letters;
set <pair<int, int> > points;
Declaring a set in descending order —
By default, set stores elements in ascending order. If we want elements in descending order, we can pass greater<> as the second template parameter —
set <int, greater<int> > reverseSet = { 10, 20, 30, 40, 50 };
After Initialization reverseSet will look like this —
| 50 | 40 | 30 | 20 | 10 |
Insertion
Insertion using insert() method
The main way to add elements to a set is the insert() method —
set <int> numbers;
numbers.insert(20);
numbers.insert(50);
numbers.insert(10);
numbers.insert(40);
numbers.insert(30);
After Insertion numbers will look like this —
| 10 | 20 | 30 | 40 | 50 |
Inserting a duplicate value
If we try to insert a value that already exists, the insertion is silently ignored —
numbers.insert(30);
numbers will still look like this —
| 10 | 20 | 30 | 40 | 50 |
💡 Quick tip on insert() return value — insert() actually returns a
pair<iterator, bool>. The bool tells us whether the insertion actually happened (true) or the value was already there (false). This is useful when we want to check for duplicates during insertion.auto result = numbers.insert(30); if (result.second) { cout << "Inserted successfully" << endl; } else { cout << "Already exists" << endl; }
Insertion using emplace() method
emplace() constructs the element in place, avoiding an extra copy —
numbers.emplace(60);
numbers.emplace(70);
Inserting a range of values
We can also insert a range of values from another container —
vector <int> extra = { 80, 90, 100 };
numbers.insert(extra.begin(), extra.end());
Accessing the element
Set does not support [ ] or at(). Access happens through iterators returned by find(), or by iterating through the set directly.
find() method
find() returns an iterator to the element if it exists, otherwise it returns end() —
auto it = numbers.find(30);
if (it != numbers.end())
{
cout << "Found " << *it << endl;
}
else
{
cout << "Not found" << endl;
}
Output will be – Found 30
Checking if an element exists
count() method
Since set elements are unique, count() returns either 0 (not present) or 1 (present) —
if (numbers.count(30))
{
cout << "30 is in the set" << endl;
}
contains() method (C++20)
From C++20 onwards, we have a cleaner way —
if (numbers.contains(30))
{
cout << "30 is in the set" << endl;
}
Iterating through a set
Range-based for loop
The cleanest way to iterate through a set is using a range-based for loop —
for (auto &value : numbers)
{
cout << value << " ";
}
Output will be – 10 20 30 40 50 60 70 80 90 100
Using iterator
We can also iterate using iterators directly —
set <int>::iterator it;
for (it = numbers.begin(); it != numbers.end(); it++)
{
cout << *it << " ";
}
💡 Note on iteration order — No matter the order in which we inserted the values, set always iterates them in sorted order (ascending by default). That is one of the most powerful features of set.
Deletion
Deletion using erase() with value
We can delete a specific element by passing the value directly to erase() —
numbers.erase(30);
After deletion, the element 30 will be removed from the set.
Deletion using erase() with iterator
We can also delete using an iterator —
auto it = numbers.find(40);
if (it != numbers.end())
{
numbers.erase(it);
}
Deletion using erase() with range
We can delete a range of elements by passing the start and end iterators —
// removing all elements from 60 onwards
numbers.erase(numbers.find(60), numbers.end());
clear() method
If we want to remove all elements of the set, we can use clear() —
numbers.clear();
After clear(), numbers will be empty.
Size and Empty
cout << numbers.size() << endl;
if (numbers.empty())
{
cout << "Set is empty" << endl;
}
size() returns the number of elements, and empty() returns true if the set has no elements.
Swapping
swap() method
STL set has a method named swap(), which swaps all the elements between two sets —
set <int> setOne = { 1, 2, 3 };
set <int> setTwo = { 10, 20, 30, 40, 50 };
setOne will look like this —
| 1 | 2 | 3 |
setTwo will look like this —
| 10 | 20 | 30 | 40 | 50 |
Now, let’s swap the elements of two sets —
// swapping values between two sets
setOne.swap(setTwo);
After Swapping setOne will look like this —
| 10 | 20 | 30 | 40 | 50 |
After Swapping setTwo will look like this —
| 1 | 2 | 3 |
lower_bound() and upper_bound()
Since set elements are stored in sorted order, we can use lower_bound() and upper_bound() just like we do for sorted vectors.
lower_bound(value) — returns an iterator to the first element that is not less than the given value.
upper_bound(value) — returns an iterator to the first element that is strictly greater than the given value.
set <int> numbers = { 10, 20, 30, 40, 50 };
auto lb = numbers.lower_bound(30);
auto ub = numbers.upper_bound(30);
cout << *lb << endl;
cout << *ub << endl;
Output will be —
30
40
These are handy for range queries — for example, finding all values in the set between two bounds.
Accessing the smallest and largest element
Since set is always sorted, the smallest element is at begin() and the largest element is at prev(end()) or rbegin() —
set <int> numbers = { 40, 10, 50, 20, 30 };
// smallest element
cout << *numbers.begin() << endl;
// largest element
cout << *numbers.rbegin() << endl;
Output will be —
10
50
💡 Quick tip on min/max access — Unlike priority_queue (which only gives us the top element), set gives us O(log n) access to both the smallest and largest elements. This makes set a great choice when we need to repeatedly query both extremes of a collection.
Set vs unordered_set
Just like map has an unordered_map cousin, set has an unordered_set cousin. This is a common confusion, so it is worth a quick note —
| Property | set | unordered_set |
|---|---|---|
| Underlying data structure | Red-Black Tree | Hash Table |
| Order of elements | Sorted | No specific order |
| Insertion / Deletion / Search | O(log n) | O(1) average, O(n) worst |
| lower_bound() / upper_bound() | Available | Not available |
| Use when | We need sorted elements or ordered traversal | We just need fast lookup, order does not matter |
Time Complexity
| Operation | Description | Time Complexity |
|---|---|---|
| insert() | Insert an element | O(log n) |
| emplace() | Construct and insert an element in place | O(log n) |
| erase() | Remove an element by value or iterator | O(log n) |
| find() | Find an element | O(log n) |
| count() | Check if an element exists (returns 0 or 1) | O(log n) |
| contains() | Check if an element exists (C++20) | O(log n) |
| lower_bound() | Iterator to first value not less than given value | O(log n) |
| upper_bound() | Iterator to first value greater than given value | O(log n) |
| begin() / end() | Iterators to the beginning and end | O(1) |
| rbegin() / rend() | Reverse iterators (access largest, smallest) | O(1) |
| size() | Returns the number of elements | O(1) |
| empty() | Returns true if set is empty | O(1) |
| clear() | Removes all elements | O(n) |
| swap() | Swaps contents of two sets | O(1) |
Applications
-
-
-
- Removing duplicates from a collection of values
- Checking if an element has been seen before (visited-set in BFS/DFS)
- Maintaining a sorted collection that supports fast insertions and deletions
- Finding the k-th smallest or largest element via iterator advance
- Range queries with
lower_bound()andupper_bound() - Sliding window problems where we need both min and max of the current window
- Implementing set operations like union, intersection, and difference (with std::set_union, std::set_intersection, std::set_difference)
- Storing unique identifiers like user IDs, words in a dictionary, or distinct IP addresses
-
-
In the next tutorial we will talk about another useful STL container.
Until then
Happy Coding