TL;DR
选择排序通常很糟糕。合并排序通常很好,但可以通过 std::sort 来改进随机访问容器和成员函数 sort() 对于基于节点的容器。
选择排序是二次方的
考虑以下通用版本的selection_sort
template<class ForwardIt, class Compare = std::less<typename std::iterator_traits<ForwardIt>::value_type>>
void selection_sort(ForwardIt first, ForwardIt 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);
}
}
在长度为N 的std::array 和std::list 上,这具有O(N^2) 复杂性:外部循环处理所有N 元素,而对std::min_element 的内部调用也具有线性复杂性,即给出整体二次缩放.
但是,由于基于比较的排序可以像 O(N log N) 一样便宜地完成,因此对于大型 N,这通常是不可接受的缩放。正如@EJP 所提到的,选择排序的一个可取之处是,虽然它进行O(N^2) 比较,但它只进行O(N) 数据交换。然而,对于非常大的N,这种优于大多数O(N log N) 排序算法的优势最终会被O(N^2) 的比较成本所压倒。
通用归并排序来拯救?
考虑以下merge_sort 的通用版本
template<class BiDirIt, class Compare = std::less<typename std::iterator_traits<BiDirIt>::value_type>>
void merge_sort(BiDirIt first, BiDirIt last, Compare cmp = Compare())
{
auto const N = std::distance(first, last);
if (N < 2) return;
auto middle = first + N / 2;
merge_sort(first, middle, cmp);
merge_sort(middle, last, cmp);
std::inplace_merge(first, middle, last, cmp);
}
在长度为N 的std::array 和std::list 上,这具有O(N log N) 复杂性:递归深度为O(log N)(因为每次将间隔减半)和对@987654345 的调用@ 具有线性复杂性,这使得整体 O(N log N) 可缩放。
但是,几乎任何严肃的排序算法竞争者都不会通过比较次数来显着区分自己,而是通过访问和放置数据的相关开销来区分自己。这种优化只能通过比通用版本更多的知识来完成。
随机存取容器可以从混合算法中受益
使用混合算法可以更便宜地对具有随机访问迭代器的容器进行排序。标准库中的std::sort() 和std::stable_sort 函数提供了O(N log N) 最坏情况复杂度的混合算法。通常,它们以 IntroSort 的形式实现,它将递归随机枢轴快速排序与堆排序和插入排序混合在一起,具体取决于各种递归排序子范围的大小。
基于节点的容器可以受益于成员函数sort()
基于比较的排序算法大量使用复制或交换迭代器指向的基础数据。对于常规容器,交换底层数据是您能做的最好的事情。对于std::list 或std::forward_list 等基于节点的容器,您更喜欢splice:仅重新排列节点指针并避免复制潜在的大量数据。但是,这需要了解迭代器之间的连接。
这就是std::list 和std::forward_list 都具有成员函数 sort() 的原因:它们具有相同的O(N log N) 最坏情况复杂性,但利用节点-基于容器的字符。