Algorithm Intuition

This page exists to catalog and explore the C++ STL algorithms, and to name and justify missed algorithms.

The name "algorithm intuition" comes from Kate Gregory by way of Conor Hoekstra. It refers to an ability to look at a problem and break it down into a series of high-level algorithmic steps, rather than as a low-level series of discrete steps, in the same way as skilled programmers are able to look at a set of characteristics and select the correct abstract data structure, without having to think about the discrete nodes or pointers that make it up. Building up an intuition for algorithms is essential to being a great C++ programmer, and makes a huge difference in the clarity, consiceness, and ultimately correctness of one's code.

This webpage is an attempt to take the long list of over a hundred named algorithms and break them down into families, providing an easy high-level view of what they do, expressed in the form of input, output, and abstract operations.

I do not actually know if a search (and particularly, a search which returns a whole subrange) is strictly speaking a catamorphism, but even if they are not, there are very strong links between several of the searching and folding algorithms, and several of the folds can be implemented in terms of a related search algorithm. I am also uncertain about whether everything I have labelled as an anamorphism truly is one, as explained below.

Blue names indicate algorithms described by Conor. Green names are from kblib. Red names in the "Default operations" column indicate non-overridable parameters and a lack of generality. The notes below the tables explain the rest of the terminology and notation I used.
Where kblib provides an alternate definition of a standard library algorithm, usually only the standard algorithm is shown, unless the two differ in some significant way. For minor differences, which affect only one or two columns, two values may be shown, with the kblib version colored green.


Catamorphisms (folds) and Searches

NameInput ranges*Accumu­latorReturnsOperationsDefault operationsCompl­exityOrderCompare to
inner_product 2 Arg Value A, bT plus, multiplies Unspc. Fwd. transform_reduce
adjacent_reduce 1s Arg Value A, bT Unspc. Fwd. inner_product
transform_reduce 1 / 2 Arg Value acR, uT / bT plus, multiplies O(N)
find 1+Value Position equal_to O(N) S/C
find_if, find_if_not 1 Position uP O(N) S/C
find_first_of 2 Position bP equal_to O(S×N) S/C find_if
min_element, max_element 1 First Position bP less O(N), =(max(N-1, 0)) Fwd.
minmax_element 1 First 2 Positions bP less O(N), ≤(max(floor((3/2)*(N−1)), 0)) Fwd.
lower_bound, upper_bound 1+Value Position bP less O(log N) + O(1)↓R; O(N) B/S
equal_range 1+Value Range bP less O(log N) + O(1)↓R; O(N) B/S
search 2 Position bP equal_to ≤(S×N)
find_end 2 Position bP equal_to ≤(S×(N-S+1)) search
starts_with 2 bool bP equal_to N > S ? O(S) : O(1)↓R; O(min(S, N)) Fwd.
ends_with 2 bool bP equal_to N > S ? O(S) : O(1)↓R; O(min(S, N))
ranges::starts_with, ranges::ends_with 2 bool bP, uT, uTΔ equal_to, identity, identity ≤(min(S, N))
search (C++17) 1 Position Searcher Depends on Searcher
find_match 2 Position bP equal_to O(N), O(min(N, M)) S/C
mismatch 2 Position bP equal_to O(N), O(min(N, M)) S/C find_match
adjacent_find 1s Position bP equal_to =(min((result-first)+1, (last-first)-1)); O(N)↓∥ S/C find_match
is_sorted_until 1s Position bP less O(N) S/C find_match
search_n 1 + Count + Value Position bP equal_to ≤(N) adjacent_find
accumulate 1 Arg Value A plus Unspc.; O(N) Fwd.
sum 1 First Value R plus O(N) Fwd. accumulate
reduce 1 Arg Value acR plus O(N)
count 1+Value 0 size_t equal_to =(N) Fwd. accumulate
count_if 1 0 size_t uP =(N) Fwd. accumulate
binary_search 1+Value bool bP less O(log N) + O(1)↓R; O(N) B/S
is_partitioned 1 bool uP O(N) Fwd.
is_sorted 1 bool bP less O(N) Fwd.
is_heap 1 bool bP less O(N)
is_permutation 2 bool bP equal_to N==M ? O(N2) : O(1)↓R; O(N2)
includes 2 bool bP less O(N+M), ≤(2×(N+M-1)) Fwd.
first_result 1+Value / 2+Value Arg Value uT / bT O(N) S/C
first_result_if 1 / 2 Value uT / bT, uP / bP O(N) S/C
all_of, none_of 1 true bool uP O(N) S/C first_result
any_of 1 false bool uP O(N) S/C first_result
contains 1+Value false bool equal_to O(N) S/C first_result
contains_any 2 false bool contains O(S×N) S/C first_result
equal 2 bool bP equal_to N==M ? ≤(N) : O(1)↓R; ≤(N); O(N)↓∥ Fwd. first_result
lexicographical_compare 2 bool bP less ≤(2×min(N, M)) Fwd. first_result
lexicographical_compare_three_way 2 ordering CompareΔ compare_three_way <(min(N, M)) Fwd. first_result
partition_point 1 Position uP O(log N)↓R; O(N) B/S
is_heap_until 1 Position bP less O(N)
NameInput ranges*Accumu­latorReturnsOperationsDefault operationsCompl­exityOrderCompare to

