数据结构中排序算法的详细解析!十大基本的排序算法分析比较以及Java实现

361 阅读16分钟

这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战

基本概念

  • 通常需要熟练掌握快速排序归并排序
  • 要熟练掌握各种排序算法之间的优缺点,各种算法的思想以及各种算法的使用场景,还要熟练分析各种算法之间的时间复杂度和空间复杂度
  • 排序: 对一序列的对象按照某个关键字的顺序进行展示
  • 排序相关术语:
    • 稳定: 如果a在b前面,并且a=ba=b,那么排序之后a还是在b前面
    • 不稳定: 如果a在b前面,并且a=ba=b,那么排序之后a可能会在b后面
    • 内排序: 所有排序操作都在内存中完成
    • 外排序: 由于数据量很大,将数据保存在外部磁盘中,所有排序操作通过磁盘和内存之间的数据传输才能完成
    • 时间复杂度: 一个算法执行耗费的时间
    • 空间复杂度: 运行完一个算法实现的程耗费的内存大小
  • 数据结构中各个排序算法比较:
排序算法名称平均时间复杂度最好情况最坏情况空间复杂度排序方式稳定性
快速排序O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(n2)O(n^2)O(logn)O(logn)占用常数内存不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(n)O(n)占用额外内存稳定
冒泡排序O(n2)O(n^2)O(n)O(n)O(n2)O(n^2)O(1)O(1)占用常数内存稳定
选择排序O(n2)O(n^2)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)占用常数内存不稳定
插入排序O(n2)O(n^2)O(n)O(n)O(n2)O(n^2)O(1)O(1)占用常数内存稳定
希尔排序O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(1)O(1)占用常数内存不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(nlogn)O(1)O(1)占用常数内存不稳定
计数排序O(n+k)O(n+k)O(n+k)O(n+k)O(n+k)O(n+k)O(k)O(k)占用额外内存稳定
桶排序O(n+k)O(n+k)O(n+k)O(n+k)O(n2)O(n^2)O(n+k)O(n+k)占用额外内存稳定
基数排序O(nk)O(n*k)O(nk)O(n*k)O(nk)O(n*k)O(n+k)O(n+k)占用额外内存稳定
  • n: 数据规模
  • k: 桶的数量
  • 数据结构中排序算法的分类:
    • 内部排序:
      • 插入排序:
        • 直接插入排序
        • 希尔排序
      • 选择排序:
        • 简单选择排序
        • 堆排序
      • 交换排序:
        • 冒泡排序
        • 快速排序
      • 归并排序
      • 基数排序
    • 外部排序

比较排序和非比较排序

比较排序

  • 比较排序: 在排序的最终结果里,元素之间的次序依赖于元素之间的比较,每个数都必须和其余的数进行比较,才能确定自己的位置
    • 快速排序
    • 归并排序
      • 在快速排序和归并排序中,问题规模通过分治法消减为nlognnlogn次, 需要比较nn次,所以平均时间复杂度为O(nlogn)O(nlogn)
    • 堆排序
    • 冒泡排序
      • 问题规模为nn,因为需要比较nn次,所以平均时间复杂度为O(n2)O(n^2)
  • 比较排序的特点:
    • 适用于各种规模的数据,不关注数据的分布,都能进行排序
    • 比较排序适用于一切需要排序的情况

非比较排序

  • 非比较排序: 通过确定每个元素之前应该有多少个元素来排序
    • 计数排序
    • 基数排序
    • 桶排序
      • 针对数组Array[i] 之前有多少个元素来唯一确定Array[i] 在排序后数组中的位置
  • 非比较排序只要确定每个元素之前已有的元素的个数即可,只要一次遍历即可解决,所以算法的时间复杂度为O(n)O(n)
  • 非比较排序的特点:
    • 非比较排序的时间复杂度低
    • 因为非比较排序需要占用空间来确定唯一位置,所以对数据规模和数据分布有一定的要求

计数排序和桶排序和基数排序

  • 这三种非比较排序算法都使用了桶的概念,但是对桶的使用方法不同:
    • 计数排序: 每个桶只存储单一键值
    • 桶排序: 每个桶存储一定范围的数值
    • 基数排序: 根据键值的每个位上的数值来分配桶

