使用数组和链表进行选择排序的区别

Difference between selection sort with array and linked list?

本文关键字:选择 排序 区别 行选 数组 链表      更新时间:2023-10-16

我想知道使用findmax的数组和findmin的链表进行选择排序背后的逻辑是什么。两者的最佳和最坏情况?

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);
        }
}

在长度为Nstd::arraystd::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);
}

在长度为Nstd::arraystd::list上,这都具有O(N log N)复杂性:递归深度为O(log N)(因为每次间隔都被减半),对std::inplace_merge的调用具有线性复杂性,这给出了O(N log N)的整体缩放。

然而,几乎任何严肃的排序算法竞争者都不会因为比较的数量而显著区分自己,而是因为访问和放置数据的相关开销。这样的优化只能用比通用版本更多的知识来完成。

随机访问容器可以受益于混合算法

使用混合算法可以更便宜地对具有随机访问迭代器的容器进行排序。来自标准库的std::sort()std::stable_sort函数提供了O(N log N)最坏情况复杂度的这种混合算法。通常,它们被实现为IntroSort,它将递归随机枢轴快速排序与堆排序和插入排序混合在一起,这取决于各种递归排序的子范围的大小。

基于节点的容器可以受益于成员函数sort()

基于比较的排序算法大量使用复制或交换迭代器指向的底层数据。对于常规容器,交换底层数据是最好的选择。对于基于节点的容器,如std::liststd::forward_list,您更喜欢splice:只重新排列节点指针,避免复制潜在的大量数据。然而,这需要了解迭代器之间的连接。

这就是std::liststd::forward_list都具有成员函数sort()的原因:它们具有相同的O(N log N)最坏情况复杂性,但利用了容器基于节点的特性。