Skip to content
C++
Library
since C++98
Basic

STL Algorithms Overview

Complete guide to <algorithm> and std::ranges algorithms — sorting, searching, transforming, partitioning, and parallel execution.

STL Algorithmssince C++98

Generic functions in <algorithm> that operate on iterator-delimited half-open ranges to sort, search, transform, and partition sequences — decoupled from any specific container type.

Overview

The <algorithm> header exposes over 100 algorithms built on one abstraction: a half-open range [first, last) described by two iterators. The algorithm is indifferent to what those iterators point into — std::vector, a raw array, a std::deque, or a custom container — which is the source of their reusability.

C++20 introduced two upgrades. First, std::ranges::* counterparts that accept range objects directly, eliminating the repetitive .begin()/.end() boilerplate. Second, projections — a callable applied to each element before comparison — letting you sort by a member field without writing a comparator.

cpp
// C++98/11: iterator-pair form
std::sort(v.begin(), v.end());
std::sort(v.begin(), v.end(), std::greater<int>{});

// C++20: ranges form — accepts the container directly
std::ranges::sort(v);
std::ranges::sort(v, std::greater{});

// C++20: projection — sort by member without a lambda
std::ranges::sort(people, {}, &Person::age);

Execution policies (C++17, <execution>) let the classic std::* iterator-pair algorithms run in parallel or with SIMD vectorisation. The std::ranges::* algorithms do not accept execution policies as of C++23; fall back to the std::* iterator-pair forms when you need std::execution::par.

std::sort has been guaranteed O(n log n) worst-case since C++11 (implementations use introsort — quicksort with a heapsort fallback). Prior to C++11, only average-case O(n log n) was required.


Sorting

cpp
std::ranges::sort(v);                             // C++20
std::ranges::sort(v, std::greater{});             // descending
std::ranges::sort(v, {}, &S::field);              // by member projection — C++20
std::ranges::stable_sort(v);                      // preserves relative order of equals

// partial_sort: only first n positions are fully sorted — O(n log k)
std::ranges::partial_sort(v, v.begin() + 5);

// nth_element: O(n) average — element at pos as if sorted; others are partitioned
std::ranges::nth_element(v, v.begin() + v.size() / 2);  // median pivot

bool ok = std::ranges::is_sorted(v);
auto it  = std::ranges::is_sorted_until(v);  // iterator to first out-of-order element

Searching

cpp
// Linear search — O(n)
auto it  = std::ranges::find(v, 42);
auto it2 = std::ranges::find_if(v, [](int n){ return n > 10; });
auto it3 = std::ranges::find_if_not(v, pred);

// Binary search — O(log n), range MUST be sorted
bool found      = std::ranges::binary_search(v, 42);
auto lo         = std::ranges::lower_bound(v, 42);   // first element >= 42
auto hi         = std::ranges::upper_bound(v, 42);   // first element > 42
auto [lo2, hi2] = std::ranges::equal_range(v, 42);   // subrange where value == 42

// Subsequence search
auto r = std::ranges::search(haystack, needle);       // first occurrence of needle
auto e = std::ranges::find_end(haystack, needle);     // last occurrence
std::ranges::search_n(v, 3, 0);                       // 3 consecutive zeros

// Min / max
auto mn_it      = std::ranges::min_element(v);
auto mx_it      = std::ranges::max_element(v);
auto [mn, mx]   = std::ranges::minmax(v);             // returns min_max_result — C++20
std::ranges::clamp(val, lo_val, hi_val);              // std::clamp since C++17

Predicates

cpp
std::ranges::all_of(v, pred);
std::ranges::any_of(v, pred);
std::ranges::none_of(v, pred);
std::ranges::count(v, val);
std::ranges::count_if(v, pred);

std::ranges::contains(v, val);   // C++23 only — use find != end in C++20

std::ranges::contains requires C++23. In C++20, write std::ranges::find(v, val) != std::ranges::end(v).


Transforming

cpp
// copy
std::ranges::copy(src, std::back_inserter(dst));
std::ranges::copy_if(src, std::back_inserter(dst), pred);
std::ranges::copy_n(src.begin(), 5, dst.begin());
std::ranges::copy_backward(src, dst_end);

// transform (map)
std::ranges::transform(v, dst.begin(), [](int n){ return n * 2; });
std::ranges::transform(a, b, dst.begin(), std::plus{});  // binary form

// generate / fill
std::mt19937 rng{std::random_device{}()};
std::uniform_int_distribution<int> dist(0, 100);
std::ranges::generate(v, [&]{ return dist(rng); });
std::ranges::fill(v, 0);
std::ranges::fill_n(v.begin(), 5, -1);

// replace
std::ranges::replace(v, 0, -1);
std::ranges::replace_if(v, pred, -1);
std::ranges::replace_copy(src, dst.begin(), 0, -1);

// reverse / rotate
std::ranges::reverse(v);
std::ranges::rotate(v, v.begin() + 3);   // rotate left by 3 positions
std::ranges::rotate_copy(v, v.begin() + 3, dst.begin());

Removing

std::ranges::remove shifts surviving elements to the front and returns a subrange representing the dead tail [new_end, old_end). It does not resize the container — you must call erase separately.

cpp
// Correct: use the returned subrange to erase the tail
auto tail = std::ranges::remove(v, 0);       // C++20 — returns subrange
v.erase(tail.begin(), tail.end());

