AlgoMaster Logo

Algorithms Overview

High Priority14 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

The STL ships with around a hundred ready-made algorithms for searching, sorting, transforming, counting, and combining sequences. They live mostly in <algorithm> and <numeric>, and they are built on a single idea: an algorithm does not know what container it is working on. It only knows how to walk a range using a pair of iterators. This chapter maps the available algorithms, explains the iterator-range convention every algorithm follows, and points to the chapters that drill into specific families.

The Stepanov Decoupling

Alexander Stepanov, the designer of the STL, made one architectural choice that defines the whole library: algorithms and containers do not know about each other. Instead, they meet at a common interface called an iterator. An iterator is anything that behaves like a pointer; you can dereference it to read or write an element, and you can advance it to the next position.

Without this decoupling, you would need a separate sort for vector, another for deque, another for built-in arrays, and so on. With it, you write one std::sort and it works on every container that offers random-access iterators, including a raw C array.

The algorithm talks to the iterator. The iterator talks to the container. Neither side has to know about the other. Add a new container type tomorrow and every existing algorithm works on it, as long as the new container hands out iterators of the correct category. Write a new algorithm and it works on every existing container. The library scales as multiplication rather than addition.

A small demonstration: std::find works the same way on a vector, a list, and a raw array.

One algorithm, three different containers, no template specialisation written by hand. The std::find source code never mentions vector or list.

The Half-Open Range [first, last)

Every STL algorithm takes a pair of iterators that mark a range. The convention is half-open: first points at the first element you want to process, and last points one past the last element. That last position is sometimes called the sentinel or end iterator, and you never dereference it.

Three things fall out of this convention. An empty range is first == last, which is easy to test. You can express the whole container with c.begin(), c.end(), and any sub-range without doing arithmetic that risks off-by-one bugs. Algorithms that did not find anything return last as their result, which is why so much code looks like if (it != v.end()).

The pattern is consistent enough that once you have learned it, every new algorithm reads the same way.

The second call hands min_element a smaller range. The algorithm does not know or care that the underlying vector has more elements before first or that the iterators came from a vector at all.

The Common Shape

Most algorithms follow one of two return shapes.

Algorithms that search or query return an iterator pointing at what they found, or last if they did not find anything: find, find_if, search, min_element, lower_bound, partition_point. Algorithms that count or fold return a value: count, count_if, accumulate, all_of, equal.

Algorithms that modify or move elements return an iterator that marks the new logical end of the range or the position just past the last written element: copy, transform, remove, unique. This is how the erase-remove idiom works, covered below.

The signature shape is almost always one of:

Once you can read those three shapes, the names and parameters of any specific algorithm fall into one of them.

Three different algorithms, three different return types, but the same iterator-pair entry point. Once familiar with the first, last, ... pattern, the calls read like English: "find if any cart price is above 30," "count if a price is under 20," "accumulate from the beginning to the end, starting at zero."

Predicates and the _if Family

A predicate is a callable that takes one or two elements and returns a bool. Algorithms that test elements come in two flavours: a "by value" version that compares against a fixed value, and an _if version that takes a predicate.

Plain_if version
find(first, last, val)find_if(first, last, pred)
count(first, last, val)count_if(first, last, pred)
remove(first, last, val)remove_if(first, last, pred)
replace(first, last, old, new)replace_if(first, last, pred, new)
copy(first, last, out)copy_if(first, last, out, pred)

The non-_if versions are convenient when you have a fixed value to look for. The _if versions are more powerful because the predicate can encode any condition.

Lambdas, available since C++11, are how predicates are written in modern code. Before lambdas, this required a separate function or a functor class. The lambda chapter covers the syntax in detail. These calls can be read as "for each element, run this block and use its bool result."

A family of quantifier algorithms takes a predicate and returns a bool directly.

all_of, any_of, and none_of short-circuit, meaning they stop the moment the answer is decided. any_of returns true as soon as one element matches; all_of returns false as soon as one element fails.

Quantifier algorithms are O(n) worst case but often much less because they bail out early. For an any_of that matches the first element, the cost is the cost of one predicate call.

Where the Algorithms Live

Most algorithms sit in <algorithm>. Numeric folds live in <numeric> because they grew out of a separate proposal. The C++20 ranges versions live in <ranges>.

HeaderContents
<algorithm>The bulk of generic algorithms: searching, sorting, modifying, partitioning, heaps, set operations
<numeric>Folds and scans: accumulate, reduce, transform_reduce, inner_product, partial_sum, iota
<ranges> (C++20)std::ranges::sort, std::ranges::find, and views like std::views::filter
<functional>Pre-built function objects used as predicates and comparators: std::less, std::greater, std::plus
<execution> (C++17)Execution policies for parallel and vectorised algorithms

Including the correct header is part of the contract. Code that uses std::accumulate will not compile against <algorithm> alone, and the error message can be cryptic.

A Map of the Algorithm Families

There are too many algorithms to learn individually. Knowing the families is what matters, so you know where to look. The remaining chapters in this section drill into each family; this is the map.

Non-Modifying

These read the range without changing it. They search, count, test, and compare.

AlgorithmJob
find, find_if, find_if_notLocate the first element matching a value or predicate
find_first_ofFirst occurrence of any element from a second range
adjacent_findFirst pair of equal (or predicate-matching) neighbours
count, count_ifCount matching elements
search, search_nFind a subsequence inside a range
mismatchFirst position where two ranges differ
equalAre two ranges element-wise equal?
all_of, any_of, none_ofQuantifiers
min_element, max_element, minmax_elementIterators to extreme elements

Modifying

