snippetcppCritical
How to implement classic sorting algorithms in modern C++?
Viewed 0 times
modernhowalgorithmssortingclassicimplement
Problem
The
There are many questions here and on sister sites such as https://codereview.stackexchange.com/ related to bugs, complexity and other aspects of implementations of these classic sorting algorithms. Most of the offered implementations consist of raw loops, use index manipulation and concrete types, and are generally non-trivial to analyse in terms of correctness and efficiency.
Question: how can the above mentioned classic sorting algorithms be implemented using modern C++?
Notes:
std::sort algorithm (and its cousins std::partial_sort and std::nth_element) from the C++ Standard Library is in most implementations a complicated and hybrid amalgamation of more elementary sorting algorithms, such as selection sort, insertion sort, quick sort, merge sort, or heap sort.There are many questions here and on sister sites such as https://codereview.stackexchange.com/ related to bugs, complexity and other aspects of implementations of these classic sorting algorithms. Most of the offered implementations consist of raw loops, use index manipulation and concrete types, and are generally non-trivial to analyse in terms of correctness and efficiency.
Question: how can the above mentioned classic sorting algorithms be implemented using modern C++?
- no raw loops, but combining the Standard Library's algorithmic building blocks from `
- iterator interface and use of templates instead of index manipulation and concrete types
- C++14 style, including the full Standard Library, as well as syntactic noise reducers such as auto
, template aliases, transparent comparators and polymorphic lambdas.
Notes:
- for further references on implementations of sorting algorithms see Wikipedia, Rosetta Code or http://www.sorting-algorithms.com/
- according to Sean Parent's conventions (slide 39), a raw loop is a for
-loop longer than composition of two functions with an operator. Sof(g(x));orf(x); g(x);orf(x) + g(x);are not raw loops, and neither are the loops inselection_sortandinsertion_sort` below.
- I follow Scott Meyers's terminology to denote the current C++1y already as C++14, and to denote C++98 and C++03 both as C++98, so don't flame me for that.
- As suggested in the comments by @Mehrdad, I provide four implementations as a Live Example at the end of the answer: C++14, C++11, C++98 and Boost and C++98.
- The answer itself is presented in terms of C++14 only. Where relevant, I denote the syntactic and library differences w
Solution
Algorithmic building blocks
We begin by assembling the algorithmic building blocks from the Standard Library:
Syntactical goodies
C++14 provides transparent comparators of the form
C++ Style
There is no generally acceptable C++14 style yet. For better or for worse, I closely follow Scott Meyers's draft Effective Modern C++ and Herb Sutter's revamped GotW. I use the following style recommendations:
Selection sort
Selection sort does not adapt to the data in any way, so its runtime is always
To implement it using the Standard Library, repeatedly use
Note that
Details omitted:
Insertion sort
Although it is one of the elementary sorting algorithms with O(N²)` worst-case time, insertion sort is the algorithm of choice either when the data is nearly sorted (because it is adaptive) or when the problem size is small (becau
We begin by assembling the algorithmic building blocks from the Standard Library:
#include // min_element, iter_swap,
// upper_bound, rotate,
// partition,
// inplace_merge,
// make_heap, sort_heap, push_heap, pop_heap,
// is_heap, is_sorted
#include // assert
#include // less
#include // distance, begin, end, next- the iterator tools such as non-member
std::begin()/std::end()as well as withstd::next()are only available as of C++11 and beyond. For C++98, one needs to write these himself. There are substitutes from Boost.Range inboost::begin()/boost::end(), and from Boost.Utility inboost::next().
- the
std::is_sortedalgorithm is only available for C++11 and beyond. For C++98, this can be implemented in terms ofstd::adjacent_findand a hand-written function object. Boost.Algorithm also provides aboost::algorithm::is_sortedas a substitute.
- the
std::is_heapalgorithm is only available for C++11 and beyond.
Syntactical goodies
C++14 provides transparent comparators of the form
std::less<> that act polymorphically on their arguments. This avoids having to provide an iterator's type. This can be used in combination with C++11's default function template arguments to create a single overload for sorting algorithms that take ::type syntaxtemplate
void xxx_sort(It first, It last, Compare cmp); // general implementation
template
void xxx_sort(It first, It last)
{
xxx_sort(first, last, std::less::value_type>());
}- Another syntactical nicety is that C++14 facilitates wrapping user-defined comparators through polymorphic lambdas (with
autoparameters that are deduced like function template arguments).
- C++11 only has monomorphic lambdas, that require the use of the above template alias
value_type_t.
- In C++98, one either needs to write a standalone function object or resort to the verbose
std::bind1st/std::bind2nd/std::not1type of syntax.
- Boost.Bind improves this with
boost::bindand_1/_2placeholder syntax.
- C++11 and beyond also have
std::find_if_not, whereas C++98 needsstd::find_ifwith astd::not1around a function object.
C++ Style
There is no generally acceptable C++14 style yet. For better or for worse, I closely follow Scott Meyers's draft Effective Modern C++ and Herb Sutter's revamped GotW. I use the following style recommendations:
- Herb Sutter's "Almost Always Auto" and Scott Meyers's "Prefer auto to specific type declarations" recommendation, for which the brevity is unsurpassed, although its clarity is sometimes disputed.
- Scott Meyers's "Distinguish
()and{}when creating objects" and consistently choose braced-initialization{}instead of the good old parenthesized initialization()(in order to side-step all most-vexing-parse issues in generic code).
- Scott Meyers's "Prefer alias declarations to typedefs". For templates this is a must anyway, and using it everywhere instead of
typedefsaves time and adds consistency.
- I use a
for (auto it = first; it != last; ++it)pattern in some places, in order to allow for loop invariant checking for already sorted sub-ranges. In production code, the use ofwhile (first != last)and a++firstsomewhere inside the loop might be slightly better.
Selection sort
Selection sort does not adapt to the data in any way, so its runtime is always
O(N²). However, selection sort has the property of minimizing the number of swaps. In applications where the cost of swapping items is high, selection sort very well may be the algorithm of choice.To implement it using the Standard Library, repeatedly use
std::min_element to find the remaining minimum element, and iter_swap to swap it into place:template>
void selection_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
for (auto it = first; it != last; ++it) {
auto const selection = std::min_element(it, last, cmp);
std::iter_swap(selection, it);
assert(std::is_sorted(first, std::next(it), cmp));
}
}Note that
selection_sort has the already processed range [first, it) sorted as its loop invariant. The minimal requirements are forward iterators, compared to std::sort's random access iterators.Details omitted:
- selection sort can be optimized with an early test
if (std::distance(first, last)
- for bidirectional iterators, the above test can be combined with a loop over the interval [first, std::prev(last))
, because the last element is guaranteed to be the minimal remaining element and doesn't require a swap.
Insertion sort
Although it is one of the elementary sorting algorithms with O(N²)` worst-case time, insertion sort is the algorithm of choice either when the data is nearly sorted (because it is adaptive) or when the problem size is small (becau
Code Snippets
#include <algorithm> // min_element, iter_swap,
// upper_bound, rotate,
// partition,
// inplace_merge,
// make_heap, sort_heap, push_heap, pop_heap,
// is_heap, is_sorted
#include <cassert> // assert
#include <functional> // less
#include <iterator> // distance, begin, end, nexttemplate<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});template<class It>
using value_type_t = typename std::iterator_traits<It>::value_type;
template<class It, class Compare = std::less<value_type_t<It>>>
void xxx_sort(It first, It last, Compare cmp = Compare{});template<class It, class Compare>
void xxx_sort(It first, It last, Compare cmp); // general implementation
template<class It>
void xxx_sort(It first, It last)
{
xxx_sort(first, last, std::less<typename std::iterator_traits<It>::value_type>());
}template<class FwdIt, class Compare = std::less<>>
void selection_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
for (auto it = first; it != last; ++it) {
auto const selection = std::min_element(it, last, cmp);
std::iter_swap(selection, it);
assert(std::is_sorted(first, std::next(it), cmp));
}
}Context
Stack Overflow Q#24650626, score: 410
Revisions (0)
No revisions yet.