【常见的十种排序算法及稳定性比较】

行动是治愈恐惧的良药,而犹豫拖延则是恐惧的营养剂

目录

  1. 插入排序
  2. 希尔排序
  3. 选择排序
  4. 堆排序

    >什么是堆?

    >堆排序

  5. 冒泡排序
  6. 快速排序
  7. 归并排序
  8. 基数排序
  9. 计数排序
  10. 桶排序
  11. 算法稳定性比较
  12. 总结


  排序算法分为内部排序外部排序,内部排序和外部排序的区别在于待排序数据是否可以全部加载到内存中,如果能够全部加载到内存中,则使用内部排序算法;否则需要使用外部排序算法。

以下是常用的内部排序算法和外部排序算法列表:

1.内部排序算法:

  • 冒泡排序
  • 插入排序
  • 选择排序
  • 希尔排序
  • 归并排序
  • 快速排序
  • 堆排序
  • 桶排序
  • 计数排序
  • 基数排序

2.外部排序算法:

  • 归并排序
  • 快速排序

  归并排序和快速排序既可以作为内部排序算法也可以作为外部排序算法,具体应用取决于待排序数据的规模和内存大小。但一般情况下,排序算法交换次数和比较次数是影响时间复杂度的重要因素,因此在选择排序算法时需要结合实际情况来确定。

一、插入排序

插入排序的基本思想是将待排序元素依次插入到已排序序列中的适当位置,以达到排序的目的。

具体实现过程如下:

  1. 从第一个元素开始,将其视为已排序序列。
  2. 取出下一个元素,在已排序序列中寻找合适的插入位置,并将该元素插入到已排序序列中。
  3. 不断重复第二步,直至完成所有元素的插入。

直接插入排序的时间复杂度为O(n2),但当待排序元素基本有序时,可以大幅缩短排序时间。

例如:

对数组 [8, 3, 2, 10, 4] 进行升序排序,使用插入排序算法

  1. 第一个元素 8 视为已排序。
  2. 取出下一个元素 3,在有序子列表 [8] 中查找插入位置,由于 3 < 8 ,因此将 3 插入到有序子列表 [3,8] 中。
  3. 取出下一个元素 2,由于插入列表长度为 2,且 2 小于 3 ,因此直接插到最前面,有序子列表为 [2,3,8]
  4. 取出下一个元素 10,查找插入位置,由于 10 大于 8 ,因此直接插入到最后,有序子列表为 [2,3,8,10]
  5. 取出下一个元素 4 ,查找插入位置,在子列表 [2,3,8,10] 中插入 4 ,得到有序子列表 [2,3,4,8,10]

数组 [8, 3, 2, 10, 4] 经过插入排序算法处理之后变成了 [2,3,4,8,10]。

二、希尔排序

希尔排序是插入排序的升级版,也被称为“缩小增量排序”。其基本思想是先将待排序序列分成若干个子序列,对每个子序列进行插入排序,在不断缩小子序列长度的情况下重复该过程,直至所有元素在一个序列中。希尔排序通过这种方式减少了需要移动的元素数目,从而使得排序效率更高。

具体实现过程如下:

  1. 选择一个增量序列,常用的增量序列为:N/2, N/4, …, 1,其中N是待排序序列长度。
  2. 对每个增量值,将待排序序列分成若干个子序列,每个子序列的元素下标相差增量值,对每个子序列进行插入排序。
  3. 不断减小增量值,重复第二步操作,直至增量值为 1。

最后以增量为 1 的序列做一次插入排序即可完成排序。

希尔排序的时间复杂度为O(n1.3)比简单插入排序低,在实践中表现良好,但由于增量序列的选取是关键因素之一,因此选取不当可能会导致排序效率变差。

例如:

对数组 [9, 7, 5, 11, 12, 2, 14, 3, 10, 6] 进行升序排序,使用希尔排序算法。

  1. 取增量 h=N/2=5,将数据分为 5 组 [9, 2], [7, 14], [5, 3], [11, 10], [12, 6]。
    a. 对每组分别进行插入排序。
    b. 得到结果 [2, 7, 3, 10, 6, 9, 14, 5, 11, 12]。

  2. 取增量 h=5/2=2,将得到的结果分为 2 组 [2,3,6,14,11], [7,10,5,9,12]。
    a. 对每组分别进行插入排序。
    b. 得到结果 [2, 3, 5, 9, 6, 7, 11, 14, 10, 12]。

  3. 取增量 h=2/2=1。
    a. 对整个序列进行插入排序。
    b. 得到结果 [2, 3, 5, 6, 7, 9, 10,11,12,14]。

