排序算法详解

小知识

如何判断一个排序算法是否稳定

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;

速记

  • 不稳定算法快选堆希

排序方式

  • In-place(原地算法):指算法执行过程中不需要额外的辅助空间,而是在原始数据上进行操作。
  • Out-of-place(非原地算法):指算法执行过程中需要额外的辅助空间来存储数据,而不会在原始数据上进行直接操作。
所以非原地算法通常具有较高的空间复杂度

算法策略

  • 分治法:快速排序,归并排序

算法详解

  冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

基本步骤:

  1. 比较相邻的两个元素,如果前一个比后一个大(升序排序)或者小(降序排序),则交换它们的位置。

  2. 对每一对相邻元素重复步骤1,直到列表末尾。这样一次遍历完成后,最大(或最小)的元素将移动到列表末尾。

  3. 针对剩下的未排序元素重复以上步骤,直到列表中的所有元素都排好序。

  选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间

基本步骤:

  1. 遍历数组:从未排序部分找到最小(或最大)的元素。
  2. 交换位置:将找到的最小(或最大)元素与未排序部分的第一个元素交换位置。
  3. 缩小范围:将未排序部分的范围缩小一个元素,即将排序范围向右移动一个位置。
  4. 重复步骤1至步骤3,直到所有元素都被排序完成。

  插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

基本步骤:

  1. 初始状态:将第一个元素视为已排序部分,其余元素视为未排序部分。
  2. 遍历未排序部分:依次取出未排序部分的元素。
  3. 插入已排序部分:将取出的元素与已排序部分的元素依次比较,找到合适的位置插入。
  4. 重复步骤2至步骤3,直到所有元素都被插入到已排序部分。

  希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

基本步骤:

  1. 确定间隔序列:选择一个增量序列(也称为间隔序列),该序列的最后一个元素必须为1。常用的增量序列有希尔建议的序列(例如:{n/2, n/4, …, 1})或者Sedgewick序列。
  2. 分组排序:根据增量序列,将原始列表分割成若干个子列表,对每个子列表分别进行插入排序。
  3. 逐步缩小间隔:逐渐缩小增量,重复步骤2,直到增量为1。
  4. 最后的插入排序:当增量为1时,进行最后一次插入排序,此时列表已经被“预排序”,插入排序的效率较高。

  归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

基本步骤:

  1. 分割:将待排序的列表分成两个子列表,直到每个子列表的长度为1(即列表无法再分割)。
  2. 排序:对每个子列表进行递归排序。递归的过程就是不断地将列表分割并排序。
  3. 合并:将两个已排序的子列表合并成一个有序列表。合并过程中,比较两个子列表的头部元素,将较小(或较大)的元素放入合并后的列表中,直到其中一个子列表为空,然后将另一个子列表的剩余部分直接放入合并后的列表中。

  快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

基本步骤:

  1. 选择基准:从列表中选择一个元素作为基准元素(一般情况下是选择数组的第一个或最后一个)。
  2. 分区:将列表中小于基准元素的元素放在基准元素的左边,大于基准元素的元素放在右边,基准元素放在中间。这个过程称为分区操作。
  3. 递归排序:对左右两个子列表分别进行递归排序,直到列表为空或只包含一个元素。
  4. 合并:由于快速排序是原地排序算法,不需要显式的合并步骤。

