Intuition The big picture (WHY do these exist?)
Before the STL, every programmer rewrote the same loops: "loop and sort", "loop and find", "loop and add up". These loops are error-prone (off-by-one, wrong comparison) and they hide intent — a reader must decode the loop to learn "oh, this sums a vector."
STL <algorithm> and <numeric> give you named, tested, generic building blocks that work on any container via iterators . You say what you want, not how to loop. This is declarative thinking — the 80/20 superpower: 7 functions cover the bulk of day-to-day data crunching.
Definition Half-open range
Almost every STL algorithm takes a pair of iterators (first, last) describing the half-open range ==[first, last)== — first is included, last is one past the end (excluded).
For a vector v: v.begin() is first, v.end() is last.
[first, last)?
Size = last - first with no +1 fudge. An empty range is naturally first == last.
"Not found" results return last — a single sentinel that always exists, even for empty containers.
Every algorithm below is just a smart loop over [first, last) . If you remember that, you can derive what each returns.
sort
sort(first, last) rearranges elements in [first, last) into non-decreasing order (using operator< by default). sort(first, last, cmp) uses a binary predicate cmp(a,b) that returns true if a should come before b.
Intuition HOW it works (so it isn't magic)
Internally it's introsort = quicksort + heapsort fallback + insertion sort for tiny ranges. Average time O ( n log n ) O(n\log n) O ( n log n ) . It needs random-access iterators (vector/array/deque) — that's why you can't std::sort a std::list (use list.sort() instead).
Worked example Sort descending
vector <int> v{ 3 , 1 , 4 , 1 , 5 };
sort (v. begin (), v. end (), []( int a , int b ){ return a > b; });
// v = {5,4,3,1,1}
Why this step? The lambda a > b returns true when a should precede b; "bigger first" ⇒ descending. The default < would give ascending.
find
find(first, last, value) returns an iterator to the first element equal to value, or ==last== if none is found. Linear search, O ( n ) O(n) O ( n ) .
Common mistake Steel-man: "I'll check
if (it == nullptr)"
It feels right because in C you used NULL for "not found". But find returns an iterator , not a pointer — the failure sentinel is last, not nullptr.
Fix: always compare to the same last you passed in:
auto it = find (v. begin (), v. end (), 7 );
if (it != v. end ()) { /* found, use *it */ }
find then use position
vector <int> v{ 10 , 20 , 30 };
auto it = find (v. begin (), v. end (), 20 );
size_t idx = it - v. begin (); // idx == 1
Why this step? Subtracting begin() from a found iterator gives the index (random-access only).
transform
transform(first, last, dest, f) applies unary f to each element of [first,last) and writes results starting at dest. Returns the iterator one past the last written element. A two-range form transform(first1, last1, first2, dest, g) applies binary g(a,b).
Intuition WHY return "one past last written"?
So you can chain writes — the return value is exactly where the next algorithm should continue. Consistent with the [first,last) philosophy.
Worked example Square each element in place
vector <int> v{ 1 , 2 , 3 , 4 };
transform (v. begin (), v. end (), v. begin (),
[]( int x ){ return x * x; });
// v = {1,4,9,16}
Why this step? dest == v.begin() means "overwrite the source." This is safe for transform because element i is read and written before element i+1.
accumulate
accumulate(first, last, init) returns init + e₀ + e₁ + … + eₙ₋₁. With a custom binary op: accumulate(first, last, init, op) returns op(...op(op(init,e₀),e₁)..., eₙ₋₁) — a left fold .
Common mistake Steel-man: "
accumulate(v.begin(), v.end(), 0) gives the average / works for doubles"
Feels right because it sums everything. But the result type is the type of init . Pass 0 (an int) over a vector<double> and every partial sum is truncated to int !
Fix: pass 0.0 (or 0LL for big sums):
double s = accumulate (v. begin (), v. end (), 0.0 );
Worked example Product instead of sum
vector <int> v{ 1 , 2 , 3 , 4 };
int p = accumulate (v. begin (), v. end (), 1 ,
[]( int a , int b ){ return a * b; });
// p == 24
Why this step? For a product the identity element is 1, not 0 — init must be the operation's identity or your answer is off.
copy
copy(first, last, dest) copies [first,last) into the range beginning at dest; returns the iterator one past the last written. The destination must already have room (or use a back_inserter).
Common mistake Steel-man: "copy into an empty vector"
vector <int> dst; // size 0!
copy (src. begin (), src. end (), dst. begin ()); // UB — no space
Feels right (we said dest is dst.begin()). But copy assigns , it does not grow . Fix: copy(src.begin(), src.end(), back_inserter(dst)); (an inserter iterator calls push_back).
all_of / any_of
all_of(first, last, pred) returns ==true== iff pred(e) is true for every element (vacuously true on an empty range).
any_of(first, last, pred) returns true iff pred(e) is true for at least one element (false on empty).
Both short-circuit .
Intuition WHY vacuous truth for
all_of on empty?
Logically ∀ x ∈ ∅ . P ( x ) \forall x\in\varnothing.\,P(x) ∀ x ∈ ∅ . P ( x ) is true (no counterexample exists). This matches &&-folding with start value true. Mirror image: any_of folds with || from false.
vector <int> v{ 2 , 4 , 6 };
bool allEven = all_of (v. begin (), v. end (), []( int x ){ return x % 2 == 0 ; }); // true
bool anyNeg = any_of (v. begin (), v. end (), []( int x ){ return x < 0 ; }); // false
Why this step? The predicate returns bool per element; all_of AND-folds, any_of OR-folds.
Recall Feynman: explain to a 12-year-old
Imagine a row of toy boxes. Instead of telling a robot "step from box 1 to box 5 and do stuff," you hand it a task card :
sort = "line them up smallest to biggest."
find = "point to the first red box; if there's none, point past the end."
transform = "paint each box and put the painted copy on a new shelf."
accumulate = "add up all the numbers on the boxes, starting from a number I give you."
copy = "make a duplicate row over here (make sure the shelf is big enough!)."
all_of = "are ALL boxes red?" any_of = "is ANY box red?"
You just say the goal; the robot handles the walking. Less to get wrong, easier to read.
"Some Friendly Tigers Are Calmly Asking Around"
S ort · F ind · T ransform · A ccumulate · C opy · A ll_of · A ny_of.
And for ranges: "begin to end, end is the friend you don't include" ([ [ [ first, last) ) ) ).
What range convention do nearly all STL algorithms use? The half-open range [first, last) — first included, last (one past end) excluded.
What does find return when the value is absent? The last iterator you passed in (NOT nullptr).
Why must accumulate's init match the desired result type? The accumulator type equals init's type; 0 (int) over doubles truncates each partial sum. Use 0.0.
What is the identity init for a product via accumulate? 1 (for sum it's 0).
Why can't you std::sort a std::list? sort needs random-access iterators; list has bidirectional only — use list.sort().
What is the worst-case time of std::sort? O ( n log n ) O(n\log n) O ( n log n ) — introsort guarantees it (quicksort + heapsort fallback).
What does all_of return on an empty range, and why? true — vacuous truth:
∀ x ∈ ∅ \forall x\in\varnothing ∀ x ∈ ∅ holds (no counterexample).
What does any_of return on an empty range? false — no element exists to satisfy the predicate.
How do you safely copy into an empty/auto-growing vector? Use a back_inserter(dst) as the destination iterator.
What does transform return? An iterator one past the last element written to the destination.
A lambda for sort returns true when…? …its first argument should come BEFORE its second (a strict-weak-ordering "less-than").
How do you get the index of a find result it in a vector? it - v.begin() (random-access iterators only).
Iterators and ranges — the abstraction every algorithm rides on.
Lambda expressions — how predicates/comparators are passed.
Function objects (functors) — alternative to lambdas for op/pred.
Time complexity Big-O — justifies the O ( n log n ) O(n\log n) O ( n log n ) and O ( n ) O(n) O ( n ) costs.
std::vector — the most common container these run on.
Ranges library (C++20) — the modern, composable successor.
needs random-access iterators
Iterator range first last half-open
Named generic tested loops
accumulate folds to one value
all_of every element true
Returns last as not-found sentinel
Intuition Hinglish mein samjho
Dekho, STL algorithms ka core idea ek hi hai: tum kya karna chahte ho woh batao, kaise loop chalana hai woh STL handle karega. Har algorithm ek range [first, last) pe kaam karta hai — yaha first included hai aur last ek-step-aage, yaani excluded. Isi wajah se size nikalna easy hota hai (last - first) aur "nahi mila" ka matlab seedha last return karna — koi +1 ka jhanjhat nahi.
Saat important functions: sort (elements ko order me lagao, default chhote-se-bade, ya lambda se descending), find (pehla matching element ka iterator do, nahi mile to end()), transform (har element pe ek function lagao, jaise har number ka square), accumulate (<numeric> me, saare elements ko ek value me fold karo, jaise sum ya product — yaad rakho init ka type result ka type hai, isliye doubles ke liye 0.0 likho!), copy (ek range ko dusri jagah copy karo, par destination me jagah honi chahiye — warna back_inserter use karo), aur all_of / any_of (sab elements condition satisfy karte hai? ya koi ek karta hai?).
Common galtiyan: find ka result nullptr se compare karna (galat — end() se karo), accumulate me 0 daal ke doubles truncate ho jana, aur khali vector me copy karna (UB). In tino ka fix note me diya hai.
Yeh sab matter kyun karta hai? Kyunki competitive coding aur real projects me 80% data-crunching inhi 7 se ho jata hai. Hand-written loops me bug aate hai, ye tested aur fast hai (sort guaranteed O ( n log n ) O(n\log n) O ( n log n ) ). Naam padhte hi reader ko intent samajh aata hai — code clean aur professional banta hai.