数组 [9, 7, 5, 11, 12, 2, 14, 3, 10, 6] 经过希尔排序算法处理之后变成了 [2, 3, 5, 6, 7, 9, 10,11,12,14]。

三、选择排序

选择排序(Selection Sort)是一种简单直观的排序算法,其基本思想是每个循环选择数组中最小的元素,和当前未排序部分的第一个元素交换位置。这样不断重复,直到整个数组都有序为止。

具体实现过程如下:

  1. 每一轮从待排序的数组中选择最小值。
  2. 把选出的最小值放在前面已排序序列的末尾位置。
  3. 不断重复步骤 1 和 2,直到所有元素排序完成。

选择排序的时间复杂度为 O(n²),不管原始数组是否有序,每次迭代都需要找到最小值,所需比较次数与数据的初始状态无关,因此复杂度为 O(n²);但是由于涉及大量的元素交换操作,选择排序的常数因子较大,效率相对较低。

例如:

用选择排序对数组 [9, 7, 5, 11, 12, 2, 14, 3, 10, 6] 进行升序排序。

  • 首先,在第一轮迭代后,我们会找到最小值 2,把它与第一个元素 9 交换位置,得到 [2, 7, 5, 11, 12, 9, 14, 3, 10, 6]。

  • 接着,在第二轮迭代中,我们会找到第二小的元素 3,把它与第二个元素 7 交换位置,得到 [2, 3, 5, 11, 12, 9, 14, 7, 10, 6]。

继续不断重复以上步骤,直到整个数组升序排序完成。

最终排序结果为:[2, 3, 5, 6, 7, 9, 10, 11, 12, 14]。

四、堆排序

什么是堆?

堆是一种特殊的树状数据结构,它满足以下两个条件:

  1. 堆是一个完全二叉树,也就是说,在堆中除了最后一层,其他层都必须是满的,并且最后一层可以不是满的,如果最后一层不满,节点也必须靠左排列。

  2. 在二叉堆中,父节点的值总是大于或等于(小于或等于)其子节点的值。这里我们称之为大根堆和小根堆。

堆有很多用处,其中最常见的是实现优先队列,以及作为各种排序算法的基础。

堆排序

堆排序是一种高效的排序算法,它利用堆这种数据结构来实现。堆排序的基本思想是将待排序的数组构建成一个大根堆或小根堆,然后依次取出堆顶元素并重排剩余元素形成新的堆,直到所有元素都被排序完成。

具体步骤如下:

  1. 创建一个空堆,把待排序序列构建成一个大根堆或小根堆。
    a. 首先对这些数据建立完全二叉树。
    b. 填充的规则是按层次遍历将数据一一填入。
    c. 接下来要把此二叉树调整为大根堆,首先看第n/2个结点,它比其子结点小,所以应该把子结点和第n/2个结点的位置对调。
    d. 接下来看(n/2)-1个结点,和子结点比较,把大的调整到根结点。
    e. 继续看(n/2)-2个结点,重复上面的步骤,直到比较到整颗完全二叉树的根结点,此时,大根堆就构建完成了。

  2. 依次取出堆顶元素(最大值或最小值),把堆顶元素与末尾元素交换位置,然后重排剩余的元素形成新的堆。

  3. 重复上述步骤,直到所有元素都被排序完成为止。

堆排序的时间复杂度为 O(nlog2n)。由于堆排序是一种基于比较的排序算法,所以其时间复杂度的下界也是 O(nlog2n)。

相对于其他基于比较的排序算法,堆排序虽然不是最快的,但它的空间复杂度很低为O(1),只需要常数级别的额外空间用来存储堆,因此在实际应用中还是非常有效的。

五、冒泡排序

冒泡排序的基本思想是通过相邻元素之间的比较和交换来达到排序的目的。