再详细一点:

  1. 原数组:[3, 5, 1, 9, 7, 2, 8, 4, 6]
  2. 选择基准元素 3,分成左半部分 [1, 2] 和右半部分 [5, 9, 7, 8, 4, 6]
  3. 对左半部分 [1, 2] 递归调用快速排序算法,得到有序数组 [1, 2]
  4. 对右半部分 [5, 9, 7, 8, 4, 6] 选择基准元素 5,分成左半部分 [4] 和右半部分 [9, 7, 8, 6]
  5. 对左半部分 [4] 递归调用快速排序算法,得到有序数组 [4]
  6. 对右半部分 [9, 7, 8, 6] 选择基准元素 9,分成左半部分 [7, 8, 6] 和右半部分 []
  7. 对左半部分 [7, 8, 6] 选择基准元素 7,分成左半部分 [6] 和右半部分 [8]
  8. 对左半部分 [6] 递归调用快速排序算法,得到有序数组 [6]
  9. 对右半部分 [8] 递归调用快速排序算法,得到有序数组 [8]
  10. 将左半部分 [6]、基准元素 7 和右半部分 [8] 拼接起来,得到有序数组 [6, 7, 8]
  11. 将左半部分 [4]、基准元素 5 和右半部分 [6, 7, 8, 9] 拼接起来,得到有序数组 [4, 5, 6, 7, 8, 9]
  12. 将左半部分 [1, 2]、基准元素 3 和右半部分 [4, 5, 6, 7, 8, 9] 拼接起来,得到有序数组 [1, 2, 3, 4, 5, 6, 7, 8, 9]

  堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

基本步骤:

  1. 建立堆:将待排序的列表构建成一个最大堆。这一步通常是从列表的中间位置开始,自底向上地调整每个节点,使得整个列表满足堆的性质。
  2. 排序:将最大堆中的根节点(即最大值)与堆中的最后一个元素交换位置,并将堆的大小减1。交换后可能会破坏堆的性质,因此需要进行堆调整操作,使得堆重新满足堆的性质。
  3. 重复步骤2,直到堆的大小为1,此时整个列表已经排序完成。

  计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

基本步骤:

  1. 统计元素出现次数:遍历待排序的列表,统计每个元素出现的次数,并将统计结果存储在一个额外的计数数组中。计数数组的索引对应于待排序元素的值,而数组中的值对应于该元素出现的次数。
  2. 累加计数:对计数数组进行累加操作,使得每个索引位置的值等于其前面所有索引位置的值之和。这一步可以使得计数数组中的值表示小于等于该索引值的元素个数。
  3. 输出排序结果:根据计数数组中的统计结果,将待排序列表中的元素放置到正确的位置上,最终得到排序结果。

  桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

在额外空间充足的情况下,尽量增大桶的数量
使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

基本步骤:

  1. 划分桶:根据待排序元素的范围,确定一定数量的桶,并将待排序元素分配到相应的桶中。
  2. 桶内排序:对每个桶中的元素进行排序。可以使用任何适合的排序算法,如插入排序、快速排序等。
  3. 合并结果:按照桶的顺序依次取出各个桶中的元素,即可得到有序的结果。

  基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

基本步骤:

  1. 按位排序:从最低位开始,对待排序的元素按照当前位数进行排序。可以使用稳定的排序算法,如计数排序或桶排序。
  2. 重复排序:重复第一步的过程,对每一位进行排序,直到对最高位进行排序。
  3. 合并结果:完成所有位的排序后,即可得到有序的结果。

总结

排序算法 平均时间复杂度 最好情况 最坏情况 空间复杂度 排序方式 稳定性
冒泡排序 O(n^2) O(n) O(n) O(1) In-place 稳定
选择排序 O(n^2) O(n^2) O(n^2) O(1) In-place 不稳定
插入排序 O(n^2) O(n) O(n^2) O(1) In-place 稳定
希尔排序 O(n log n) O(n log^2 n) O(n log^2 n) O(1) In-place 不稳定
归并排序 O(n log n) O(n log n) O(n logn) O(n) Out-place 稳定
快速排序 O(n log n) O(n log n) O(n^2) O(log n) In-place 不稳定
堆排序 O(n log n) O(n log n) O(n log n) O(1) In-place 不稳定
计数排序 O(n+k) O(n+k) O(n+k) O(k) Out-place 稳定
桶排序 O(n+k) O(n+k) O(n^2) O(n+k) Out-place 稳定
基数排序 O(n×k) O(n×k) O(n×k) O(n+k) Out-place 稳定