快速排序

  • 快速排序QuickSort:
    • 通过分区操作将待排序的元素分隔成独立的两部分
    • 其中一部分记录的关键字均比另一部分记录的关键字小
    • 然后对这两个独立的部分分别进行排序以达到整体排序的目的
  • 快速排序算法:
    • 从数列中选取一个元素,作为基准pivot
    • 使用分区操作partition重新排序数列,所有比基准值小的元素都在基准前面,所有比基准序列大的元素都在基准后面.在一个分区操作完成之后,这个基准刚好处于数列的"中间"位置
    • 递归地将小于基准的子序列和大于基准的子序列进行排序
  • 数据结构快速排序算法示例
  • 算法分析:
    • 最好情况: T(n)=O(nlogn)T(n)=O(nlogn)
    • 最坏情况: T(n)=O(n2)T(n)=O(n^2)
    • 平均情况: T(n)=O(nlogn)T(n)=O(nlogn)

归并排序

  • 归并排序MergeSort:
    • 归并排序是建立在归并操作上的数据排序算法,是一种采用分治的稳定的排序算法
    • 归并排序先将需要排序的序列进行分治,先使得子序列有序,再使得子序列段之间有序,然后将已经有序的子序列进行合并,得到完全有序的序列
    • 归并排序的性能不受输入数据的影响,始终都是O(nlogn),O(nlogn),只是需要额外的内存空间
  • 归并排序算法:
    • 将需要排序的序列分成两个长度一致的子序列
    • 将这两个子序列分别进行归并排序
    • 将两个排序好的子序列合并成一个排序好的序列
  • 数据结构归并排序算法示例
  • 算法分析:
    • 最好情况: T(n)=O(nlogn)T(n)=O(nlogn)
    • 最坏情况: T(n)=O(nlogn)T(n)=O(nlogn)
    • 平均情况: T(n)=O(nlogn)T(n)=O(nlogn)

冒泡排序

  • 冒泡排序BubbleSort:
    • 冒泡排序是一种简单的排序算法,因为将较小的元素经过交换慢慢浮到顶端,所以称为冒泡排序
    • 重复访问比较需要排序的序列,每次比较两个元素,如果顺序不对则将元素进行交换
    • 访问比较数列的目的是重复的进行比较直到没有需要交换的序列为止,此时说明序列已经排序完成
  • 冒泡排序算法:
    • 比较相邻的两个元素,如果第一个比第二个大,则交换两个元素的顺序
    • 对每一对相邻的元素进行相同的操作,从开始的第一对到最后一对,这样每一轮比较结束,在最后的元素就是最大的元素
    • 对每一轮最后一个元素之前所有的元素序列进行这样的比较,直到排序完成
  • 数据结构冒泡排序算法示例
  • 算法分析:
    • 最好情况: T(n)=O(n)T(n)=O(n)
    • 最坏情况: T(n)=O(n2)T(n)=O(n^2)
    • 平均情况: T(n)=O(n2)T(n)=O(n^2)

选择排序

  • 选择排序SelectionSort:
    • 选择排序是最普遍的排序算法.无论数据是什么,选择排序算法的时间复杂度都是O(n2).O(n^2). 因此使用选择排序时,数据规模越小越好
    • 选择排序算法唯一的好处就是不会占用额外的内存空间
    • 选择排序算法原理:
      • 首先在未排序的序列中找到最小的元素,存放到排序序列的起始位置
      • 然后从剩余的元素中继续寻找最小的元素,放到已排序的序列的末尾位置
      • 重复这样的方法,直到所有的元素都排序完成
  • 选择排序算法: n个记录经过n-1轮选择排序得到有序结果
    • 初始时, 无序区为R[1...n], 有序区为空
    • 第i轮排序开始时,当前有序区为R[1...i-1], 无序区为R[i+1...n]. 这一轮排序从当前无序区中选出关键字最小的记录R[k], 将这个元素和无序区中的第一记录进行交换,使得有序区R[1...i-1] 和无序区R[i...n] 分别变为记录个数增加1个的新的有序区和记录个数减少1的新的无序区
    • n-1轮结束后,数组序列就是有序化的
  • 数据结构选择排序算法示例
  • 算法分析:
    • 最好情况: T(n)=O(n2)T(n)=O(n^2)
    • 最坏情况: T(n)=O(n2)T(n)=O(n^2)
    • 平均情况: T(n)=O(n2)T(n)=O(n^2)

插入排序

  • 插入排序InsertionSort:
    • 通过构建有序序列,对于未排序的数据,在已排序的序列中从后向前扫描,找到相应的位置并插入
    • 在从后向前扫描的过程中,需要反复将已经排序的元素逐步向后移动一位,为最新的排序元素提供插入空间
    • 通常采用in-palce占用常数内存实现插入排序算法,只需要使用O(1)O(1)的额外空间
  • 插入排序算法:
    • 第一的元素默认是已经排序的,从第一个元素开始取下一个元素,在已经排序的序列中从后向前扫描
    • 如果扫描的元素大于取出的元素,则将序列中扫描的元素向后移动一个位置,直到找到扫描的元素小于或者等于取出的新元素的位置
    • 将取出的元素插入到这个位置
    • 直到整个序列都完成这样的操作,整个序列就是有序的
  • 数据结构插入排序算法示例
  • 算法分析:
    • 最好情况: T(n)=O(n)T(n)=O(n)
    • 最坏情况: T(n)=O(n2)T(n)=O(n^2)
    • 平均情况: T(n)=O(n2)T(n)=O(n^2)