具体来说,冒泡排序的过程如下:

  1. 从数组的第一个元素开始,依次比较相邻两个元素的大小。如果前面一个元素大于后面一个元素,就把它们交换位置。

  2. 继续对相邻两个元素进行比较和交换,直到最后一个元素。

  3. 重复上述过程,直到所有元素都被排序完成。这里需要注意的是,每次排序一轮后,在下一轮排序时只需要考虑未排序的元素即可。

冒泡排序的时间复杂度为 O(n2),其中 n 是待排序序列的长度。由于冒泡排序每轮只能将一个元素排好序,因此其时间复杂度的下界是 O(n)。在实际应用中,冒泡排序主要用作教学和理论研究,而不太适合处理大规模数据。

需要特别注意的是,由于冒泡排序是稳定的排序算法,即如果两个元素的值相等,那么它们的相对位置不会发生变化。

例如:

假设我们要对以下数组进行排序:[5, 3, 8, 6, 4]。

  • 首先,我们将从第一个元素开始比较相邻的两个元素。由于第一个元素是5,第二个元素是3,且5大于3,因此交换它们的位置,数组变为[3, 5, 8, 6, 4]。

  • 接下来,继续比较相邻的两个元素。此时,5和8的顺序正确,因此不需要交换它们的位置。但是,8和6的顺序不正确,因此需要将它们交换,数组变为[3, 5, 6, 8, 4]。

  • 再次比较相邻的两个元素,此时8和4的顺序不正确,因此需要将它们交换,数组变为[3, 5, 6, 4, 8]。

这样一轮下来,我们已经将最大的元素8“冒泡”到了数组的最后一个位置。在接下来的排序中,只需要考虑前面四个元素即可。继续按照上述方法进行排序,直到所有元素都排好序。

最终,数组的排序结果为:[3, 4, 5, 6, 8]。

六、快速排序

快速排序的基本思想是通过“分治”的思想将数组分成两个子数组,一部分比另一部分小,然后递归地对两个子数组继续进行快速排序,最终得到一个有序数组。

具体来说,快速排序的步骤如下:

  1. 从数组中选择一个元素作为“基准值”。

  2. 将数组中所有小于基准值的元素移到基准值的左边,大于基准值的元素移到基准值的右边。这一步称为“分区”。

  3. 对基准值左边的子数组和右边的子数组分别递归进行快速排序。

  4. 重复上述步骤,直到所有子数组都被排好序。

快速排序的时间复杂度是 O(nlog2n),其中 n 是待排序序列的长度。由于需要将数组不断地拆分并进行排序,因此快速排序使用了递归算法。但在某些情况下,快速排序的退化情况(如基准值选择不当或者数据本身已经近乎有序等)会使得其时间复杂度退化到 O(n2) 级别。

需要特别注意的是,在快速排序中,基准值的选择对算法的效率有很大的影响。通常,我们会选择数组的中间元素、第一个元素或最后一个元素作为基准值。

七、归并排序

归并排序是一种经典的分治算法,它的主要思想是将待排序数组分成若干个子数组,然后对每个子数组进行排序,最终将它们合并成一个有序的大数组。归并排序采用了递归方式进行实现。

具体的排序步骤如下:

  1. 将待排序数组左右拆分,直到每个子数组只剩下一个元素。

  2. 对于每个子数组,将它们两两合并,并将它们排序。这里需要使用借助一个临时数组来存储排好序的元素。

  3. 重复上述过程,直到所有的子数组都被合并为一个大的有序数组。

例如:

假设待排序的数组为[38, 27, 43, 3, 9, 82, 10]。

  1. 首先,将数组从中间位置分成左右两个子数组:[38, 27, 43, 3] 和 [9, 82, 10]。

  2. 然后对左右两个子数组进行递归操作。对左子数组执行步骤1和步骤2,得到[27, 38, 3, 43]。对右子数组执行步骤1和步骤2,得到[9, 10, 82]。

  3. 接着,我们将已经排好序的子数组[27, 38, 3, 43]和[9, 10, 82]合并成一个新的有序数组,步骤如下:

    a. 创建一个长度为7的临时数组temp。
    b. 比较左右两个子数组的第一个元素,将较小的元素存入temp中。如果有一个子数组已经被遍历完了,就将另一个子数组的剩余元素直接存入temp中。
    c. 重复上述过程,直到左右两个子数组都被遍历完了。此时,temp中就存储了原始数组中所有元素按大小排序后的结果。

