Course outline · 0% complete

0/27 lessons0%

Course overview →

set, sort, and the Algorithm Library

lesson 9-2 · ~14 min · 25/27

Deduplication and sorting appear in some form in most interview sets, and <algorithm> is why C++ engineers rarely hand-write either: the library versions are correct, heavily optimized, and O(n log n) where it matters (lesson 8-4). Reaching for std::sort instead of writing a loop is a professional habit, not laziness.

set: membership and dedup

std::set<T> stores each value at most once, kept sorted (std::unordered_set is the hashed cousin, like Python's set):

#include <set>
std::set<int> seen;
seen.insert(5);
seen.insert(5);        // ignored, already present
seen.count(5)          // 1
seen.count(7)          // 0
seen.size()            // 1

Iterating a set visits values in sorted order, so "unique + sorted" is one container away.

The algorithm library

<algorithm> provides functions that work on any container via begin/end iterator pairs (lesson 8-3):

#include <algorithm>
std::vector<int> v = {4, 1, 3};

std::sort(v.begin(), v.end());              // 1 3 4
std::sort(v.begin(), v.end(), std::greater<int>());  // 4 3 1
std::reverse(v.begin(), v.end());

std::max_element(v.begin(), v.end())   // ITERATOR to the largest, * it
std::min_element(v.begin(), v.end())
std::find(v.begin(), v.end(), 3)       // iterator to first 3, or v.end() if absent
std::count(v.begin(), v.end(), 3)      // how many 3s
std::min(a, b); std::max(a, b);        // for two plain values

std::sort runs in O(n log n) and also sorts strings and pairs (by first, then second) out of the box. Note the * on max_element's result: algorithms return iterators, dereference to get the value.

An algorithms sampler on one vector

Three library calls on the same data, two that only read and one that reorders in place.

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> v = {40, 10, 30, 10, 20};

    std::cout << "max: " << *std::max_element(v.begin(), v.end()) << "\n";
    std::cout << "count of 10: " << std::count(v.begin(), v.end(), 10) << "\n";

    std::sort(v.begin(), v.end());
    std::cout << "sorted:";
    for (int x : v) {
        std::cout << " " << x;
    }
    std::cout << "\n";
    return 0;
}

Output

max: 40
count of 10: 2
sorted: 10 10 20 30 40

The * in front of std::max_element is essential, since the algorithm returns an iterator to the largest element rather than the value. Without the dereference the code would not compile, and if the vector were empty the returned iterator would be v.end(), which must not be dereferenced at all.

std::count returns 2 for the duplicated 10, and note that duplicates survive sorting. The sorted output has five elements, the same five that went in, because std::sort rearranges rather than deduplicating. The set at the top of this lesson is the tool when duplicates should disappear.

std::sort modifies v in place and returns nothing, which is a real difference from Python's sorted(). Keeping the original order around means copying the vector first.

Custom sort orders: lambdas

std::sort orders by < by default. To sort by any other rule, you pass a comparison function, and the idiomatic way to write one inline is a lambda, meaning an anonymous function defined right where it is used. The syntax is [](parameters) { body }, where the leading [] is what marks it as a lambda.

std::vector<std::string> words = {"kiwi", "fig", "banana"};

std::sort(words.begin(), words.end(),
          [](const std::string& a, const std::string& b) {
              return a.size() < b.size();   // true when a should come first
          });
// fig, kiwi, banana: shortest first

The comparator receives two elements and returns true when the first must come before the second. Getting that convention backwards reverses the sort, which is usually easy to spot.

The parameters are const std::string&, which is lesson 4-2's cheap read-only pass, and it matters more here than usual because the comparator runs O(n log n) times. Taking them by value would copy two strings on every comparison.

One rule that trips people up: the comparator must express a strict ordering, so it has to return false when the two elements are equivalent. Writing a.size() <= b.size() looks harmless and is undefined behavior, since std::sort can then run off the end of the range while partitioning.

Sorting by length, by the second member of a pair, or in descending order are all one lambda away, and this exact pattern appears in a large share of interview solutions.

Sorting by length, longest first

The same length comparator with the inequality flipped, which reverses the order.

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> words = {"kiwi", "fig", "banana"};
    std::sort(words.begin(), words.end(),
              [](const std::string& a, const std::string& b) {
                  return a.size() > b.size();
              });
    for (const std::string& w : words) {
        std::cout << w << "\n";
    }
    return 0;
}

Output

banana
kiwi
fig

Flipping < to > is the whole change, and the result runs 6, 4, 3 characters instead of 3, 4, 6. That single-character edit is why descending sorts rarely need any other technique.

The comparator ignores the strings' contents entirely and looks only at their lengths, so two words of equal length could come out in either relative order. std::sort makes no promise about ties, and std::stable_sort is the version that preserves the original relative order of equivalent elements.

The printing loop takes const std::string&, which avoids copying each string just to print it, following the same rule as the comparator.

How algorithms report a failed search

std::find(v.begin(), v.end(), 99) returns v.end() when 99 is not in the vector, meaning the one-past-the-last iterator.

Every searching algorithm in <algorithm> signals a failure the same way, by returning the end iterator you passed in. That is a sensible choice, because the end iterator is the one value in the range that can never refer to a real element.

The idiom that goes with it is two lines:

auto it = std::find(v.begin(), v.end(), 99);
if (it != v.end()) {
    std::cout << "found " << *it << "\n";
}

Comparing against -1 is the Python and JavaScript habit and does not compile here, since an iterator is not a number. Getting the index instead requires subtracting, as int idx = it - v.begin();, and that subtraction is only meaningful after the end() check has passed.

Note the contrast with std::string::find from lesson 8-2, which returns an index and uses std::string::npos for its not-found marker. The two conventions coexist in the standard library, so which one applies depends on whether you are calling a container's own method or a free algorithm.

Deduplicating and sorting in one container

Six integers read into a std::set, which discards repeats and keeps what remains in order.

#include <iostream>
#include <set>

int main() {
    std::set<int> s;

    for (int i = 0; i < 6; i++) {
        int x;
        std::cin >> x;
        s.insert(x);
    }

    for (int x : s) {
        std::cout << x << "\n";
    }
    return 0;
}

Input

4 2 4 1 2 4

Output

1
2
4

Six values go in and three come out, because insert on a value already present does nothing at all. No check is needed and no error is reported, which makes a set the shortest possible answer to a deduplication question.

The output arrives sorted with no sorting call anywhere, since iterating a set visits its values in order. Doing the same job with a vector would take three steps, being fill, std::sort, then std::unique followed by erase, and that route is actually faster for large data because it avoids building a tree.

The insert loop reads into a local int x declared inside the body, so a fresh variable exists per iteration. Note that s.insert(x) returns a pair whose second member is a bool saying whether the insertion actually happened, which is a neat way to detect the first duplicate in a stream.