希尔排序

  • 希尔排序ShellSort:
    • 希尔排序也是一种插入排序,是简单插入排序经过改进之后的更高效的排序版本,也叫作缩小增量排序,这个排序方法是第一批突破O(n2)O(n^2)的算法之一
    • 希尔排序与插入排序不同的是: 希尔排序会优先比较距离较远的元素,因此希尔排序叫作缩小增量排序
    • 希尔排序是将需要排序的元素按照一定的增量进行分组,然后对每组的元素使用直接插入排序算法进行排序.随着增量的减少,那么每组包含的元素会越来越多,当增量减少至1时,整个文件恰好分为1组,此时算法完成
  • 希尔排序算法: 将整个待排序的序列分割成若干个子序列分别进行直接插入排序
    • 选择一个增量序列,t1,t2,...,tk.t1, t2, ..., tk.其中ti>tj,tk=1ti>tj,tk=1
    • 对于增量个数kk个的序列,进行kk趟排序
    • 每一趟排序中,根据对应的增量ti,ti,将待排序的序列分割成若干个长度为mm的子序列,分别对各个子序列进行直接插入排序,仅当增量因子为1时,整个序列作为一整个序列进行排序处理 在这里插入图片描述
  • 数据结构希尔排序算法示例
  • 算法分析: 希尔排序的算法时间复杂度位于O(n^(1.3—2)^), 达不到O(n^2^)
    • 最好情况: T(n)=O(nlogn)T(n)=O(nlogn)
    • 最坏情况: T(n)=O(nlogn)T(n)=O(nlogn)
    • 平均情况: T(n)=O(nlogn)T(n)=O(nlogn)

堆排序

  • 堆排序HeapSort:
    • 堆是一个近似完全二叉树的结构.总是一棵完全树,也就是说除了最底层,其余层的节点都被元素填满,且最底层尽可能地从左到右填入
    • 堆的父节点总是大于或者小于子节点的值
      • 父节点大于子节点的值的堆叫作大顶堆
      • 父节点小于子节点的值的堆叫作小顶堆
  • 堆排序算法:
    • 将初始的待排序关键字序列R1,R2,...,Rn构建成大顶堆,这个堆就是初始的无序区
    • 将堆顶元素R1与最后一个元素Rn交换,得到一个新的无序区R1,R2,...,Rn-1和一个新的有序区Rn,并且满足R1,R2,...,Rn-1都小于等于Rn
    • 因为交换后的堆的新的堆顶元素R1可能不符合堆顶的性质,所以需要对新的无序区R1,R2,...,Rn-1调整为新的大顶堆,然后再一次将新的无序区的堆顶元素R1和无序区的最后一个元素Rn-1交换,得到一个新的无序区R1,R2,...,Rn-2和一个新的有序区Rn-1,Rn
    • 重复这样的过程,直到整个待排序序列排序完成
  • 数据结构堆排序算法示例
  • 算法分析:
    • 最好情况: T(n)=O(nlogn)T(n)=O(nlogn)
    • 最坏情况: T(n)=O(nlogn)T(n)=O(nlogn)
    • 平均情况: T(n)=O(nlogn)T(n)=O(nlogn)

计数排序

  • 计数排序CountingSort:
    • 计数排序的核心在于将输入的数据值转化为键值值存储在额外开辟的数组空间中
    • 计数排序是一种线性时间复杂度的排序,要求输入的数据必须是有确定范围的整数
    • 计数排序是一种稳定的排序算法:
      • 计数排序使用一个额外的计数数组,索引位置上的元素是待排序数组中值等于索引值的元素的个数
      • 然后根据计数数组来将待排序数组中的元素排列到正确的位置
      • 计数排序只能对整数进行排序
  • 计数排序算法:
    • 找出待排序数组的最大元素和最小元素
    • 统计待排序数组中每个偏移量为i的元素出现的次数,存入计数数组的索引位置为i
    • 对统计完成的计数数组中的每个索引位置的值累加,得到待排序数组中每个值的存放位置
    • 将排序好的计数数组中的值按照次序填充到待排序数组中
  • 数据结构计数排序算法示例
  • 算法分析:
    • 当对nn00kk之间的整数进行排序时,计数排序的算法时间复杂度为O(n+k)O(n+k)
    • 计数排序不是比较排序,因此算法时间复杂度比任何一个比较排序的算法要好
    • 因为创建的用来计数的数组的长度取决于待排序数组中的整数的取值范围,即最大值与最小值之差加1, 所以对于数据范围很大的待排序数组而言,计数排序需要大量的内存空间
    • 最好情况: T(n)=O(n+k)T(n)=O(n+k)
    • 最坏情况: T(n)=O(n+k)T(n)=O(n+k)
    • 平均情况: T(n)=O(n+k)T(n)=O(n+k)

