C++ Sorting Algorithms
Sorting means arranging the elements of a collection — numbers, strings, or custom objects — into a defined order, typically ascending or descending. It is one of the most fundamental operations in computer science: searching, deduplication, scheduling, and many other algorithms become dramatically faster (or only become possible) once data is sorted. C++ ships a highly optimized std::sort in the Standard Library, so you rarely need to hand-write a sorting routine, but understanding how sorting actually works — comparators, stability, and complexity — is essential for writing correct, efficient C++ programs.
Overview / How Sorting Works
Almost every general-purpose sort in C++ is a comparison-based sort: the algorithm repeatedly asks "is element A less than element B?" and rearranges elements based on the answers. This means any type you want to sort just needs a way to compare two elements — either the built-in < operator or a comparator function you supply. It is a mathematical fact that any comparison-based sort needs at least O(n log n) comparisons in the worst case to sort n elements — you cannot do better using only pairwise comparisons.
The C++ Standard Library’s std::sort (declared in <algorithm>) is not a single fixed algorithm. Most implementations use introsort: it starts with quicksort (fast on average, in-place, cache-friendly), switches to heapsort if the recursion goes too deep (protecting against quicksort’s O(n²) worst case), and switches to insertion sort for very small sub-ranges (where its low constant overhead beats quicksort’s overhead). This hybrid strategy gives std::sort a guaranteed O(n log n) worst case while staying fast in practice. std::sort is not stable: elements that compare equal may be reordered relative to each other. If you need equal elements to keep their original relative order, use std::stable_sort, which is typically implemented with merge sort (O(n log n), but may use extra memory).
Under the hood, sorting a std::vector works directly on the underlying contiguous array using iterators (really pointers) — elements are swapped or moved in place, so no new container is allocated by std::sort itself. Because C++ move semantics apply, sorting large objects (like strings) is efficient: the algorithm moves data rather than deep-copying it wherever possible.
Syntax
#include <algorithm>
std::sort(first, last);
std::sort(first, last, comparator);
std::stable_sort(first, last, comparator);
| Part | Meaning |
|---|---|
first |
Iterator to the first element of the range to sort (e.g. v.begin()). |
last |
Iterator one-past-the-last element (e.g. v.end()). The range is [first, last). |
comparator |
Optional callable taking two elements and returning true if the first should come before the second. Defaults to operator<. |
A comparator must implement a strict weak ordering: it must be false when comparing an element to itself, and it must be consistent (never say A < B and B < A at the same time). Violating this causes undefined behavior, discussed under Common Mistakes.
Examples
Example 1: Sorting a vector of integers
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> nums = {5, 2, 9, 1, 5, 6};
sort(nums.begin(), nums.end());
for (int n : nums) cout << n << " ";
cout << endl;
return 0;
}
Output:
1 2 5 5 6 9
By default std::sort uses operator<, so calling it with just a begin and end iterator sorts in ascending order. This is the form you will use most often.
Example 2: Descending order with a lambda comparator
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> nums = {5, 2, 9, 1, 5, 6};
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
for (int n : nums) cout << n << " ";
cout << endl;
return 0;
}
Output:
9 6 5 5 2 1
Passing a lambda that returns a > b flips the ordering to descending. You could equally use std::greater<int>() from <functional> instead of writing your own lambda.
Example 3: Sorting objects by multiple keys
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
struct Student {
string name;
int score;
};
int main() {
vector<Student> students = {
{"Alice", 85},
{"Bob", 92},
{"Carol", 85},
{"Dave", 78}
};
sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
if (a.score != b.score) return a.score > b.score;
return a.name < b.name;
});
for (const auto& s : students) {
cout << s.name << ": " << s.score << endl;
}
return 0;
}
Output:
Bob: 92
Alice: 85
Carol: 85
Dave: 78
Here the comparator sorts primarily by score (descending); when two students tie on score, it falls back to comparing names alphabetically. This "compare by primary key, then tiebreak by secondary key" pattern is extremely common when sorting real-world records.
Under the Hood: A Manual Bubble Sort
To understand what a sorting algorithm actually does to memory, it helps to implement one by hand. Bubble sort repeatedly walks through the array, comparing each pair of adjacent elements and swapping them if they are out of order. Larger values "bubble" toward the end with each pass.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> arr = {64, 25, 12, 22, 11};
int n = arr.size();
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
for (int x : arr) cout << x << " ";
cout << endl;
return 0;
}
Output:
11 12 22 25 64
Step by step: the outer loop runs n - 1 passes. On each pass, the inner loop scans left to right, swapping any adjacent pair that is out of order. After the first pass, the largest element is guaranteed to be at the end, so the next pass can safely ignore it (that is what n - i - 1 achieves). This makes bubble sort O(n²) in the worst and average case — fine for teaching or tiny arrays, but far too slow for real workloads, which is exactly why the Standard Library gives you std::sort instead.
Common Sorting Algorithms Compared
| Algorithm | Average Time | Worst Time | Space | Stable? |
|---|---|---|---|---|
| Bubble Sort | O(n²) | O(n²) | O(1) | Yes |
| Selection Sort | O(n²) | O(n²) | O(1) | No |
| Insertion Sort | O(n²) | O(n²) | O(1) | Yes |
| Merge Sort | O(n log n) | O(n log n) | O(n) | Yes |
| Quicksort | O(n log n) | O(n²) | O(log n) | No |
std::sort (introsort) |
O(n log n) | O(n log n) | O(log n) | No |
std::stable_sort |
O(n log n) | O(n log n) | O(n) | Yes |
Common Mistakes
Mistake 1: Forgetting to include <algorithm>
Some standard library implementations happen to pull in <algorithm> transitively through <vector> or other headers, so this mistake can appear to work and then break the moment you switch compilers or standard library versions.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = {3, 1, 2};
sort(v.begin(), v.end());
for (int x : v) cout << x << " ";
return 0;
}
Fix: always explicitly include <algorithm> when you use std::sort, regardless of what other headers you already have.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {3, 1, 2};
sort(v.begin(), v.end());
for (int x : v) cout << x << " ";
cout << endl;
return 0;
}
Output:
1 2 3
Mistake 2: A comparator that is not a strict weak ordering
It is tempting to write >= when you want descending order, but a correct comparator must return false when comparing an element to itself. Using >= breaks this rule and causes undefined behavior — possibly a crash, an infinite loop, or silently wrong output, depending on the implementation.
vector<int> v = {3, 1, 2};
sort(v.begin(), v.end(), [](int a, int b) {
return a >= b; // WRONG: violates strict weak ordering, undefined behavior
});
Fix: use a strict comparison (> for descending, < for ascending), never >= or <=.
vector<int> v = {3, 1, 2};
sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
for (int x : v) cout << x << " ";
Output:
3 2 1
Best Practices
- Prefer
std::sortover hand-written algorithms; it is heavily optimized and well tested. - Use
std::stable_sortwhenever the relative order of equal elements matters (e.g. sorting already-sorted data by a secondary key). - Write comparators using strict operators (
<or>), never<=or>=. - For custom types, either overload
operator<for the "natural" ordering or pass a lambda/comparator for one-off orderings — don’t force one canonical order if the type is sorted differently in different places. - If you only need the k smallest/largest elements, use
std::partial_sortorstd::nth_elementinstead of sorting the whole range — both are faster when k is much smaller than n. - Remember
std::sortworks on random-access iterators (vectors, arrays, deques); forstd::list, use its member functionlist::sort()instead.
Practice Exercises
- Implement selection sort by hand on a
vector<int>: on each pass, find the minimum of the unsorted portion and swap it into place. Print the sorted array. - Given a
vector<pair<string, int>>of (name, age) pairs, usestd::sortwith a lambda to sort by age in descending order. - Given a
vector<string>of words, sort them first by length (shortest first), and for words of equal length, sort alphabetically.
Summary
- Sorting arranges data into order and is bounded by O(n log n) comparisons for comparison-based algorithms.
std::sort(from<algorithm>) is a fast, in-place, but unstable O(n log n) sort using an introsort hybrid strategy.std::stable_sortpreserves the relative order of equal elements, at the cost of extra memory.- Comparators must implement a strict weak ordering — using
<=/>=causes undefined behavior. - Simple O(n²) algorithms like bubble sort are valuable for learning how sorting works, but should not be used in production code.
- Use
partial_sortornth_elementwhen you only need part of the sorted result.
