Lesson 07 / 11
Algorithms
The measurement of how std::sort changes a vector in place, and how read-only algorithms such as std::count leave the source unchanged.
Published Updated
In this lesson
- Measures that std::sort changes a container's element order in place
- Shows that read-only algorithms (count, find_if, accumulate) leave the source unchanged
- Explains how algorithms that take a comparison predicate apply a custom ordering rule
- Recognizes that an algorithm works with an iterator pair and does not depend on the container
Contents
The previous lesson showed that reading an iterator after erase is
unspecified, while the new iterator erase returns stays valid. That
lesson tracked a single position. This lesson widens the boundary back
out: it turns to the standard library’s ready-made algorithms that
operate on all of a container’s elements. What does std::sort leave
in the source when it sorts a vector? This same question will be asked
again for every algorithm in this lesson; some algorithms read the source
and never change it, while others permanently reorder the source itself,
and this difference is drawn not from the algorithm’s name but from its
standard definition.
std::sort: An Algorithm That Changes in Place
std::sort arranges the elements in the given iterator range according
to the specified ordering rule in place — it returns no new container,
it changes the given range itself. The default rule is operator<.
// sort-in-place.cpp — how std::sort changes a vector
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> numbers{40, 10, 30, 20};
std::sort(numbers.begin(), numbers.end());
std::printf("sorted array:");
for (int value : numbers) {
std::printf(" %d", value);
}
std::printf("\n");
return 0;
}
sorted array: 10 20 30 40
SL13. After the call to std::sort, numbers‘s element count did not
change, but the elements’ order changed — the kind of this operation
is changed. The standard guarantees exactly which final arrangement
results after sorting (ascending, by the operator< rule), so the
outcome is not unspecified, it is on the contrary predictable and
repeatable. This is what separates sort from erase in the previous
lesson: the iterator erase left behind was unspecified, but the array
sort leaves behind is defined.
Read-Only Algorithms: The Ones That Leave the Source Unchanged
Not every algorithm changes the source. std::count returns the number
of elements equal to the given value; std::find_if returns an iterator
to the first element that satisfies a predicate; std::accumulate
accumulates the elements by summing them. All three only read the range.
// read-only-algorithms.cpp — do count, find_if, accumulate change the source
#include <algorithm>
#include <cstdio>
#include <numeric>
#include <vector>
int main() {
std::vector<int> numbers{4, 7, 2, 7, 9};
auto before = numbers;
long count = std::count(numbers.begin(), numbers.end(), 7);
auto it = std::find_if(numbers.begin(), numbers.end(),
[](int value) { return value > 8; });
int total = std::accumulate(numbers.begin(), numbers.end(), 0);
std::printf("count of 7 %ld\n", count);
std::printf("first value greater than 8 %d\n", *it);
std::printf("total %d\n", total);
std::printf("did the source change %s\n", (numbers != before) ? "yes" : "no");
return 0;
}
count of 7 2 first value greater than 8 9 total 29 did the source change no
SL14. All three algorithms only read numbers; at the end of the
operation the source is equal to its pre-operation copy — the kind is
unchanged. This stands in direct contrast to std::sort: both
operate on the same vector, but sort changes the ordering while these
three algorithms never touch the ordering at all. An algorithm’s name
usually indicates its intent — the verbs count, find, accumulate
say “query,” the verbs sort, reverse, transform say “change” — but
this is a convention, not a guarantee; the definitive answer is always in
the algorithm’s standard definition.
This Course Does Not Count: The Measurement Axis Is Not Run Count
Traits, Generics and Error Handling (Rust, M10/K03) made the runtime
cost of abstraction visible by measuring how many times a body runs —
what was measured there was how many times a call was repeated. This
course works on a different axis and does not count: a number such as
the 2 that std::count returns is a result that states how many
elements equal the sought value, not a measurement of how many times the
algorithm was called or how many times a body ran. The question this
lesson always asks is the same: which kind (unchanged/changed/empty/unspecified)
did an operation leave the source in? std::count itself is called once
and leaves the source unchanged; how many comparisons it performs
internally is not this measurement’s subject. This distinction matters so
a reader does not mix up the two courses: a reader who wants to learn the
runtime cost of abstraction looks to rust-soyutlama; a reader who wants
to learn what state an operation leaves the source in looks to this
course.
A Custom Comparison Predicate
std::sort can take a third argument: a predicate that compares two
elements. This predicate replaces operator<, and the programmer defines
the ordering rule.
// sort-predicate.cpp — descending order with a custom comparison
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> numbers{40, 10, 30, 20};
std::sort(numbers.begin(), numbers.end(),
[](int a, int b) { return a > b; });
std::printf("descending:");
for (int value : numbers) {
std::printf(" %d", value);
}
std::printf("\n");
return 0;
}
descending: 40 30 20 10
SL15. When the predicate returns a > b, it states that a comes
before b; this is why the array was sorted from largest to smallest.
This operation’s kind is also changed — the same kind as the previous
sort call, because both are the same algorithm called with a different
rule. The predicate itself never read or changed numbers; it only
compared two int values. This distinction matters: what makes sort
mutating is that it writes into the iterator range, while what keeps the
predicate non-mutating is that it is only a value comparison.
Algorithms Work with an Iterator Pair, Not a Container
None of the standard algorithms take a container type; all of them take a
begin and end iterator. This lets the same algorithm work on all of a
vector, a sub-range of a vector, or a C array.
// iterator-pair.cpp — the same algorithm over different ranges
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> numbers{5, 3, 8, 1, 9, 2};
std::sort(numbers.begin(), numbers.begin() + 3);
std::printf("partially sorted array:");
for (int value : numbers) {
std::printf(" %d", value);
}
std::printf("\n");
return 0;
}
partially sorted array: 3 5 8 1 9 2
SL16. std::sort(numbers.begin(), numbers.begin() + 3) sorted only
the first three elements; the remaining three (1 9 2) stayed in their
input order. This shows that the same std::sort call produces a
different result with a different range — the algorithm sees not the
whole container but only the iterator pair it was given. This
operation’s kind is again changed, because part of numbers (the
first three elements) changed in ordering; but this time the scope of the
change is limited to the given range, not the whole container. One
consequence of this design is that algorithms never know a container’s
own bounds: the iterator numbers.begin() + 3 given to std::sort does
not denote the container’s own end, only the bound the caller specified.
The same algorithm works the same way on a raw C array too, called with a
pointer pair — the concept of a container never appears in the
algorithm’s interface, only the iterator category (here, random access)
is what the interface requires.
std::transform: An Algorithm That Writes to a New Destination
std::sort changed the source range itself. std::transform offers a
different pattern: it reads the source, applies a function to each
element, and writes the result to a separate destination. As long as
the source is a container separate from the destination, std::transform
never touches the source.
// transform-separate-destination.cpp — does std::transform change the source or the destination
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> source{1, 2, 3, 4};
std::vector<int> destination(source.size());
std::transform(source.begin(), source.end(), destination.begin(),
[](int value) { return value * value; });
std::printf("source:");
for (int value : source) {
std::printf(" %d", value);
}
std::printf("\n");
std::printf("destination:");
for (int value : destination) {
std::printf(" %d", value);
}
std::printf("\n");
return 0;
}
source: 1 2 3 4 destination: 1 4 9 16
SL17. source is still 1, 2, 3, 4 at the end of the operation —
std::transform only read the source and never wrote to it; the
source’s kind is unchanged. destination was filled with the array
of squares. std::transform‘s third argument is the destination’s begin
iterator; this argument can also be given as the source itself
(source.begin()) — in that case the algorithm reads each element and
writes it back to the same position, and the source then falls into the
changed kind instead. Which kind applies depends on whether the
destination is the same range as the source; this lesson keeps the two
separate to show std::transform’s default behavior, which is to read
the source and write elsewhere. Because std::transform‘s destination
argument is an iterator, it is the caller’s responsibility to have
already allocated enough size at the destination beforehand — the
algorithm does not insert elements into the destination, it only writes
to existing positions; if the destination is not as large as source,
the write runs past the bound of allocated memory, and this is undefined
behavior. This lesson never creates this risk, by always sizing
destination with source.size().
std::stable_sort: A Sort That Preserves the Order of Equal Elements
std::sort does not guarantee preserving the relative order between
two elements considered equal — this is a permitted side effect of the
algorithm, not a bug. std::stable_sort does the same sorting work but
carries an additional guarantee: elements considered equal by the
comparison predicate keep their relative position from the input order.
This guarantee is not free — std::sort runs with average O(n log n)
complexity, while std::stable_sort can drop to O(n log^2 n) if
sufficient extra memory is not available; if extra memory is available,
it still achieves O(n log n).
// stable-sort-equality.cpp — how the order of equal elements differs between sort and stable_sort
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
int main() {
std::vector<std::pair<int, char>> records{
{2, 'a'}, {1, 'b'}, {2, 'c'}, {1, 'd'}};
std::stable_sort(records.begin(), records.end(),
[](const auto& left, const auto& right) { return left.first < right.first; });
std::printf("sorted records:");
for (const auto& record : records) {
std::printf(" (%d,%c)", record.first, record.second);
}
std::printf("\n");
return 0;
}
sorted records: (1,b) (1,d) (2,a) (2,c)
SL18. The pair {1, 'b'} and {1, 'd'}, considered equal by key,
kept their relative order from the input after the sort — b still comes
before d. The same holds for {2, 'a'} and {2, 'c'}. This
operation’s kind is also changed — the array was reordered from start
to end — but its difference from std::sort is the extra guarantee that
the relative order among equal elements was also preserved. std::sort
could have left (1,d) and (1,b) in reverse order on the same input;
this is not a rule std::sort violates, it is a promise it never makes.
Why std::list Carries Its Own sort
std::sort requires a random access iterator — it needs to jump between
elements in constant time in order to achieve its average O(n log n)
complexity. This is why std::sort does not compile with
std::list’s bidirectional iterator. The standard library provides a
separate member function for this case: std::list::sort.
// list-sort-member-function.cpp — why list needs a separate sort
#include <cstdio>
#include <list>
int main() {
std::list<int> values{40, 10, 30, 20};
// The line below does not compile, because std::sort requires a random access iterator:
// std::sort(values.begin(), values.end());
values.sort();
std::printf("sorted list:");
for (int value : values) {
std::printf(" %d", value);
}
std::printf("\n");
return 0;
}
sorted list: 10 20 30 40
The reason std::list::sort exists as a separate member function is this
constraint: because list’s nodes do not sit in contiguous memory, the
generic std::sort algorithm cannot operate on them, but list can
produce the same result at a different complexity by relinking its own
nodes. This is a concrete example of the rule “not every container works
with every algorithm.” The same constraint applies to other algorithms
that require random access: std::nth_element and binary-search-based
algorithms such as std::binary_search and std::lower_bound also
cannot be called directly on std::list; the standard library does not
fill this gap with a separate member function for every container, it
only offers a few operations that require node linkage, such as
list::sort, list::merge, list::unique and list::remove, as member
functions. The choice of container therefore determines not only the
complexity guarantees but also which standard algorithms can be used
directly.
The Bounding Measurement
When the same data is given to two different algorithms with a comparison
predicate over the same key, the question of how much of the source can
be reliably read depends on the algorithm’s name. std::sort does
not guarantee preserving the relative order between elements with equal
keys — so printing that order would be presenting a promise the standard
never made. The same data is sorted once more below with std::stable_sort,
and the two results are compared:
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
int main() {
std::vector<std::pair<int, char>> keyed{
{2, 'a'}, {1, 'b'}, {2, 'c'}, {1, 'd'}};
std::vector<std::pair<int, char>> stable = keyed;
std::sort(keyed.begin(), keyed.end(),
[](const auto& left, const auto& right) { return left.first < right.first; });
std::stable_sort(stable.begin(), stable.end(),
[](const auto& left, const auto& right) { return left.first < right.first; });
std::printf("sort key order:");
for (const auto& record : keyed) {
std::printf(" %d", record.first);
}
std::printf("\n");
std::printf("stable_sort full order:");
for (const auto& record : stable) {
std::printf(" %d%c", record.first, record.second);
}
std::printf("\n");
return 0;
}
sort key order: 1 1 2 2 stable_sort full order: 1b 1d 2a 2c
Over keyed we printed only the keys (1 1 2 2); we did not print the
letters, because which letter std::sort leaves first among equal keys
is a detail the standard does not define — even the same compiler may
apply a different internal ordering at a different input size. Over
stable, we could print the full records, because std::stable_sort
guarantees preserving the relative input order of equal keys: 1b always
comes before 1d, 2a always comes before 2c. Both calls move the
same data into the changed kind — the source is reordered in place —
but which detail of the source can be safely read and reported is
determined by the algorithm’s name: sort guarantees only the final key
order, stable_sort guarantees the key order together with the relative
position among equals.
Summary
std::sortchanges the order of the elements in the given iterator range in place; the kind of this lesson’s central measurement is changed. Giving a custom comparison predicate changes the ordering rule, but the predicate itself never reads or changes the source;std::stable_sortalso preserves the relative order of elements considered equal, and this extra guarantee comes at a complexity cost that can rise toO(n log^2 n)when sufficient extra memory is not available. Bounding measurement: when the same data is given tostd::sortandstd::stable_sort, both move the source into the changed kind, but which detail of the source can be safely reported depends on the algorithm’s name —sortguarantees only the key order,stable_sortalso guarantees the relative order among equals.- Read-only algorithms such as
std::count,std::find_if,std::accumulateleave the source unchanged. - This course does not count runs:
rust-soyutlama‘s “how many times did it run” axis is not this course’s measurement axis; what is measured here is which kind the source is left in. - Algorithms take an iterator pair, not a container type; the same algorithm operates over a different scope with a different range.
std::transformleaves the source’s kind unchanged when it reads the source and writes to a separate destination; making the destination the source itself turns the kind into changed.std::sortrequires a random access iterator and does not compile onstd::list;listinstead provides its ownsortmember function.
Next Step
This lesson showed how std::sort changes a container in place. The next
lesson takes up the two-step idiom required to remove elements from a
container: why does std::remove not actually delete elements, only move
them toward the end?
To keep your progress and take notes, Log in
My notes
Log in to take notes.