最终,经过递归操作和合并排序,我们得到排好序的数组 [3, 9, 10, 27, 38, 43, 82]。

八、基数排序

基数排序是一种非常高效的排序算法,它主要针对数字类型数据的排序。基数排序的核心思想是将待排元素按照从低位到高位的顺序依次排序。因为每个数字有多位,我们可以先根据各位大小进行排序,再根据十位大小排序,以此类推,直到排完所有位数。

【常见的十种排序算法及稳定性比较】_第1张图片

九、计数排序

计数排序(Counting Sort)是一种非比较型整数排序算法,它的时间复杂度为O(n+k),其中n是待排序数组长度,k是待排序元素的取值范围。当k很小,且序列比较密集时,计数排序的效率往往高于基于比较的排序算法。

计数排序的基本思路是对于待排序数组中每个元素,统计有多少个元素小于等于该元素的值,并根据这个结果将该元素直接放到输出数组中的正确位置。

十、桶排序

桶排序(Bucket Sort)是一种外部排序算法,通常使用在待排序元素均匀分布的情况下,可以有效减小时间复杂度。

桶排序的基本思路是将待排序元素分到若干个桶中,每个桶内部再使用其他排序算法(如插入排序、快速排序等)进行排序,最后依次输出每个桶内排好序的元素。

例如:

假设我们有以下一组待排序元素:[30, 40, 50, 60, 10, 20, 70, 80]

  1. 确定桶的个数及每个桶存放元素的范围:

假设选择4个桶,每个桶存放元素范围为10~ 29、30~ 49、50~ 69、70~89

  1. 将n个元素逐个分配到对应的桶中:

    ● 将10、20分别放入第1个桶(10~29)
    ● 将30、40、50分别放入第2个桶(30~49)
    ● 将60放入第3个桶(50~69)
    ● 将70、80分别放入第4个桶(70~89)

此时各个桶内部的元素如下:

Bucket 1: [10, 20]
Bucket 2: [30, 40, 50]
Bucket 3: [60]
Bucket 4: [70, 80]

  1. 对每个桶中的元素进行排序:

这里选用插入排序对每个桶中的元素进行排序,排序后各个桶变为:

Bucket 1: [10, 20]
Bucket 2: [30, 40, 50]
Bucket 3: [60]
Bucket 4: [70, 80]

  1. 各个桶已经排好序了,可以按照桶的顺序依次将元素输出到结果数组中:

最终排好序的结果为:[10, 20, 30, 40, 50, 60, 70, 80]

可以看到,桶排序是一种非常高效的排序算法,适用于待排序元素均匀分布的情况下。

十一、算法稳定性比较

  在排序算法中,若策略能够保证待排序序列中相等元素之间的原有顺序不变,即经过排序后,相等元素之间的相对位置仍然保持不变,那么就称这个排序算法是稳定的。

  以计数排序为例,如果有两个相同值的元素在原始数据中位置靠前的元素先出现,在排序的结果中也应该在位置靠前,如果排序算法可以保证这两个元素的顺序不变,我们就说计数排序是稳定的。

  稳定性在某些情况下很重要。例如,当需要对学生成绩进行排序,如果两名学生有相同分数,但是一个比另一个先参加考试,则在排序后前者应该排在后者的前面。如果算法不是稳定的,则无法满足这种需求。

【常见的十种排序算法及稳定性比较】_第2张图片
  桶排序可以是稳定的。桶排序的实现中,元素是通过将其分配到为每个桶预先定义的范围内来分类的,而每个桶之间是没有关系的,所以对于输入的序列中相同大小的元素,它们会被分配到同一个桶中,即在桶内部它们顺序不变,从而保证了相对位置的稳定性。因此,如果在桶内使用稳定的排序算法,则可以实现稳定的桶排序。然而,在对桶进行划分时,如何设置桶的数量和其划分范围会影响最终结果,需要根据具体需求进行调整。

可以通过计基插桶泡归来记忆稳定的排序算法。

十二、总结

【常见的十种排序算法及稳定性比较】_第3张图片

你可能感兴趣的:(数据库系统工程师,排序算法,算法,数据结构)