排序算法 | 青训营笔记

95 阅读4分钟

排序算法

插入排序

插入排序的基本思想是将一个待排序的元素逐个插入到已经排序好的序列中的适当位置,从而逐步形成有序序列。该算法的原理如下:

  • 从第二个元素开始,将其与前面的元素进行比较。
  • 如果当前元素比前面的元素小,则将当前元素插入到前面的位置,然后再与前面的元素比较,直到找到合适的位置。
  • 重复上述步骤,直到所有元素都被插入到适当的位置。 插入排序的时间复杂度为O(n^2),其中n是待排序序列的长度。

快速排序

快速排序是一种分治的排序算法,通过递归地将待排序序列分割成较小的子序列,然后对这些子序列进行排序,最终得到完整有序的序列。该算法的原理如下:

  • 选择一个元素作为基准(通常是待排序序列的第一个元素)。
  • 将序列中的其他元素按照与基准的大小关系划分为两个子序列,一个小于基准的子序列和一个大于基准的子序列。
  • 对两个子序列递归地应用快速排序算法。
  • 将排序好的子序列合并起来,得到最终的有序序列。 快速排序的时间复杂度为O(nlogn),其中n是待排序序列的长度。

堆排序

堆排序(Heap Sort): 堆排序利用堆这种数据结构进行排序,它使用最大堆或最小堆来进行排序操作。该算法的原理如下:

  • 将待排序序列构建成一个最大堆(或最小堆)。
  • 将堆顶元素(最大值或最小值)与堆的最后一个元素交换,然后将堆的大小减1。
  • 对交换后的堆进行调整,使其重新满足堆的性质。
  • 重复上述步骤,直到堆的大小为1,即所有元素都已经有序。 堆排序的时间复杂度为O(nlogn),其中n是待排序序列的长度。

算法思路小结:

对于大部分场景,快速排序是平均最优的情况,但是如果数据量过大,或者数据已经基本有序,这个时候快速排序的性能将大幅跳水,这个时候堆排序可以保证时间复杂度仍为Onlogn,通常可以使用优先队列来构造堆,一般分为大根堆和小根堆。

在数量比较少的情况下,插入排序则是更简单的算法。

pdqsort(pattern-defeating-quicksort)

一种不稳定的混合排序算法,不同版本被应用在C++BOOST、Rust以及Go1.19中。它对常见的序列类型做了特殊的优化,使得在不同条件下都拥有不错的性能。

version 1

结合三种排序的有点:

  • 对于短序列(小于一定长度),使用插入排序:短序列的具体长度一般是12-32,泛型版本中选择24
  • 其他情况,使用快速排序保证整体性能
  • 当快速排序表现不佳时,使用堆排序来保证最坏情况下时间复杂度仍为Onlogn:当最终的pivot(首次选择头元素)的位置离序列两端很接近时,即距离小于len/8时,判定其表现不佳,这种情况的次数到达limit即bits.Len(length)时,切换为堆排序

改进:使得快排的pivot为序列的中位数,同时要使得Partition速度更快

version 2

提供更好的pivot选举策略

针对有序、逆序的情况,根据采样的序列状态翻转或插入排序

final version

如何优化重复元素很多的情况?

首先,采样的数量有限,可能没有采到相同的元素

解决方案:如果两次partition生成的pivot相同,即partition进行了无效分割

此时认为pivot的值为重复元素,相比上一种方法有更高的采样率

当检测到此时的pivot和上次相同时,使用partitioneuqal将重复元素排列在一起,减少重复元素对于pivot选择的干扰

党pivot选择策略表现不佳时,随机交换元素,避免极端情况和黑客攻击

image-20230528212210905