桶排序

  • 桶排序BucketSort:
    • 工作原理: 如果输入的数据服从均匀分布,即待排序的元素是等可能的落在等间隔的值区间内将数据分到有限数量的桶中,然后对每个桶再分配排序.可以使用其余算法进行排序,也可以递归使用桶排序算法进行排序
      • 一个长度为N的数组使用桶排序时:
        • 需要长度为N的辅助数组
        • 等间隔的区间称为桶,每个桶内放入待排序数组的元素
      • 待排序数组中的元素越均匀,桶排序的效率越高,也就是桶中的元素的个数大体相等,这样桶排序达到最优效率
    • 桶排序是利用函数的映射关系对待排序数组中的元素进行排序,桶排序算法高效的关键就在于这个映射函数的确定
    • 桶排序映射函数: 建立桶位置和待排序数组元素的函数映射
      • 将元素通过恰当的映射关系尽量等数量的分入到各个桶中,可以保证桶排序达到最优效率
      • index=f(v):index = f(v):
        • indexindex为桶数组的索引
        • vv为待排序数组中的元素
        • 保证如果v1<v2,v1<v2, 那么index1<index2.index1 < index2 .也就是说小索引桶中的最大元素要小于大索引桶中的最小元素.由此可见,桶排序映射函数的确定由数据本身的分布特点决定
  • 桶排序算法:
    • 首先初始化待排序数组长度的集合作为桶排序中的桶区间
    • 遍历待排序数组,根据桶排序映射函数将待排序数组中的元素分入到对应的桶中
    • 对于非空的桶进行排序,可以使用其余的排序算法,推荐使用比较排序,比如快速排序,归并排序,堆排序和冒泡排序等.也可以递归使用桶排序
    • 将非空桶中排序好的元素拼接起来,完成待排序数组的排序
  • 数据结构桶排序算法示例
  • 算法分析:
    • 桶排序的算法时间复杂度分为两个部分:
      • 循环计算每个待排序元素的桶映射函数的索引值.这个过程的算法时间复杂度是O(n)O(n)
      • 对桶中的数据进行排序,这是提升桶排序算法效率的决定因素:
        • 一是保证桶排序映射函数能够将待排序数组中的元素平均的分布到桶中
        • 二是尽可能增加桶的数量
    • 桶排序的算法时间复杂度是O(n),O(n), 桶排序的算法时间复杂度取决于每个桶中的数据进行排序的算法时间复杂度,其余部分的算法时间复杂度都是O(n).O(n). 由此可见,桶划分得越小,桶中的数据越少,桶排序的算法时间复杂度也就越小,但是相应的空间复杂度会增大
    • 最好情况: T(n)=O(n+k)T(n)=O(n+k)
    • 最坏情况: T(n)=O(n2)T(n)=O(n^2)
    • 平均情况: T(n)=O(n+k)T(n)=O(n+k)

基数排序

  • 基数排序RadixSort:
    • 基数排序是一种非比较排序算法.从最低位开始,对每一位进行排序,算法时间复杂度和待排序数组中元素的最大位数有关
    • 基数排序是按照低位先排序,然后收集.接着再按照高位排序,然后再收集.直到完成最高位的排序和收集.这里的位数也可以是不同的优先级
    • 基数排序基于待排序元素的差别排序,基于差别收集.所以属于稳定排序
    • 基数排序适用于小范围的数的排序
  • 基数排序算法:
    • 获取待排序数组中元素最大的数字,计算出数字的位数
    • 从最低位开始按照待排序数组元素指定位上的数字对待排序数组中的元素进行基数排序
    • 每一轮完成一个位数上的排序,直到完成所有位数上的排序后,完成整个待排序数组的排序
  • 数据结构基数排序算法示例
  • 算法分析:
    • 基数排序有两种方法:
      • LSD: 从低位开始排序
      • MSD: 从高位开始排序
      • 通常使用LSD从低位开始排序
    • 最好情况: T(n)=O(nk)T(n)=O(n*k)
    • 最坏情况: T(n)=O(nk)T(n)=O(n*k)
    • 平均情况: T(n)=O(nk)T(n)=O(n*k)