Anamorphisms (unfolds/transforms)

NameInput ranges*Accumu­latorOutput rangesOperationsDefault operationsCompl­exityOrderCompare to
transform_inclusive_scan 1 First, Arg 1 aR, uT O(N)
transform_exclusive_scan 1 Arg 1 aR, uT O(N)
partial_sum 1 First 1 A plus O(N), =(N - 1) Fwd.
inclusive_scan 1 First, Arg 1 aR plus O(N)
exclusive_scan 1 Arg 1 aR plus O(N)
adjacent_difference 1s Quasi 1 fD minus O(N), =(N - 1) Fwd. adjacent_transform
adjacent_transform 1s Quasi 1 D O(N), =(N - 1) Fwd. adjacent_difference
adjacent_inclusive_scan 1s First 1 A, D O(N) Fwd.
transform 1 / 2 1 uT / bT =(N)
copy, copy_n 1 1 =(N) Fwd. transform
copy_backward 1 1 =(N) Rev.
move 1 1 =(N) Fwd. shift_left
move_backward 1 1 =(N) Rev. shift_right
replace_copy 1+Value Arg 1 equal_to =(N) Fwd. transform
replace_copy_if 1 Arg 1 uP =(N) Fwd. transform
reverse_copy 1 1 =(N) transform
rotate_copy 1d 1 O(N)
sample 1 Arg 1 O(N) Fwd.
partial_sort_copy 1 1 bP less O(N log(min(D,N))
partition_copy 1 2 uP =(N) Fwd.
transform_if 1 1 uP, uT O(N) Fwd.
copy_if, remove_copy_if 1 1 uP O(N) Fwd. transform_if
remove_copy 1+Value 1 equal_to =(N) Fwd. transform_if
unique_copy 1 First 1 bP equal_to O(N) Fwd.
set_difference, set_intersection, set_symmetric_difference, set_union 2 1 bP less O(N+M), ≤(2⋅(N+M)−1) Fwd.
merge 2 1 bP less O(N+M) Fwd.
regex_replace 2 1 Regex Unspc. See note
search_replace_copy 3 1 bP equal_to O(N×(S+R)) regex_replace
generate, generate_n 0 1 G =(N) Fwd.
iota 0 Arg 1 ++ =(N) Fwd. generate
iota 0 Arg 1 uT ++ =(N) Fwd. generate
fill, fill_n 0 Arg 1 =(N) Fwd. generate, generate_n
for_each, for_each_n 1 0 muT =(N) Fwd.
for_each, for_each_n 1 / 2 0 muT / mbT =(N) Fwd.
swap_ranges 2 Self swap O(N) Fwd. for_each
remove 1+Value Self equal_to O(N) Fwd.
remove_if 1 Self uP O(N) Fwd.
unique 1 Self bP equal_to O(N) Fwd.
replace 1+Value Self equal_to =(N) Fwd. transform
replace_if 1 Self uP =(N) Fwd. transform
reverse 1 Self O(N), =(N/2) Yes swap_ranges
rotate 1d Self O(N) Yes
shift_left 1d Self ≤(N-n) Fwd. move
shift_right 1d Self ≤(N-n) Yes move_backward
shuffle 1+URBG Self O(N)
next_permutation, prev_permutation 1 Self O(N), ≤(N/2)
partition 1 Self uP O(N); O(N log N)↓∥
stable_partition 1 Self uP O(N) w/ memory, else O(N log N); O(N log N)↓∥
sort 1 Self bP less O(N log N)
partial_sort 1 Self bP less O(N log M)
stable_sort 1 Self bP less O(N log N) w/ memory, else O(N log(N)2)
nth_element 1 Self bP less O(N); O(N log N)↓∥
make_heap 1 Self bP less O(N), ≤(3N)
push_heap, pop_heap 1 Self bP less O(log N)
sort_heap 1 Self bP less O(N log N)
inplace_merge 1d Self bP less =(N-1) w/ memory, O(N log N); O(N log N)↓∥
NameInput ranges*Accumu­latorOutput rangesOperationsDefault operationsCompl­exityOrderCompare to

Named Operators and Function Objects

NameArityClass (<T>)Signature (<T>)Class (<void>)Definition
plus2(ac)R(T, T) → T bTx + y
minus2R(T, T) → T bTx - y
multiplies2(ac)R(T, T) → T bTx * y
divides2R(T, T) → T bTx / y
modulus2R(T, T) → T bTx % y
negate1uT(T) → T uT-x
equal_to2(c)bP(T, T) → bool bTx == y
not_equal_to2(c)bP(T, T) → bool bTx != y
less2bP(T, T) → bool bTx < y
greater2bP(T, T) → bool bTx > y
less_equal2bP(T, T) → bool bTx <= y
greater_equal2bP(T, T) → bool bTx >= y
logical_and2(c)bP(T, T) → bool bTx and y
logical_or2(c)bP(T, T) → bool bTx or y
logical_not1uP(T) → bool uTnot x
bit_and2(ac)R(T, T) → T bTx & y
bit_or2(ac)R(T, T) → T bTx | y
bit_xor2(ac)R(T, T) → T bTx ^ y
bit_not1uT(T) → T uT~x
NameArityClass (<T>)Signature (<T>)Class (<void>)Definition

* 's' indicates staggered access to a range. 'd' indicates that a second position in the range is also required.
 The abbreviations used for operations are described below.

Operation types

(A, T)A
(T, T)T
Adjacent Op
(T, T)U

Operation semantics

op(a, op(b, c))op(op(a, b), c)
op(a, b)op(b, a)
op(b, a)
op(a, b)
op(a) may modify a

If an operation is specified to be associative (but not commutative), and the actual operation provided is not, the result is potentially non-deterministic, however, it is not UB for any such algorithm, as far as I can tell.
 A blank space means unspecified evaluation/traversal order. "S/C" means forward order with short-circuiting (early return). "B/S" means binary search. "Yes" means that an order is specified, but it is not one of the above. The mark indicates that an algorithm may execute out-of-order and/or in parallel, and may accept an execution policy. It may be used to indicate that a given time complexity only pertains to the parallel version, see next note.
Several algorithms are defined to have different time complexities for different iterator concepts, such as between random access and others, or between forward and input iterators, or anything else. For such algorithms, the computational complexity is marked with an arrow followed by a letter, indicating which iterator category that complexity guarantee applies to. The letter codes are: C for contiguous, R for Random Access, B for Bidirectional, F for Forward, and I for Input. Additionally, the ∥ mark indicates that the given complexity guarantee applies to the parallel version of the algorithm.
Δ This analysis makes no distinction between unary transformations and projections as used in the ranges library, nor does it make any distinction between Compare predicates and general predicates. lexicographical_compare_three_way is unique in that the Compare predicate it requires must return one of the standard three-way ordering types (strong_ordering, weak_ordering, or partial_ordering), rather than bool.
 For the purposes of this analysis, returning an iterator, an index, or a pair of corresponding iterators in different ranges (i.e. mismatch) are all considered to be a single "position". "Range" as a return value of a catamorphism indicates that two iterators identifying a subrange of the input are returned, not that a new range is produced. "2 Positions" means two unassociated iterators are returned, which do not form a range.
 I'm not certain if for_each is, strictly speaking, an anamorphism, because it does not directly produce any output. However, I include it in the anamorphisms table because, as a void function it doesn't neatly fit into this analysis, and because swap_ranges, which clearly is an anamorphism, can be implemented in terms of (the binary version of) it.
 regex_replace is a strange case for this list because it's not really meant to be an algorithm. However, the regular expression can be considered a kind of predicated in exactly the same way as the Searcher of search (C++17) can be, and the actual operation performed is a kind of conditional two-input copy, in a similar way as set_intersection et al. are, but with the conditionality being somewhat more like unique_copy in that it relates to the relationship between nearby elements instead of their individual values. Another unusual thing is that it can return its output as a string by value, in addition to writing to an output iterator. Despite that, I'm considering it an anamorphism because its output is a range of the same kind as its input, even if it is represented in the language as a value type, unlike those catamorphisms which can be used to return ranges, but which, for analytical purposes, return values. I wrote a more generic search_replace_copy, which is a much more normal entry for this list.



This page would not exist without Conor Hoekstra's two-part talk "Algorithm Intuition" (CppCon 2019, parts one and two) to inspire me and provide the basic framework for me to expand upon. Jonathan Boccara's talk "105 STL Algorithms in Less Than an Hour" (CppCon 2018) was also very helpful for me in framing this work.

These presentations themselves reference excellent presentations by Sean Parent ("C++ Seasoning", GoingNative 2013), Marshall Clow ("STL Algorithms - why you should use them, and how to write your own", CppCon 2016), and Kate Gregory ("It's Complicated", Meeting C++ 2017 Keynote, "Simplicity: not just for beginners", ACCU 2018, and her appearance on CppCast, episode 30, "Stop Teaching C (When Teaching C++)").