Ответ 1
Алгоритмические строительные блоки
Начнем с сборки алгоритмических строительных блоков из стандартной библиотеки:
#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, next
- инструменты итератора, такие как не-член
std::begin()
/std::end()
, а такжеstd::next()
доступны только с С++ 11 и выше. Для С++ 98 их нужно написать сами. Есть замены из Boost.Range вboost::begin()
/boost::end()
и от Boost.Utility вboost::next()
. - Алгоритм
std::is_sorted
доступен только для С++ 11 и более поздних версий. Для С++ 98 это может быть реализовано в терминахstd::adjacent_find
и рукописного объекта функции. Boost.Algorithm также предоставляетboost::algorithm::is_sorted
в качестве замены. - Алгоритм
std::is_heap
доступен только для С++ 11 и более поздних версий.
Синтаксические лакомства
С++ 14 предоставляет прозрачные компараторы формы std::less<>
, которые полиморфно действуют на свои аргументы. Это позволяет избежать ввода типа итератора. Это можно использовать в комбинации с С++ 11 аргументы шаблона функции по умолчанию, чтобы создать одиночную перегрузку для сортировки алгоритмы, которые принимают <
как сравнение, и те, которые имеют пользовательский объект функции сравнения.
template<class It, class Compare = std::less<>>
void xxx_sort(It first, It last, Compare cmp = Compare{});
В С++ 11 можно определить многоразовый псевдоним шаблона, чтобы извлечь тип значения итератора, который добавляет незначительный беспорядок в сортировку подписи алгоритмов:
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{});
В С++ 98 необходимо написать две перегрузки и использовать подробный синтаксис typename xxx<yyy>::type
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>());
}
- Другая синтаксическая тонкость заключается в том, что С++ 14 облегчает перенос пользовательских компараторов с помощью полиморфных lambdas (с параметрами
auto
, которые выводятся как аргументы шаблона функции). - С++ 11 имеет только мономорфные лямбды, для которых требуется использование вышеупомянутого псевдонима шаблона
value_type_t
. - В С++ 98 нужно либо написать отдельный объект функции, либо обратиться к подробному типу синтаксиса
std::bind1st
/std::bind2nd
/std::not1
. - Boost.Bind улучшает это с помощью синтаксиса
boost::bind
и_1
/_2
. - С++ 11 и далее также имеют
std::find_if_not
, тогда как С++ 98 нуждается вstd::find_if
сstd::not1
вокруг объекта функции.
Стиль С++
Пока нет общепринятого стиля С++ 14. К лучшему, к худшему, я внимательно слежу за Скоттом Мейерсом черновик Эффективный современный С++ и Herb Sutter обновленный GotW. Я использую следующие рекомендации стиля:
- Herb Sutter "Почти всегда Авто" и Скотт Мейерс "Предпочитает автоматическую декларацию определенного типа" , для которой краткость является непревзойденной, хотя ее ясность иногда оспаривал.
- Скотт Мейерс " Различать
()
и{}
при создании объектов и последовательно выбирать сочетания-инициализации{}
вместо старой старой инициализации в скобках()
(чтобы устранить все наиболее неприятные-разборные проблемы в общем коде). - Скотт Мейерс " Предпочитайте псевдонимы для typedefs. Для шаблонов это необходимо в любом случае, и использование его везде вместо
typedef
экономит время и добавляет согласованность. - Я использую шаблон
for (auto it = first; it != last; ++it)
в некоторых местах, чтобы разрешить проверку инвариантности цикла для уже отсортированных поддиапазонов. В производственном коде использованиеwhile (first != last)
и a++first
где-то внутри цикла может быть немного лучше.
Сортировка сортировки
Сортировка сортировки не адаптируется к данным каким-либо образом, поэтому ее время выполнения всегда O(N^2)
. Однако сортировка выбора имеет свойство , минимизирующее количество свопов. В приложениях, где стоимость подкачки элементов высока, выбор сортировки очень хорошо может быть алгоритмом выбора.
Чтобы реализовать его с использованием стандартной библиотеки, повторно используйте std::min_element
, чтобы найти оставшийся минимальный элемент, и iter_swap
, чтобы заменить его на место:
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));
}
}
Обратите внимание, что selection_sort
имеет уже обработанный диапазон [first, it)
, отсортированный как его инвариант цикла. Минимальные требования: форвардные итераторы, по сравнению с std::sort
итераторами произвольного доступа.
Детали опущены:
- Выбор сортировки может быть оптимизирован с помощью раннего теста
if (std::distance(first, last) <= 1) return;
(или для форвардных/двунаправленных итераторов:if (first == last || std::next(first) == last) return;
). - для двунаправленных итераторов, вышеуказанный тест можно комбинировать с циклом в интервале
[first, std::prev(last))
, поскольку последний элемент гарантированно является минимальным оставшимся элементом и не требует свопа.
Сортировка вставки
Хотя это один из элементарных алгоритмов сортировки с наихудшим временем O(N^2)
, вставка сортировки алгоритм выбора либо при почти сортировке данных (потому что он адаптивен), либо когда размер проблемы мал (поскольку он имеет низкие накладные расходы). По этим причинам, а также потому, что он также является стабильным, сортировка вставки часто используется как рекурсивный базовый случай (когда размер проблемы мал) для более сложных алгоритмов сортировки с разбивкой и победой, таких как слияние сортировать или быстро сортировать.
Чтобы реализовать insertion_sort
в стандартной библиотеке, повторно используйте std::upper_bound
, чтобы найти место, куда должен идти текущий элемент, и используйте std::rotate
для перемещения остальных элементов вверх в диапазоне ввода:
template<class FwdIt, class Compare = std::less<>>
void insertion_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
for (auto it = first; it != last; ++it) {
auto const insertion = std::upper_bound(first, it, *it, cmp);
std::rotate(insertion, it, std::next(it));
assert(std::is_sorted(first, std::next(it), cmp));
}
}
Обратите внимание, что insertion_sort
имеет уже обработанный диапазон [first, it)
, отсортированный как его инвариант цикла. Сортировка вставки также работает с итераторами вперед.
Детали опущены:
- сортировка вставки может быть оптимизирована с помощью раннего теста
if (std::distance(first, last) <= 1) return;
(или для форвардных/двунаправленных итераторов:if (first == last || std::next(first) == last) return;
) и цикла над интервалом[std::next(first), last)
, поскольку первый элемент гарантированно будет на месте и не будет " t требуется поворот. - для двунаправленных итераторов, бинарный поиск для поиска точки вставки можно заменить на обратный линейный поиск с использованием алгоритма стандартной библиотеки
std::find_if_not
.
Четыре Live Примеры ( С++ 14, С++ 11, С++ 98 и Boost, С++ 98) для фрагмента ниже:
using RevIt = std::reverse_iterator<BiDirIt>;
auto const insertion = std::find_if_not(RevIt(it), RevIt(first),
[=](auto const& elem){ return cmp(*it, elem); }
).base();
- Для случайных входов это дает сравнение
O(N^2)
, но это улучшает сравнениеO(N)
для почти отсортированных входов. В двоичном поиске всегда используются сравненияO(N log N)
. - Для небольших диапазонов ввода лучшая локальность памяти (кеш, предварительная выборка) линейного поиска также может доминировать в бинарном поиске (нужно, конечно, проверить это).
Быстрая сортировка
При тщательном применении быстрая сортировка является надежной и имеет O(N log N)
ожидаемую сложность, но с O(N^2)
наихудшая сложность, которая может быть инициирована с использованием смежных входных данных. Когда стабильный вид не нужен, быстрый сортировка - отличный вид общего назначения.
Даже для самых простых версий быстрый сорт довольно сложнее реализовать с использованием стандартной библиотеки, чем другие классические алгоритмы сортировки. Подход ниже использует несколько итератора утилиты, чтобы найти средний элемент диапазона входного [first, last)
в качестве оси поворота, а затем использовать два вызова std::partition
(которые являются O(N)
), чтобы трехходовой разделите диапазон ввода на сегменты элементов, которые меньше, равны и больше, чем выбранный опорный элемент, соответственно. Наконец, рекурсивно отсортированы два внешних сегмента с элементами меньшего размера и больше, чем точка поворота:
template<class FwdIt, class Compare = std::less<>>
void quick_sort(FwdIt first, FwdIt last, Compare cmp = Compare{})
{
auto const N = std::distance(first, last);
if (N <= 1) return;
auto const pivot = *std::next(first, N / 2);
auto const middle1 = std::partition(first, last, [=](auto const& elem){
return cmp(elem, pivot);
});
auto const middle2 = std::partition(middle1, last, [=](auto const& elem){
return !cmp(pivot, elem);
});
quick_sort(first, middle1, cmp); // assert(std::is_sorted(first, middle1, cmp));
quick_sort(middle2, last, cmp); // assert(std::is_sorted(middle2, last, cmp));
}
Однако быстрый сортировка довольно сложная, чтобы получить правильную и эффективную работу, так как каждый из вышеуказанных шагов должен быть тщательно проверен и оптимизирован для кода уровня производства. В частности, при сложности O(N log N)
стержень должен приводить к сбалансированному разделению входных данных, который вообще не может быть гарантирован для pivot O(1)
, но который может быть гарантирован, если установить ось в качестве O(N)
медиана входного диапазона.
Детали опущены:
- вышеупомянутая реализация особенно уязвима для специальных ресурсов, например. он имеет
O(N^2)
сложность для ввода < трубки органа "1, 2, 3, ..., N/2, ... 3, 2, 1
(поскольку середина всегда больше всех остальных элементов). - медианный из 3 выбор пика из случайно выбранных элементов из диапазона входных диапазонов против почти отсортированных входов, для которых сложность в противном случае ухудшилась бы до
O(N^2)
. - 3-полосное разбиение (разделение элементов меньше, чем равное и большее, чем точка поворота), как показано два вызова
std::partition
не являются наиболее эффективным алгоритмомO(N)
для достижения этого результата. - для итераторов с произвольным доступом, гарантированная
O(N log N)
сложность может быть достигнута с помощью медианного выбора поворота с помощьюstd::nth_element(first, middle, last)
, за которым следует рекурсивный вызовquick_sort(first, middle, cmp)
иquick_sort(middle, last, cmp)
. - эта гарантия стоит за счет, однако, поскольку постоянный коэффициент сложности
O(N)
std::nth_element
может быть более дорогим, чем сложностьO(1)
срединной оси 3, за которой следуетO(N)
вызовstd::partition
(который является независимым от кэширования одним прохождением вперед по данным).
Сортировка слияния
Если использование O(N)
дополнительного пространства не вызывает беспокойства, то merge sort - отличный выбор: it является единственным стабильным O(N log N)
алгоритмом сортировки.
Простая реализация с использованием стандартных алгоритмов: используйте несколько утилит итератора, чтобы найти середину диапазона ввода [first, last)
и объединить два рекурсивно отсортированных сегмента с помощью std::inplace_merge
:
template<class BiDirIt, class Compare = std::less<>>
void merge_sort(BiDirIt first, BiDirIt last, Compare cmp = Compare{})
{
auto const N = std::distance(first, last);
if (N <= 1) return;
auto const middle = std::next(first, N / 2);
merge_sort(first, middle, cmp); // assert(std::is_sorted(first, middle, cmp));
merge_sort(middle, last, cmp); // assert(std::is_sorted(middle, last, cmp));
std::inplace_merge(first, middle, last, cmp); // assert(std::is_sorted(first, last, cmp));
}
Для сортировки слияния требуются двунаправленные итераторы, узким местом которых является std::inplace_merge
. Обратите внимание, что при сортировке связанных списков сортировка слияния требует только O(log N)
дополнительного пространства (для рекурсии). Последний алгоритм реализован std::list<T>::sort
в стандартной библиотеке.
Сортировка кучи
сортировка кучи прост в реализации, выполняет O(N log N)
сортировку на месте, но не стабильна.
Первый цикл, O(N)
"heapify", помещает массив в порядок кучи. Второй цикл, этап O(N log N
) "сортировки", многократно извлекает максимум и восстанавливает порядок кучи. Стандартная библиотека делает это предельно простым:
template<class RandomIt, class Compare = std::less<>>
void heap_sort(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
lib::make_heap(first, last, cmp); // assert(std::is_heap(first, last, cmp));
lib::sort_heap(first, last, cmp); // assert(std::is_sorted(first, last, cmp));
}
Если вы считаете, что это "обман" для использования std::make_heap
и std::sort_heap
, вы можете пойти на один уровень глубже и самостоятельно записать эти функции в терминах std::push_heap
и std::pop_heap
соответственно:
namespace lib {
// NOTE: is O(N log N), not O(N) as std::make_heap
template<class RandomIt, class Compare = std::less<>>
void make_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
for (auto it = first; it != last;) {
std::push_heap(first, ++it, cmp);
assert(std::is_heap(first, it, cmp));
}
}
template<class RandomIt, class Compare = std::less<>>
void sort_heap(RandomIt first, RandomIt last, Compare cmp = Compare{})
{
for (auto it = last; it != first;) {
std::pop_heap(first, it--, cmp);
assert(std::is_heap(first, it, cmp));
}
}
} // namespace lib
Стандартная библиотека определяет как push_heap
, так и pop_heap
как сложность O(log N)
. Однако обратите внимание, что внешний цикл в диапазоне [first, last)
приводит к сложности O(N log N)
для make_heap
, тогда как std::make_heap
имеет только сложность O(N)
. Для общей сложности O(N log N)
heap_sort
это не имеет значения.
Детали опущены: O(N)
реализация make_heap
Тестирование
Вот четыре Live Примеры ( С++ 14, С++ 11, С++ 98 и Boost, С++ 98), проверяя все пять алгоритмов на различных входы (не должны быть исчерпывающими или строгими). Просто обратите внимание на огромные различия в LOC: С++ 11/С++ 14 требуется около 130 LOC, С++ 98 и Boost 190 (+ 50%) и С++ 98 более 270 (+100%).