Skip to content

2025-07-31-排序 ​

本文章默认是从小到大排序

排序的概念 ​

  • 稳定性: 排完序后,位置会不会乱。比如 [8,5a,5b] 这个数组(a,b 仅仅用来标记)

    稳定的排完序后是 [5a,5b,8]

    不稳定的排完序后可能是 [5b,5a,8]

  • 内部排序:在内存上的排序,适用于数据比较少的情况

  • 外部排序:在外存(硬盘)上的排序,适用于数据比较多的情况

排序的类型 ​

  1. 基于比较排序
    1. 插入排序
      1. 直接插入排序
      2. 希尔排序
    2. 选择排序
      1. 选择排序
      2. 堆排序
    3. 交换排序
      1. 冒泡排序
      2. 快速排序
    4. 归并排序
  2. 非基于比较排序(了解即可)
    1. 计数排序
    2. 基数排序
    3. 桶排序

排序的实现 ​

直接插入排序 ​

思想:

  1. 从第 2 个元素开始,获取到这个元素的值,并用临时变量存储
  2. 如果这个元素比前一个元素大,那么就不需要覆盖,直接跳出这个循环
  3. 如果比前一个元素小,那么就要前一个元素的内容就要覆盖后一个元素的内容,然后继续判断,直到遍历完或者条件不满足
  4. 在 2/3 这两步循环完后,就可以把之前临时存储的元素放入最后一个覆盖元素的原位置上
  5. 1/2/3/4 是一次循环,按照这样的步骤,遍历完整个数组后即可完成循环

性质:

  • 时间复杂度:​O(N2)
  • 空间复杂度:​O(1)
  • 稳定性:​稳定
用例 1/1
步骤: 0 / 0
temp (临时变量)
空

当前动作:等待输入...

希尔排序 ​

希尔排序是直接插入排序的优化

思想:

  1. 它是分组排序,分为 array.length / gap 组,通过让每组元素的个数变小从而让排序次数变小
  2. 内部排序(shell() 执行的过程)的思路与直接插入排序基本相同,只是从相隔 1 之间排序变为相隔 gap
  3. 外部排序只需要保证 gap 最后为 1 的时候在执行一次内部排序即可

性质:

  • 时间复杂度:​O(N1.3)∼O(N1.5)

    由于 gap 不确定什么时候最优解,所以目前来说,这个时间复杂度还没有确定的值

  • 空间复杂度:​O(1)
  • 稳定性:​不稳定
用例 1/1
步骤: 0 / 0

当前动作:等待输入...

选择排序 ​

由于时间复杂度/稳定性都没有优势,所以通常不使用这个排序

思想:

  1. 先遍历一次数组获取最小值的下标
  2. 接着把第一个元素与最小值交换,把第一个元素变为最小值
  3. 然后遍历第二个,第三个元素,直到遍历完数组为止

性质:

  • 时间复杂度:​O(N2)
  • 空间复杂度:​O(1)
  • 稳定性:​不稳定
用例 1/1
步骤: 0 / 0

当前动作:等待输入...

堆排序 ​

详情见:通过 优先级队列 这篇文章,里面有堆的 创建+排序

性质:

  • 时间复杂度:​O(N×log2⁡N)
  • 空间复杂度:​O(1)
  • 稳定性:​不稳定
用例 1/1
步骤: 0 / 0

当前动作:等待输入...

冒泡排序 ​

思路:

  1. 如果第一个数据比第二个数据大,那么就交换
  2. 一直执行上诉步骤,直到遍历完一次,此时最后一个元素一定是最大值
  3. 接着排除最后一个,反复循环1/2两个步骤,即可排完序

性质:

  • 时间复杂度:​O(N2)
  • 空间复杂度:​O(1)
  • 稳定性:​稳定
用例 1/1
步骤: 0 / 0

当前动作:等待输入...

快速排序 ​

快速排序类型分为 Hoare法,挖坑法,前后指针法

思路:

  1. 找到一个中间值,通过 partition 这个方法后,保证这个中间值的左边比这个值小,右边比这个值大
  2. partition 返回这个中间值的下标 pivot,通过递归,排序完左边与右边的那一部分
  3. parition 由于类型不同,思路也有所不同,所以在具体的内部在解析

partition 是在具体的方法内部实现 完整代码是 框架内容+具体类型的partition实现 性质:

  • 时间复杂度:​最好:O(N∗log2⁡N),最坏:O(N∗2)
  • 空间复杂度:​最好:O(log2⁡N),最坏:O(N)
  • 稳定性:​不稳定

思考 ​

  1. 为什么是先从右边开始,而不是从左边开始?

    ​排序的结果从小到大,从左边开始,最后一次就会获取到较大值,与 tmp 交换,导致较大值换到左边

  2. 获取较大值/较小值的下标时候,能不能省略等号? 为什么?

    比如 left < right && array[right] >= tmp 能不能变为 left < right && array[right] > tmp

    ​不取等号会有死循环这个可能,比如 [5,5,5] 这个数组就有这个问题

快速排序类型实现 ​


Hoare法 ​

