C++标准库中的std::sort算法(及其表亲std::partial_sort和std::nth_element)在大多数实现中是更基本的排序算法的复杂和混合合并,例如选择排序、插入排序、快速排序、合并排序或堆排序。
std::sort
std::partial_sort
std::nth_element
这里和姊妹网站(例如https://codereview.stackexchange.com/ )上存在许多与这些经典排序算法实现的错误、复杂性和其他方面相关的问题。大多数提供的实现由原始循环、使用索引操作和具体类型组成,并且通常在正确性和效率方面进行分析并非易事。
问题 :如何使用现代 C++ 实现上述经典排序算法?
<algorithm>
auto
备注 :
for
f(g(x));
f(x); g(x);
f(x) + g(x);
selection_sort
insertion_sort
我们首先从标准库中组装算法构建块:
#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()
boost::begin()
boost::end()
boost::next()
std::is_sorted
std::adjacent_find
boost::algorithm::is_sorted
std::is_heap
C14 提供了 transparent comparators,std::less<>它们以多态方式作用于它们的参数。这避免了必须提供迭代器的类型。这可以与 C11 的默认函数模板参数 结合使用,为作为比较的排序算法和具有用户定义的比较函数对象的排序算法创建 一个重载。<
std::less<>
<
template<class It, class Compare = std::less<>> void xxx_sort(It first, It last, Compare cmp = Compare{});
在 C++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{});
在 C++98 中,需要编写两个重载并使用详细typename xxx<yyy>::type语法
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>()); }
value_type_t
std::bind1st
std::bind2nd``std::not1
boost::bind
_1
_2
std::find_if_not
std::find_if
std::not1
目前还没有普遍接受的 C14 风格。无论好坏,我都密切关注 Scott Meyers 的 **草稿 Effective Modern C** 和 Herb Sutter 修改后的 GotW 。我使用以下样式建议:
()
{}
typedef
for (auto it = first; it != last; ++it)
while (first != last)
++first
选择排序 不会以任何方式适应数据,所以它的运行时间总是O(N虏). 但是,选择排序具有 最小化交换次数的 特性。在交换项目的成本很高的应用程序中,选择排序非常可能是首选算法。
O(N虏)
要使用标准库实现它,请重复使用std::min_element以查找剩余的最小元素,并将iter_swap其交换到位:
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
[first, it)
细节省略 :
if (std::distance(first, last) <= 1) return;
if (first == last || std::next(first) == last) return;
[first, std::prev(last))
尽管它是具有O(N虏)最坏情况时间的基本排序算法之一,但 插入排序 是当数据接近排序(因为它是 自适应 的)或当问题规模很小(因为它具有低开销)时的首选算法。由于这些原因,并且因为它也是 稳定 的,插入排序经常被用作递归基本情况(当问题规模较小时),用于更高开销的分治排序算法,例如合并排序或快速排序。
insertion_sort用标准库来实现,重复使用std::upper_bound找到当前元素需要去的位置,使用std::rotate将剩余元素在输入范围内向上移动:
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)排序为其循环不变量。插入排序也适用于前向迭代器。
[std::next(first), last)
以下片段的四个 实时示例 ( C++14 、 C++11 、 C++98 和 Boost 、 C++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)
O(N log N)
如果仔细实施, 快速排序 是健壮的并且具有O(N log N)预期的复杂性,但O(N虏)最坏情况下的复杂性可以通过对抗性选择的输入数据触发。当不需要稳定排序时,快速排序是一种出色的通用排序。
即使对于最简单的版本,使用标准库实现快速排序也比其他经典排序算法要复杂得多。下面的方法使用一些迭代器实用程序将输入范围的 中间元素[first, last)定位为枢轴,然后使用两次调用std::partition(which are O(N)) 将输入范围三路划分为小于、等于、和大于选定的枢轴,分别。最后,对元素小于和大于枢轴的两个外部段进行递归排序:
[first, last)
std::partition
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)复杂性,枢轴必须导致输入数据的平衡分区,这通常不能保证O(1)枢轴,但如果将枢轴设置为O(N)输入范围的中值,则可以保证。
O(1)
O(N^2)
1, 2, 3, ..., N/2, ... 3, 2, 1
std::nth_element(first, middle, last)
quick_sort(first, middle, cmp)
quick_sort(middle, last, cmp)
如果不考虑使用O(N)额外的空间,那么 归并排序 是一个很好的选择:它是唯一 稳定 O(N log N)的排序算法。
使用标准算法很容易实现:使用一些迭代器实用程序来定位输入范围的中间[first, last)并将两个递归排序的段与 a 组合std::inplace_merge:
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(log N)
std::list<T>::sort
堆排序 很容易实现,执行O(N log N)就地排序,但不稳定。
第一个循环,O(N)“heapify”阶段,将数组放入堆顺序。第二个循环,O(N log N“排序”阶段,反复提取最大值并恢复堆顺序。标准库使这变得非常简单:
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_heapand是“作弊” std::sort_heap,您可以更深入一层,分别根据 and 编写这些std::push_heap函数std::pop_heap:
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并不重要。
push_heap
pop_heap
make_heap
heap_sort
这里有四个 实时示例 ( C++14 、 C++11 、 C++98 和 Boost 、 C++98 )在各种输入上测试所有五种算法(并不意味着详尽或严格)。请注意 LOC 的巨大差异:C11/C14 需要大约 130 LOC,C98 和 Boost 190 (+50%) 和 C98 超过 270 (+100%)。