Module 8 · set and multiset: Sorted, Unique, Found in log n
set: Sorted, No Repeats, and Found in a Few Steps
In this lesson
- Declare a
std::set, insert values in any order, and walk them back sorted, with every repeat dropped. - Ask "is x there?" with
countandfind, erase a value, and read the smallest and the largest with*s.begin()and*s.rbegin(). - Explain in one sentence why a find takes only a few steps, and choose a
multisetwhen repeats must stay.
Amara runs the sign-up sheet for the coding club, and names arrive in any order, some of them twice. She needs the different names in alphabetical order, and Kenji keeps asking whether he is on the list yet. A vector would need a sort and a sweep of repeats after every sign-up, or a full scan for every question. A sorted C array shifts every later name along by one for each new name. This lesson gives Amara a container that does both jobs as the names arrive.
Two promises: sorted, and no repeats
A set is a container that holds each value at most once and keeps its values sorted. You insert values in any order. Walk the set from its start, and the values come out from the smallest to the largest. Smaller means the same < you use on numbers, and on strings it means the byte order of Module 3.
Insert a value the set already holds, and nothing changes. Module 0 gave a set the values 30, 10, 20 and 10, and it printed 10 20 30. Here is the same idea with Amara's names.
So a set gives Amara two things at once: the names come out sorted, and each name comes out once.
Inside: a tree that stays short
How does a set find a name without looking at every one? It keeps its values in a binary search tree. Each value sits in a node, a small box in memory. Every smaller value lives somewhere to a node's left, and every larger value to its right. A search starts at the top node, the root, compares once, and goes left or right, leaving a whole side behind.
The picture finds 50 among seven values. A scan of the sorted array checks 10, 20, 30, 40 and then 50: five steps. The tree asks 40, then 60, then 50: three steps. GCC 12's library builds exactly this tree when the seven values arrive as 40, 20, 60, 10, 30, 50 and 70. Other orders give other shapes, as the next section shows.
Each new level of a tree doubles the room for values, so 20 levels hold over a million. The shortest tree for a million values has 20 levels, and a find in it takes at most 20 steps. Module 2's find on a vector may take a million.
A tree kept close to that shortest shape is called balanced. The library rebalances its tree after every insert and erase. The real tree can be taller than the shortest one, but at most twice as tall; lesson 05 measures it. So a find costs one comparison per level, and the library keeps the levels few.
Watch the tree grow, one insert at a time
GCC's standard library, called libstdc++, keeps a set in a red-black tree. Each node also carries a colour, red or black. After each insert or erase, the library recolours and turns a few nodes to keep its colour rules. The widget shows the real shape after each step, without the colours. The C++ standard names no tree at all: it promises only O(log n) steps for a find, an insert and an erase.
O(log n) means that doubling n adds about one step, as Module 0 put it. Now watch 10 to 80 arrive in rising order, the worst order for a tree that never rebalances.
Eight rising values made 4 levels. A plain tree that never rebalanced would hang each value right of the last: a chain 8 levels deep. Watch the root: it moved from 10 to 20, then to 40. The repeated insert(50) walked down to the 50 already there and changed nothing. find(35) fell off the tree below 30, so 35 is not in the set.
So the shape is the library's choice, and it stays a few levels deep in any arrival order.
The syntax you need on day one
std::set and std::multiset, and their everyday calls
#include <set>
set<T> s; an empty set of T
set<T> s{a, b, c}; a set made from a list; repeats are dropped
s.insert(x); add x; nothing happens if x is already there
s.erase(x); remove x, if it is there
s.count(x) 1 if x is there, 0 if not
s.find(x) an iterator to x, or s.end() if x is not there
s.size() s.empty() how many values, and whether there are none
*s.begin() *s.rbegin() the smallest and the largest value
for (T x : s) every value, from the smallest to the largest
multiset<T> m; the same calls, but repeats are kept
set<T>: one type in the angle brackets, the type of the values, as for a vector. The header<set>brings bothsetandmultiset.insert,erase,countandfindall take a value. You never say where a value goes: the set decides.findreturns an iterator, the marker for one place in a container that Module 2 used.s.end()is the place just after the last value, so here it means "not found".*s.begin()and*s.rbegin(): only when the set is not empty. Checkempty()first.
So everyday set code names values, never positions.
insert, and the walk in sorted order
s.insert(x) puts x in its sorted place. A range-for walks the set from the smallest value to the largest. This program prints the whole set after each insert.
#include <iostream>
#include <set>
using namespace std;
void show(const char* step, const set<int>& s) {
cout << step << ':';
for (int x : s) {
cout << ' ' << x;
}
cout << " (size " << s.size() << ")\n";
}
int main() {
set<int> s;
s.insert(30);
show("insert(30)", s);
s.insert(10);
show("insert(10)", s);
s.insert(20);
show("insert(20)", s);
s.insert(10);
show("insert(10)", s);
return 0;
}
insert(30): 30 (size 1)
insert(10): 10 30 (size 2)
insert(20): 10 20 30 (size 3)
insert(10): 10 20 30 (size 3)
The values arrived as 30, 10 and 20, yet 10 went before 30 and 20 between them. The second insert(10) found 10 already there, so the size stayed 3. So the walk is always sorted, whatever order the inserts came in.
Is it there? count and find
s.count(x) says how many copies of x the set holds. On a set that is only ever 1 or 0, so it reads as yes or no. s.find(x) returns an iterator to x, or s.end() when x is missing.
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s{3, 9, 14, 27, 31};
cout << s.count(14) << ' ' << s.count(5) << '\n';
auto it = s.find(27);
if (it != s.end()) {
cout << "found " << *it << '\n';
}
if (s.find(5) == s.end()) {
cout << "5 is not there\n";
}
return 0;
}
1 0
found 27
5 is not there
Compare find's answer with end() before you read it with *. Reading *s.end() is undefined behaviour: the C++ standard says nothing about what happens. So use count for a plain yes or no, and find when you also want the place.
erase by value
s.erase(x) removes x if the set holds it. If it does not, nothing happens.
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s{3, 9, 14, 27, 31};
s.erase(14);
s.erase(5);
cout << "left:";
for (int x : s) {
cout << ' ' << x;
}
cout << " (size " << s.size() << ")\n";
return 0;
}
left: 3 9 27 31 (size 4)
erase(14) removed 14. erase(5) found no 5 and did nothing, with no error. So erasing a value that is not there is safe, while reading one that is not there is not.
The smallest and the largest
s.begin() is the place of the smallest value, so *s.begin() reads it. s.rbegin() is where a walk backwards starts, the r is for reverse, so *s.rbegin() is the largest value.
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> temps{31, 28, 35, 29};
cout << "coolest " << *temps.begin() << ", hottest " << *temps.rbegin() << '\n';
temps.insert(38);
temps.erase(28);
cout << "coolest " << *temps.begin() << ", hottest " << *temps.rbegin() << '\n';
set<int> none;
if (none.empty()) {
cout << "an empty set has no coolest\n";
}
return 0;
}
coolest 28, hottest 35
coolest 29, hottest 38
an empty set has no coolest
After the insert and the erase, both ends moved, and the program never searched for them. An empty set has no smallest and no largest value. There, *s.begin() and *s.rbegin() are undefined, and lesson 06 shows what one run did. So check empty() before you read either end.
No index, and no changing a value in place
A vector has positions, so v[0] is its first element. A set has no positions, and an index does not compile.
set<int> s{10, 20, 30};
cout << s[0] << '\n';
GCC 12 stops at the runner's flags with error: no match for 'operator[]' (operand types are 'std::set<int>' and 'int'). To reach the third value you walk to it from begin(), and lesson 03 shows how. Changing a value through its iterator does not compile either.
set<int> s{10, 20, 30};
auto it = s.find(20);
*it = 25;
GCC 12 says error: assignment of read-only location 'it.std::_Rb_tree_const_iterator<int>::operator*()'. The reason is the tree, which is ordered by its values. A 20 changed to 25 in place would sit in the wrong spot, and later searches would go the wrong way. So the library hands out every value read-only: you may read it, but never write it.
So you change a set only by insert and erase: erase the old value, then insert the new one. Lesson 02 shows a way that keeps the node.
multiset: the cousin that keeps repeats
Sometimes repeats are the point. A class voting on a trip needs every vote, not one per place. A multiset is a set that keeps repeats. It sorts the same way, lives in the same header and has the same calls. Its count(x) says how many copies of x it holds.
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main() {
multiset<string> votes{"zoo", "museum", "zoo", "park", "zoo"};
cout << "votes:";
for (const string& v : votes) {
cout << ' ' << v;
}
cout << " (size " << votes.size() << ")\n";
cout << "zoo: " << votes.count("zoo") << ", lake: " << votes.count("lake") << '\n';
return 0;
}
votes: museum park zoo zoo zoo (size 5)
zoo: 3, lake: 0
The three zoo votes stayed, side by side, because equal values sit next to each other in sorted order. Removing just one of them takes a call that lesson 02 shows. So reach for a multiset when you need every copy, and for a set when one is enough.
The smallest set program: five numbers with one repeat go in, and come out sorted on one line, with the size.
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s;
for (int x : {42, 7, 19, 7, 3}) {
s.insert(x);
}
cout << "sorted:";
for (int x : s) {
cout << ' ' << x;
}
cout << '\n' << s.size() << " different values\n";
return 0;
}
sorted: 3 7 19 42
4 different values
The 7 that came twice is printed once, and the size counts it once. The first loop walks a list written right in the code, and the second walks the set.
Run in CompilerAmara's five sign-ups go into a set, with Bob twice. Kenji asks whether he is on the list, and so does Zara. Then Kenji signs up.
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main() {
set<string> signed_up;
for (string name : {"maria", "bob", "zara", "bob", "amara"}) {
signed_up.insert(name);
}
for (string who : {"kenji", "zara"}) {
if (signed_up.count(who) == 1) {
cout << who << " is on the list\n";
} else {
cout << who << " is not on the list yet\n";
}
}
signed_up.insert("kenji");
auto it = signed_up.find("kenji");
if (it != signed_up.end()) {
cout << "now " << *it << " is on it too\n";
}
cout << signed_up.size() << " different people so far\n";
return 0;
}
kenji is not on the list yet
zara is on the list
now kenji is on it too
5 different people so far
Bob counted once, so after Kenji's sign-up there are 5 different people, not 6. count answered both questions, and find gave back the stored name. With a million names, each question would still take only a few dozen steps.
Kenji books seats for the school play. Seat numbers start at the stage, so the smallest booked number is the seat closest to it. One person cancels, and someone new books seat 30.
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> booked{12, 3, 27, 8, 15};
booked.erase(3);
booked.insert(30);
cout << "booked:";
for (int seat : booked) {
cout << ' ' << seat;
}
cout << '\n';
cout << "nearest the stage: " << *booked.begin() << ", farthest: " << *booked.rbegin() << '\n';
if (booked.count(3) == 0) {
cout << "seat 3 is free again\n";
}
return 0;
}
booked: 8 12 15 27 30
nearest the stage: 8, farthest: 30
seat 3 is free again
The cancelled seat 3 left the set, so seat 8 is now the closest booking. The farthest seat became 30 the moment it was booked. The set kept both ends ready while the bookings changed.
Run in CompilerMaria wants every mark from her class quiz, ties included, so she uses a multiset. A set of the same marks sits beside it to compare.
#include <iostream>
#include <set>
using namespace std;
int main() {
multiset<int> marks{85, 72, 85, 90, 64, 85, 72};
set<int> different{85, 72, 85, 90, 64, 85, 72};
cout << "every mark:";
for (int m : marks) {
cout << ' ' << m;
}
cout << '\n';
cout << marks.count(85) << " students scored 85\n";
cout << "lowest " << *marks.begin() << ", highest " << *marks.rbegin() << '\n';
cout << "the multiset keeps " << marks.size() << ", a set keeps " << different.size() << '\n';
return 0;
}
every mark: 64 72 72 85 85 85 90
3 students scored 85
lowest 64, highest 90
the multiset keeps 7, a set keeps 4
Three students tied on 85, and the multiset kept all three. The set kept 4 marks, one of each. Maria would write this program to report ties: here count(85) is a number of students, not a yes or no.
Where this is used
- The Linux kernel's red-black trees.
lib/rbtree.cis the kernel's own balanced tree, used all over the kernel. The CFS scheduler, Linux's default from 2.6.23 to 6.5, kept the tasks ready to run in one, ordered by virtual run time. It ran the leftmost task, the one with the least virtual run time. - epoll in Linux. A server that waits on many network connections hands them to epoll. epoll keeps the file descriptors it watches in a red-black tree, so adding, finding or removing one takes
O(log n)steps. - Java's
TreeSet. Java's sorted set keeps its values in a red-black tree. Its documentation promiseslog(n)time foradd,removeandcontains, the promise C++ makes forinsert,eraseandfind.
Common mistakes
1. Changing every value in a range-for.
set<int> prices{10, 20, 30};
for (int& p : prices) {
p += 5;
}
An error at the runner's flags: error: binding reference of type 'int&' to 'const int' discards qualifiers. A set's values are read-only, so a reference that could change them is refused. Build a new set from the changed values instead. You will try it because the same loop changed a vector in Module 1.
2. Forgetting the header.
#include <iostream>
using namespace std;
int main() {
set<int> s{3, 1, 2};
cout << s.size() << '\n';
return 0;
}
An error at the runner's flags: error: 'set' was not declared in this scope, then GCC 12's own fix: note: 'std::set' is defined in header '<set>'; did you forget to '#include <set>'?. A multiset needs the same header. You will forget it because you add the set in the middle of a program, far from the includes.
3. Expecting the order you inserted.
set<string> steps;
steps.insert("wash");
steps.insert("cut");
steps.insert("cook");
for (const string& s : steps) {
cout << s << '\n';
}
No message at the runner's flags, and it prints cook, cut and wash: alphabetical, not the recipe's order. A set keeps no record of the order its values arrived in. If the order matters, keep the steps in a vector. You will expect it back because vector, deque and list all kept it.
Amara typed the club's paper sign-up form into the computer, one name per line, in the order people wrote them. Some people signed up twice, and a few three times. For the welcome email she needs the number of different people, and their names in alphabetical order.
Input. A line with n, then n names, one per line.
Output. First a line with the number of different names. Then every different name on its own line, in alphabetical order.
Constraints. 1 <= n <= 90000. A name is 1 to 10 lowercase English letters. Alphabetical order is the order in which string compares, so al comes before alice.
Sample. Input 6, then maria, bob, zara, bob, amara and maria gives 4, then amara, bob, maria and zara. Bob and Maria signed up twice each.
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n = 0;
cin >> n;
set<string> names; // every different name, kept in alphabetical order
for (int i = 0; i < n; i++) {
string name;
cin >> name;
// Add name to names.
}
// Print how many different names there are, then every name in
// alphabetical order, one per line.
return 0;
}
Graded as club-sign-ups, a free problem in this module's problem set. The hidden tests include a single name, one name typed 1000 times, and names that begin other names, such as al and alice. They also include 90000 different names.
Maria's class and David's class took the same quiz, marked out of 100. Their teachers want the marks that appear in both classes, each mark once, to compare the students who scored them.
Input. A line with n, then a line with the n marks of the first class. Then a line with m, then a line with the m marks of the second class.
Output. One line: every mark that appears in both classes, each once, from the lowest up, separated by single spaces. If no mark appears in both, print the word none.
Constraints. 1 <= n, m <= 100000. Every mark is a whole number from 0 to 100.
Sample. Input 5, 70 85 85 40 92, 4 and 85 60 92 92 gives 85 92. Both marks appear in both classes, and each is printed once.
#include <iostream>
#include <set>
using namespace std;
int main() {
int n;
cin >> n;
set<int> first;
for (int i = 0; i < n; i++) {
int mark;
cin >> mark;
first.insert(mark);
}
int m;
cin >> m;
set<int> second;
for (int i = 0; i < m; i++) {
int mark;
cin >> mark;
second.insert(mark);
}
// Walk first from its lowest mark. Print every mark that second also
// holds, on one line, separated by single spaces, or the word none.
return 0;
}
Not graded on its own. Walk one set in order and let count on the other answer for each mark; the walk already gives the rising order.
Kenji's arcade machine shows a board of the five highest different scores ever played. Two games with the same score share one place on the board. After every game, the machine shows the lowest score on the board. Once the board is full, a new game has to beat it to get on.
Input. A line with n, then n scores, one per game, in the order they were played.
Output. One line per game: the lowest score on the board after that game.
Constraints. 1 <= n <= 100000. Every score is between 0 and 1000000000. After each game, your set holds at most five scores.
Sample. Input 7 and 50 80 50 90 10 70 60 gives 50, 50, 50, 50, 10, 10 and 50. The sixth game fills the board, so the seventh game's 60 pushes 10 off it.
#include <iostream>
#include <set>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
set<int> board; // the highest different scores, never more than five
for (int i = 0; i < n; i++) {
int score;
cin >> score;
// Put score on the board, keep only the five highest, then print
// the lowest score left on the board.
}
return 0;
}
Not graded on its own. The idea the lesson only pointed at: s.erase(s.begin()) removes the smallest value, so one erase after each insert keeps the board at five.
Common doubts
Why is my set sorted when I inserted the values in another order?
Because each insert puts its value in its sorted place in the tree. The order you inserted in is not stored anywhere. If you need it later, keep the values in a vector as well.
Can a set hold two equal values?
No. The second insert finds the first value and changes nothing. A multiset holds both, and its
countsays how many there are.Why can't I write
s[2]?A set has no positions to number, because its values live in tree nodes, not in a row. To reach the third value, start at
begin()and step twice, which lesson 03 shows.Is
countslower thanfind?On a set, no: both walk down the same tree in
O(log n)steps. On a multiset,countalso counts every copy, so it can take longer, and lesson 02 gives its cost.
Key takeaways
- A set holds each value once and walks them back sorted by
<, whatever order they were inserted in. insertanderasetake a value;count(1 or 0) andfind(an iterator, orend()) ask whether it is there.*s.begin()is the smallest value and*s.rbegin()the largest; checkempty()before either.- The values live in a balanced tree, so a find costs one step per level, and 20 levels hold over a million values.
- A set has no index and its values are read-only; a
multisetkeeps repeats, and itscountsays how many. - Go deeper: Under the Hood, the tree's nodes and iterators that stay (Pro).
Next, lesson 02 goes through every set and multiset operation, one by one, each with its cost.
End of lesson 1
Mark it done, and your progress moves with you.
Next: Every set and multiset Operation, One by One, With Its Cost