思路:

  1. 获取左边第一个元素 tmp 作为基准,并记录下此时的下标 i
  2. 先从右开始,获取到比 tmp 小的下标 right
  3. 然后从左边开始,获取到比 tmp 大的下标 left
  4. 使用 swap() 这个方法交换 right 与 left 所对应的值
  5. 直到循环结束,最后 使用 swap() 交换 i 与 left 所对应的值 这个下标并返回 left
用例 1/1
步骤: 0 / 0

当前动作:等待输入...

挖坑法 ​

思路:

  1. 获取左边第一个元素 tmp 作为基准,用 tmp 存储
  2. 先从右开始,获取到比 tmp 小的下标 right,循环结束后,用 right 这个值覆盖掉 left 对应的值
  3. 然后从左边开始,获取到比 tmp 大的下标 left,循环结束后,用 left 这个值覆盖掉 right 对应的值
  4. 直到循环结束,最后 将 tmp 赋值给 left 这个下标并返回 left
用例 1/1
步骤: 0 / 0
temp (基准/被挖出的坑)
空

当前动作:等待输入...

前后指针法 ​

这个仅仅只需要了解即可

用例 1/1
步骤: 0 / 0

当前动作:等待输入...

快速排序优化 ​

背景:

由于快速排序主要的问题在于 在N个数据是有序 的情况下,

树的高度会趋于 N(遍历次数),而每次遍历的个数是 N, 导致时间复杂度会区域 O(N2)

优化目标: 把 树的高度尽可能降低 来实现排序的优化

  1. 获取三数的中位数
  2. 在递归到小区间,使用插入排序经行优化

非递归快排 ​

用 Stack<Integer> 存储,用两个元素分别表示头和尾,这样就可以知道快排的范围了

归并排序 ​

思想:分而治之,分为“归”和“并”这两个过程

  1. 一直对半分,直到这个数组只剩下2个元素为止(归)
  2. 2个元素可以看为两个有序数组,将两个有序数组合并为一个有序数组,并覆盖掉原数组(并)
  3. 反复执行 1/2 这两个步骤,直到全部都遍历完

性质:

  • 时间复杂度:​O(N∗log2⁡N)
  • 空间复杂度:​O(N)
  • 稳定性:​稳定
用例 1/1
步骤: 0 / 0
原数组用于“分割”与对比元素
↓ 归 并 ↓
↑ 复 制 ↑
临时数组 (Temp)用于存放排序好的子区间元素

当前动作:等待输入...

递归方式实现归并排序 ​

非递归方式实现归并排序 ​

疑问 ​
  1. 为什么 gap < array.length 不需要等号

    ​gap 表示有序数组的长度,等于 length 时候,有序数组的长度与原数组一样,那么就根本不需要排序了

  2. 为什么是使用 merge(array, left, mid, right) 而不是使用 merge(array, left, right)

    非递归的情况下,right 由于会数组越界,所以在越界的时候会修正一下,此时如果还按照 mid = (right + left) / 2 来计算,那么 mid 就不是两个有序数组的中间了,而是整个数组的中间了

计数排序 ​

适用于数据比较集中的数字集合

思路:

  1. 计算最大值 max 与最小值 min,依据 max 与 min 创建计数数组
  2. 遍历数组,通过元素值获取到相关值在计数数组的下标,然后在计数数组内加一
  3. 最后依据计数数组的下标与内部的计数,把相关信息赋值给原数组即可

性质:

  • 时间复杂度:​O(max(N,max−min))
  • 空间复杂度:​O(max−min)
  • 稳定性:​稳定
用例 1/1
步骤: 0 / 0
主数组 (Main)待排序数据与最终结果
↓ 1. 统计频率 ↓
↑ 2. 依次重构 ↑
计数数组 (Count Array)索引代表具体数值,内部存储该数值出现的频次

当前动作:等待输入...

基数排序 ​

适用于纯数字的情况

思路:

  1. 创建10个队列(从 0 下标开始)
  2. 从个位数开始,通过当前位数的数字获取到队列,然后添加进去即可
  3. 一轮添加完毕后,从 0 下标的队列开始,依次出来,返回原来数组中
  4. 反复循环 1/2 这两个步骤,直到添加所有的位数都遍历过
  5. 最后一次出来后,数组就有序了
用例 1/1
步骤: 0 / 0
主数组
↓ 1. 分配 (Distribute) ↓
↑ 2. 收集 (Collect) ↑
0-9 号桶 (Buckets)根据当前位数的数值,将元素按序入桶

当前动作:等待输入...

桶排序 ​

适用于纯数字的情况

思路:

  1. 分为若干桶,每桶存储一定范围内的数字
  2. 然后每桶内部经行排序
  3. 最后利用有序数组合并这个思想,把各桶之间的数据进行合并
用例 1/1
步骤: 0 / 0
主数组待分配的原始数据与合并后的最终结果
↓ 1. 按值域入桶 ↓
↑ 2. 桶内排序并收回 ↑
数据桶 (Buckets)每个桶负责一个固定的数值范围(区间长度为 )

当前动作:等待输入...