// C++20 one-liners — strongly prefer for vector/deque/string/list
std::erase(v, 0);                            // C++20
std::erase_if(v, [](int n){ return n < 0; }); // C++20

// Deduplicate (sort first — unique only removes consecutive duplicates)
std::ranges::sort(v);
auto dup_tail = std::ranges::unique(v);      // C++20 — returns subrange
v.erase(dup_tail.begin(), dup_tail.end());

Partitioning

cpp
std::ranges::partition(v, pred);               // relative order not preserved
std::ranges::stable_partition(v, pred);        // preserves relative order — O(n log n)

bool ok  = std::ranges::is_partitioned(v, pred);
auto mid = std::ranges::partition_point(v, pred);  // O(log n) on partitioned range

Set Operations (sorted ranges)

All set algorithms require both input ranges to be sorted under the same comparator. Violating this precondition is undefined behaviour.

cpp
std::ranges::merge(a, b, out_it);
std::ranges::set_union(a, b, out_it);
std::ranges::set_intersection(a, b, out_it);
std::ranges::set_difference(a, b, out_it);
std::ranges::set_symmetric_difference(a, b, out_it);
std::ranges::inplace_merge(v, mid);
bool subset = std::ranges::includes(a, b);    // true if every element of b is in a

Heap Operations

cpp
std::ranges::make_heap(v);    // O(n) — establishes max-heap invariant
std::ranges::push_heap(v);    // insert v.back() into heap — O(log n)
std::ranges::pop_heap(v);     // moves max to v.back(), re-heapifies — O(log n)
std::ranges::sort_heap(v);    // O(n log n) — heap must be valid first
bool h = std::ranges::is_heap(v);

Permutations

cpp
std::ranges::next_permutation(v);       // C++20 — advances to next lexicographic permutation
std::ranges::prev_permutation(v);
std::ranges::shuffle(v, rng);           // Fisher-Yates; std::shuffle since C++11, ranges since C++20
std::ranges::sample(src, out, n, rng);  // reservoir sample; std::sample since C++17, ranges since C++20
std::ranges::is_permutation(a, b);

std::random_shuffle was deprecated in C++14 and removed in C++17. Always use std::shuffle (C++11) or std::ranges::shuffle (C++20) with an explicit URBG.


Parallel Execution (C++17)

Only the classic std::* iterator-pair algorithms accept execution policies. std::ranges::* algorithms have no parallel overloads as of C++23.

cpp
#include <execution>

std::sort(std::execution::par, v.begin(), v.end());
std::transform(std::execution::par_unseq,
               v.begin(), v.end(), dst.begin(), fn);

// Execution policies (C++17 unless noted):
// seq        — sequential; identical to omitting a policy
// par        — parallel threads
// par_unseq  — parallel + SIMD; callable must not lock or allocate
// unseq      — SIMD only, single thread (C++20)

par_unseq permits the runtime to interleave SIMD lanes across threads. Any callable passed under this policy must be free of locks, memory allocation, and non-vectorisable control flow.


Best Practices

Prefer std::ranges::* in C++20 codebases. Projections replace single-field lambdas, accepting a range object removes iterator boilerplate, and the result types carry richer information (e.g., in_out_result, min_max_result). Fall back to the std::* iterator-pair forms only when you need parallel execution.

Use std::erase / std::erase_if over the erase-remove idiom (C++20+). The two-step pattern is error-prone and the C++20 free functions are available for vector, deque, string, list, forward_list, and the associative containers.

Exploit projections to avoid temporary transforms.

cpp
// Unnecessary allocation:
std::vector<std::string> names;
std::ranges::transform(people, std::back_inserter(names), &Person::name);
std::ranges::sort(names);

// Sort the original by field directly — no allocation:
std::ranges::sort(people, {}, &Person::name);

Know the complexity contracts. partial_sort is O(n log k) where k is the sorted prefix length — much cheaper than full sort when k ≪ n. nth_element is O(n) average with introselect implementations guaranteeing O(n) worst-case. stable_sort degrades to O(n log² n) when it cannot obtain extra memory.


Common Pitfalls

Calling binary search algorithms on unsorted ranges. binary_search, lower_bound, upper_bound, equal_range, and all set operations have a sorted-input precondition. They will silently produce wrong results — not a crash or an exception.

Forgetting erase after remove or unique. The algorithm only reorders elements; size() and end() are unchanged until you explicitly erase the tail.

cpp
// Bug: v retains its original size; the tail holds garbage
std::ranges::remove(v, 0);

// Correct
v.erase(std::ranges::remove(v, 0).begin(), v.end());
// Or prefer the C++20 one-liner:
std::erase(v, 0);

Using std::ranges::contains in a C++20 project. It is a C++23 addition. Compilers targeting C++20 will reject it; use std::ranges::find(v, val) != std::ranges::end(v).

Calling std::sort on std::list. std::sort requires random-access iterators; std::list provides only bidirectional iterators. The code will not compile. Use list.sort(), which is a member function.

Using par_unseq with stateful callables. Any mutex, allocation, or non-vectorisable branch inside the callable can deadlock or corrupt state. Under par_unseq the implementation may execute multiple loop iterations in a single thread via SIMD before switching threads.


See Also

  • std::ranges adaptors — lazy, composable views over ranges
  • <numeric>std::accumulate, std::reduce (C++17 parallel), std::iota, std::partial_sum
  • <execution> — execution policy types and requirements
  • Iterators — iterator categories, concepts, and the requirements each algorithm imposes