在大多数实现中,C ++标准库中的std::sort算法(及其表亲std::partial_sort和 std::nth_element)是 更多基本排序 算法(例如选择 排序,插入排序,快速排序,合并排序或堆排序)的复杂混合混合。
在这里以及诸如 https://codereview.stackexchange.com/之类的姐妹网站上,存在许多与 这些经典排序算法的错误,复杂性以及实现的其他方面有关的问题。 提供的大多数实现都是由原始循环,使用索引操作和 具体类型组成的,并且从 正确性和效率方面来说,通常都是很重要的。
问题:如何 使用现代C ++实现上述经典排序算法?
<algorithm>
注意事项:
我们首先从标准 库中组装算法构建块:
#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
boost::algorithm::is_sorted
语法优势C图14提供了透明的比较器,其形式std::less<>对它们的自变量具有多态作用。这样避免了必须提供迭代器的类型。可以与C结合使用11的默认函数模板参数,以创建单个重载以对<作为比较的排序算法和具有用户定义的比较函数对象的算法进行排序。
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>()); }
alias value_type_t
C ++ 98
std::bind1st/ std::bind2nd/ std::not1
Boost.Bind
boost::bind
_1/ _2
std::find_if_not
std::find_if
std::not1
目前尚无普遍接受的C 14样式。不管是好是坏,我都 密切关注Scott Meyers的草案有效的现代 C 和Herb Sutter 修改后的GotW。我使用 以下样式建议:
赫伯·萨特(Herb Sutter)的“几乎总是自动的”和斯科特·迈耶斯(Scott Meyers)的“首选自动使用特定类型的声明”的建议,尽管有时它的清晰度有时会引起争议,但其简洁性并没有超越。 斯科特·迈耶斯(Scott Meyers)的“区分()和{}创建对象时”,始终选择支撑初始化{}而不是旧的带括号的初始化()(以避开通用代码中所有最烦人的解析问题)。 斯科特·迈耶斯(Scott Meyers)的“首选别名声明而不是typedefs”。对于模板而言,无论如何都是必须的,并且在各处使用它而不是typedef节省时间和增加一致性。 我for (auto it = first; it != last; it)在某些地方使用了模式,以允许对已经排序的子范围进行循环不变检查。在生产代码中,在循环内使用while (first != last)和first可能会更好。
选择排序
选择排序不会 以任何方式适应数据,因此其运行时间始终为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器。
详细信息省略:
选择排序可以通过早期测试进行优化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来在输入范围内向上移动其余元素:
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),因为可以保证第一个元素就位并且不需要旋转。 对于双向迭代器,可以使用标准库的算法将二进制搜索(找到插入点)替换为反向线性搜索std::find_if_not。 以下片段的四个实时示例(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 log N)预期的复杂性,但O(N²)最坏情况下的复杂性可以通过对抗性选择的输入数据来触发。当不需要稳定排序时,快速排序是一种出色的通用排序。
即使是最简单的版本,使用标准库实现的快速排序也比其他经典的排序算法要复杂得多。下面的方法使用一些迭代器实用程序将输入范围的中间元素定位[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)复杂性,枢轴必须导致输入数据的平衡分区,这通常不能保证用于O(1)枢轴,但是如果将枢轴设置为O(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标准资源库中
std::inplace_merge
O(log N)
std::list<T>::sort
堆排序
堆排序易于实现,执行O(N log N)就地排序,但不稳定。
第一个循环是O(N)“堆”阶段,将数组置于堆顺序。在第二循环中,O(NlogN)“sortdown”阶段,反复提取最大和还原堆顺序。标准库使这一过程变得非常 简单:
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)); }
In case you consider it “cheating” to use std::make_heap and std::sort_heap, you can go one level deeper and write those functions yourself in terms of std::push_heap and std::pop_heap, respectively:
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
[first, last)
O(N log N)
make_heap
O(N)
heap_sort
这是四个实时示例(C 14,C 11,C 98和Boost,C 98),它们在各种 输入上测试所有五种算法(并非详尽无遗或严格)。只需注意 LOC 的巨大差异即可:C
11 /摄氏度14需要大约130 LOC,C98和Boost 190(+ 50%)和C98个超过270个(+ 100%)。