These produce a new sequence somewhere, either in place or written to an output iterator. They never resize containers; that is an iterator-level operation, not an algorithm one.

AlgorithmJob
copy, copy_if, copy_n, copy_backwardCopy elements
move, move_backwardMove elements (uses std::move per element)
transformApply a function to each element, write the result somewhere
replace, replace_ifReplace elements that equal a value or match a predicate
fill, fill_nWrite the same value to every position
generate, generate_nCall a nullary function and write its result to each position
swap_rangesSwap elements between two ranges
reverse, rotate, shuffleRearrange elements

Removing

These do not actually remove anything from a container. They logically remove by shifting unwanted elements past a new end iterator, and you call the container's erase to physically shrink. This is the erase-remove idiom.

std::remove does not know what container it is working on. It cannot call erase. All it can do is overwrite the wanted elements toward the front and return an iterator pointing past them. The container is still seven elements long after the call. The erase call is what physically drops the tail. Other algorithms in this family are remove_if and unique (which removes consecutive duplicates).

std::remove is O(n) and does the work in one pass. The follow-up erase is also O(n) but only over the tail being dropped. Skipping the erase leaves the container's size wrong, which is a common mistake.

C++20 adds std::erase and std::erase_if free functions in <vector>, <list>, and friends that wrap the idiom into one call. With C++20, prefer them.

Sorting

The sorting family includes:

AlgorithmJob
sortSort the whole range, not stable
stable_sortSort and preserve relative order of equal elements
partial_sortSort the first k smallest into the front
nth_elementPlace the element that belongs at position n there, partition the rest
is_sorted, is_sorted_untilCheck sortedness

These require a sorted range as input. Calling them on an unsorted range is undefined behaviour. They run in O(log n) on random-access iterators.

AlgorithmJob
lower_boundFirst position where value could be inserted without breaking sort order
upper_boundFirst position strictly greater than value
equal_rangePair of (lower_bound, upper_bound)
binary_searchA bool: is the value present?

Searching gets its own chapter. binary_search only tells you yes or no, while lower_bound and upper_bound give you positions to work with.

Numeric

The folds and scans live in <numeric>.

AlgorithmJob
accumulateLeft fold with + (or a custom op)
reduce (C++17)Unordered fold, parallelisable
transform_reduce (C++17)Fused map-then-reduce, parallelisable
inner_productSum of products of two ranges
partial_sumRunning totals
iotaFill with successive integers starting at a given value
adjacent_differenceElement-wise differences

accumulate is the default. reduce is for parallelised operations, because reduce does not promise left-to-right order, which lets the implementation split the work across threads.

The third argument is the initial value of the accumulator. Its type also drives the result type, which is a common pitfall: std::accumulate(v.begin(), v.end(), 0) on a vector<double> accumulates into an int, truncating decimals without warning. Pass 0.0 to keep the math in double.

Heap

A heap is a binary tree laid out flat in an array such that the largest element is always at the front. The STL does not give you a heap container directly; it gives you a set of algorithms that maintain the heap invariant on any random-access range. std::priority_queue is a container adapter built on top of these.

AlgorithmJob
make_heapRearrange a range into a heap, O(n)
push_heapAdd the last element of the range to the heap, O(log n)
pop_heapMove the front (largest) to the back and re-heapify the rest, O(log n)
sort_heapSort a heap into ascending order, O(n log n)
is_heap, is_heap_untilCheck heap-ness

Execution Policies (C++17)

Since C++17, most standard algorithms accept an execution policy as their first argument that tells the implementation it is allowed to parallelise or vectorise the work. The policies live in <execution>.

PolicyWhat it allows
std::execution::seqSequential, in order, as if no policy
std::execution::parParallel: multiple threads may execute the algorithm
std::execution::par_unseqParallel and unsequenced: threads plus SIMD vectorisation, with stricter requirements
std::execution::unseq (C++20)Single-threaded but vectorisable

par_unseq is the most aggressive. It permits the implementation to interleave element processing across threads and within a thread via SIMD instructions. The trade is that your predicate or operation must be both thread-safe and free of memory ordering assumptions. Taking a mutex inside a par_unseq lambda is undefined behaviour because the same thread might "re-enter" the lambda while still holding the lock.

There is an ABI catch with these policies. On libstdc++ (g++) and libc++ historically, the parallel implementations were backed by Intel's TBB library. Compiling code that uses std::execution::par may require linking against TBB explicitly (-ltbb) or installing it through the system package manager. Standard libraries are moving toward self-contained implementations, but a linker error mentioning TBB has this cause.

Parallel execution only pays off when the per-element work is substantial or the range is large enough to amortise thread setup. For a par sum over a 100-element vector, the threading overhead usually exceeds the speedup. Profile before assuming parallel is faster.

Ranges (C++20)

C++20 introduces a redesign of the algorithm interface that lets you pass a container directly instead of two iterators.

std::ranges::sort(prices) is equivalent to std::sort(prices.begin(), prices.end()). The ranges versions also support projections, which let you sort by a member or computed value without writing a comparator. The Ranges Library chapter in the Modern C++ Features section covers all of this. Every classic algorithm has a std::ranges:: cousin that is shorter to call. The iterator-pair versions are not going away; they are still what the new versions are built on.

Practical Walkthrough: A Cart Summary

A practical example: given a cart of products with prices and quantities, compute the subtotal, find the most expensive item, and report how many items are on offer (price below a threshold).

Three different algorithms, three different return shapes, one consistent calling convention. None of the algorithms know that CartItem exists. The lambdas teach each one what to do with the elements, and the iterators carry the data through.

Quiz

STL Algorithms Overview Quiz

10 quizzes