STL Algorithm Mastery
Reviewed & published by Brayan K
By the end of this lesson you'll replace hand-written loops with the C++ Standard Library's <algorithm> and <numeric> tools — sorting with your own rules, searching, summing, transforming, and safely deleting elements — using one line where you used to write fifteen.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Sort a range with std::sort and a custom comparator lambda
- Search with std::find, count with std::count and std::count_if
- Fold a range to one value with std::accumulate (from <numeric>)
- Reshape data with std::transform and act with std::for_each
- Find extremes with std::max_element / std::min_element
- Delete elements correctly with the erase-remove idiom
💡 Real-World Analogy
Think of STL algorithms as power tools. You could cut every plank with a hand saw (your own for loop), but a builder reaches for the right power tool: a circular saw to cut (sort), a stud finder to locate (find), a tape measure to total up (accumulate). Each tool clamps onto a stretch of work — that's the iterator range, from begin() to end(). You supply the rule (a lambda: "cut here", "keep this one") and the tool does the repetitive work, faster and with fewer mistakes than doing it by hand.
1. Sort, Find & Count
std::sort rearranges a range in place — ascending by default. To sort by your rule, pass a third argument: a comparator. A comparator is a lambda that takes two elements and returns a bool answering one question — "should a come before b?" Return a > b and you've flipped it to descending. find hunts for an exact value and hands back an iterator (compare it to end() to check for a miss), while count and count_if tally matches. Read this worked example, run it, then you'll write your own.
#include <iostream>
#include <vector>
#include <algorithm> // sort, find, count, count_if, min/max_element
using namespace std;
int main() {
vector<int> nums = {42, 17, 8, 95, 23, 61, 3};
// sort — rearranges the range in place, ascending by default.
// You pass a RANGE: begin() ... end() (end is one-past-the-last).
sort(nums.begin(), nums.end());
cout << "Ascending: ";
for (int n : nums) cout << n << " "; // 3 8 17 23 42 61 95
cout << endl;
// Custom comparator — a lambda that returns "should a come before b?".
// Returning a > b sorts descending.
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // biggest first
});
cout << "Descending: ";
for (int n : nums) cout << n << " "; // 95 61 42 23 17 8 3
cout << endl;
// find — looks for an exact value, returns an iterator (or end()).
auto it = find(nums.begin(), nums.end(), 23);
if (it != nums.end())
cout << "Found 23 at index "
<< distance(nums.begin(), it) << endl; // index 3
// count — how many elements equal a value.
cout << "How many 42s? " << count(nums.begin(), nums.end(), 42) << endl; // 1
// count_if — how many satisfy a predicate (a lambda returning bool).
int evens = count_if(nums.begin(), nums.end(),
[](int n) { return n % 2 == 0; });
cout << "Even numbers: " << evens << endl; // 3 (42, 8, ...)
return 0;
}
// ⚠️ No expected-output panel for this one, on purpose.
// These numbers are stopwatch readings. They depend on the machine, the
// compiler and what else it is doing, so only the direction of the
// comparison is the lesson — never the figures.
//
// One real run on the machine that builds this site printed:
// Ascending: 3 8 17 23 42 61 95
// Descending: 95 61 42 23 17 8 3
// Found 23 at index 3
// How many 42s? 1
// Even numbers: 2Your turn. The program below is almost complete — fill in the two blanks marked ___ using the hints, then run it and check it against the expected output.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
vector<int> scores = {72, 48, 91, 63, 30, 88, 55};
// 1) Sort scores HIGHEST first using a comparator lambda.
// A comparator returns "should a come before b?".
sort(scores.begin(), scores.end(), [](int a, int b) {
return ___; // 👉 a > b (so bigger values come first)
});
cout << "Ranked: ";
for (int s : scores) cout << s << " ";
cout << endl;
// 2) Count how many scores are a PASS (60 or more) with count_if.
int passes = count_if(scores.begin(), scores.end(), [](int s) {
return ___; // 👉 s >= 60
});
cout << "Passes: " << passes << endl;
// ✅ Expected output:
// Ranked: 91 88 72 63 55 48 30
// Passes: 4
return 0;
}2. Accumulate, Transform & for_each
std::accumulate (from <numeric>, not <algorithm>) folds a whole range into a single value — its third argument is both the starting total and the type of the result. std::transform applies a function to every element and writes the results into another range. std::for_each runs an action on each element without producing a new range. And min_element / max_element return an iterator to the smallest or largest value — remember to dereference it with * to read the value out.
#include <iostream>
#include <vector>
#include <algorithm> // transform, for_each, min_element, max_element
#include <numeric> // accumulate (lives here, NOT in <algorithm>)
using namespace std;
int main() {
vector<int> prices = {1299, 2499, 599, 3999, 899}; // cents
// accumulate — fold a whole range into ONE value.
// The 3rd argument (0) is the starting total AND fixes the type.
int totalCents = accumulate(prices.begin(), prices.end(), 0);
cout << "Total cents: " << totalCents << endl; // 9295
// transform — apply a function to every element, write into a new range.
// Here: cents -> dollars. dollars must already be the right size.
vector<double> dollars(prices.size());
transform(prices.begin(), prices.end(), dollars.begin(),
[](int cents) { return cents / 100.0; });
cout << "In dollars: ";
for (double d : dollars) cout << "$" << d << " "; // $12.99 $24.99 ...
cout << endl;
// min_element / max_element — return an ITERATOR to the smallest/largest.
// Dereference with * to read the value.
auto cheapest = min_element(prices.begin(), prices.end());
auto priciest = max_element(prices.begin(), prices.end());
cout << "Cheapest: " << *cheapest << "c" << endl; // 599c
cout << "Priciest: " << *priciest << "c" << endl; // 3999c
// for_each — run an action on every element (no new range produced).
cout << "All prices: ";
for_each(prices.begin(), prices.end(), [](int c) {
cout << c << "c ";
});
cout << endl;
return 0;
}
// ✅ Expected output:
// Total cents: 9295
// In dollars: $12.99 $24.99 $5.99 $39.99 $8.99
// Cheapest: 599c
// Priciest: 3999c
// All prices: 1299c 2499c 599c 3999c 899cNow you try. Total a list of workout minutes, then convert each one to hours. Fill in the two blanks — watch the type on the accumulate seed and the division:
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
// 🎯 YOUR TURN — fill in the blanks, then run.
vector<int> minutes = {30, 45, 15, 60, 20}; // workout lengths
// 1) Add them all up with accumulate. Start the total at 0.
int totalMins = accumulate(minutes.begin(), minutes.end(), ___); // 👉 0
cout << "Total minutes: " << totalMins << endl;
// 2) Convert each value to HOURS (a double) using transform.
// Divide by 60.0 so you keep the decimals.
vector<double> hours(minutes.size());
transform(minutes.begin(), minutes.end(), hours.begin(),
[](int m) { return ___; }); // 👉 m / 60.0
cout << "In hours: ";
for (double h : hours) cout << h << " ";
cout << endl;
// ✅ Expected output:
// Total minutes: 170
// In hours: 0.5 0.75 0.25 1 0.333333
return 0;
}3. Deleting Elements: the Erase-Remove Idiom
This is the one that surprises everybody. std::remove and std::remove_if do not actually delete anything. An algorithm only sees a pair of iterators — it has no power to resize the container. So remove just shuffles the elements you're keeping to the front and returns an iterator to the new logical end; the leftover slots are still there. To truly shrink the container you pass that iterator to the container's own erase() method. Doing both together is the famous erase-remove idiom.
#include <iostream>
#include <vector>
#include <algorithm> // remove, remove_if
using namespace std;
void printVec(const string& label, const vector<int>& v) {
cout << label << ": ";
for (int n : v) cout << n << " ";
cout << endl;
}
int main() {
// remove() does NOT delete anything. It shifts the elements you keep
// to the front and returns an iterator to the new "logical end".
// Everything after that point is leftover junk — still in the vector!
vector<int> data = {1, 2, 3, 2, 4, 2, 5};
auto newEnd = remove(data.begin(), data.end(), 2); // removes the 2s
printVec("After remove (NOT erased yet)", data); // 1 3 4 5 ? ? ?
cout << "Size is still " << data.size() << "!" << endl; // 7
// erase() is the container's OWN method — it actually shrinks the vector.
// Pass it the iterator remove() returned, up to the real end().
data.erase(newEnd, data.end());
printVec("After erase (the erase-remove idiom)", data); // 1 3 4 5
cout << "Size now " << data.size() << endl; // 4
// The idiom in one line: remove_if + erase.
// remove_if drops every element whose predicate returns true.
vector<int> scores = {85, 42, 93, 67, 51, 28, 76};
scores.erase(
remove_if(scores.begin(), scores.end(),
[](int s) { return s < 60; }), // drop fails (< 60)
scores.end()
);
printVec("Passing scores (>= 60)", scores); // 85 93 67 76
return 0;
}
// ✅ Expected output:
// After remove (NOT erased yet): 1 3 4 5 4 2 5
// Size is still 7!
// After erase (the erase-remove idiom): 1 3 4 5
// Size now 4
// Passing scores (>= 60): 85 93 67 76Pro Tips
- 💡 A comparator must be a strict weak ordering: use < not <=. Returning true for equal elements breaks sort and can crash your program.
- 💡 min/max_element return iterators, not values. Dereference with *: int hi = *max_element(v.begin(), v.end());.
- 💡 Seed accumulate with the right type: accumulate(v.begin(), v.end(), 0.0) sums as double; 0 would truncate to int.
- 💡 Always pair remove_if with erase — by itself it leaves stale elements behind the new end.
Common Errors (and the fix)
- "My remove didn't remove anything!": remove only shifts elements and returns the new end — it can't resize the container. You must follow it with v.erase(newEnd, v.end()). That's the erase-remove idiom.
- Crash / "invalid comparator": your comparator returned true for equal elements (you wrote <= instead of <). A comparator must be a strict weak ordering — strictly <, never <=.
- "'accumulate' was not declared in this scope": you forgot #include <numeric>. accumulate lives there, not in <algorithm>.
- Garbage value from max_element: you forgot the *. The function returns an iterator; read the value with *max_element(...).
- Off-by-one / mismatched ranges: end() points one past the last element, so a range is [begin, end). Passing v.end() as a start, or mixing iterators from two different containers, is undefined behaviour.
📋 Quick Reference
| Task | Code | Returns |
|---|---|---|
| Sort ascending | sort(v.begin(), v.end()) | void (in place) |
| Sort by rule | sort(b, e, [](int a, int b){ return a>b; }) | void (in place) |
| Find a value | find(v.begin(), v.end(), 42) | iterator / end() |
| Count matches | count_if(b, e, pred) | how many |
| Sum a range | accumulate(b, e, 0) | the total |
| Map each element | transform(b, e, out, fn) | writes to out |
| Largest value | *max_element(b, e) | the value |
| Delete matches | v.erase(remove_if(b, e, pred), e) | shrinks v |
Mini-Challenge: Temperature Report
No blanks this time — just a brief and an outline to keep you on track. Combine sort, min_element/max_element, accumulate, and count_if into one small report. Build it, run it, and check your output against the example in the comments.
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: Temperature report
vector<int> temps = {18, 25, 31, 14, 22, 29, 16};
// 1. Sort temps from COLDEST to hottest (a comparator lambda, or plain sort).
// 2. Use min_element / max_element to print the low and the high.
// Remember they return iterators — dereference with * to read the value.
// 3. Use accumulate to get the total, then divide by temps.size() for the
// average (cast to double so you keep decimals).
// 4. Use count_if to count how many days were "warm" (>= 25).
//
// ✅ Example output:
// Low: 14 High: 31
// Average: 22.1429
// Warm days (>= 25): 3
// your code here
return 0;
}🎉 Lesson Complete
- ✅ sort orders a range in place; a comparator lambda sets your own rule (strict <, never <=)
- ✅ find returns an iterator; count / count_if tally matches
- ✅ accumulate (from <numeric>) folds a range to one value — seed it with the right type
- ✅ transform reshapes data into another range; for_each acts on each element
- ✅ min_element / max_element return iterators — dereference with *
- ✅ remove_if doesn't shrink anything — pair it with erase() (the erase-remove idiom)
Practice quiz
Which header is std::sort, std::find, and std::count_if declared in?
- <numeric>
- <vector>
- <algorithm>
- <functional>
Answer: <algorithm>. sort, find, count_if, transform, for_each, remove_if and min/max_element all live in <algorithm>.
Which header does std::accumulate live in?
- <numeric>
- <algorithm>
- <vector>
- <cmath>
Answer: <numeric>. accumulate lives in <numeric>, not <algorithm> — a common include mistake.
What should a comparator lambda passed to std::sort return?
- The larger of the two elements
- An int: -1, 0, or 1
- The sorted range
- A bool answering 'should a come before b?'
Answer: A bool answering 'should a come before b?'. A comparator returns a bool — true when a should sort before b. Use a < b for ascending, a > b for descending.
To sort a vector in DESCENDING order, the comparator should return:
- a < b
- a > b
- a <= b
- a == b
Answer: a > b. Returning a > b means bigger elements come first, giving descending order.
What does std::find return?
- An iterator to the element, or end() if not found
- The matching value, or 0 if not found
- The index of the element
- A bool
Answer: An iterator to the element, or end() if not found. find returns an iterator; compare it to end() to detect a miss.
What is the result of accumulate(v.begin(), v.end(), 0) for v = {1299, 2499, 599, 3999, 899}?
- 5
- 3999
- 9295
- 0
Answer: 9295. accumulate folds the range starting from the seed 0: 1299+2499+599+3999+899 = 9295.
Why does the seed type in accumulate matter, e.g. 0 vs 0.0?
- It controls the iteration order
- It fixes the result type — 0 sums as int and truncates decimals, 0.0 sums as double
- 0.0 is invalid for accumulate
- It changes which header is needed
Answer: It fixes the result type — 0 sums as int and truncates decimals, 0.0 sums as double. The seed sets the accumulation type; seeding with int 0 truncates decimals, while 0.0 keeps them.
What does std::transform do?
- Sorts a range in place
- Removes elements that match a predicate
- Counts matching elements
- Applies a function to every element and writes results into another range
Answer: Applies a function to every element and writes results into another range. transform maps each input element through a function into an output range.
What do min_element and max_element return?
- The smallest/largest value directly
- An iterator to the smallest/largest element — dereference with * to read the value
- An index
- A sorted copy of the range
Answer: An iterator to the smallest/largest element — dereference with * to read the value. They return iterators; you write *max_element(v.begin(), v.end()) to get the value.
Why must remove_if be paired with the container's erase() (the erase-remove idiom)?
- erase is faster than remove_if
- remove_if deletes the wrong elements without erase
- remove_if only shifts kept elements forward and returns the new logical end; it can't resize the container
- erase sorts the range first
Answer: remove_if only shifts kept elements forward and returns the new logical end; it can't resize the container. Algorithms see only iterators, so remove_if can't shrink the container — erase() does the actual shrinking.
Continue this course
- Previous: Templates Deep Dive: Type Deduction, Variadic Templates & Fold Expressions
- Next: Advanced Containers: vector, list, map, unordered_map, deque Internals — Understand the time/space complexity and internals of each STL container
- Quick reference: C++ cheat sheet › Algorithms
Frequently asked questions
Do I always need to include <algorithm> and <numeric>?
Yes. sort, find, count_if, transform, for_each, remove_if, min_element and max_element all live in <algorithm>. accumulate lives in <numeric>. If you forget the include you'll get an 'X was not declared in this scope' error.
Why does erasing after remove_if look so awkward?
remove_if only shuffles the kept elements to the front and returns an iterator to the new logical end — it cannot resize the container because algorithms only see iterators, not the container itself. You pass that returned iterator to the container's own erase() to actually shrink it. This two-step combo is the 'erase-remove idiom'.
What does a comparator lambda return?
A bool. It answers one question: 'should a come before b?' Return true when a should sort earlier. For ascending order return a < b; for descending return a > b. It must NOT return true for equal values, or you break sort's rules.
What's the difference between find and find_if?
find looks for a specific value (find(v.begin(), v.end(), 42)). find_if looks for the first element that makes a predicate return true (find_if(v.begin(), v.end(), [](int n){ return n > 50; })). Both return end() when nothing matches.
Why does accumulate start with a value like 0?
That third argument is the starting total (the 'seed') and it also fixes the result type. accumulate(v.begin(), v.end(), 0) sums into an int; accumulate(v.begin(), v.end(), 0.0) sums into a double. Seeding with the wrong type silently truncates your decimals.