常见排序算法
一. 选择排序
在循环中,每次找到剩下数组中最小的元素,并将这个最小的元素与当前元素交换位置。最后,整个数组就是有序的了。
1 | private static void selectSort(int[] arr) { |
选择排序是最稳定的排序算法,哪怕是已经有序的数组进行选择排序的时间复杂度仍为O(n^2)
,其不会利用数组的初始状态。空间复杂度为O(1)
。
二. 插入排序
与选择排序一样,插入排序当前索引左侧的所有元素都是有序的,但他们的最终位置不确定。
对于当前元素current
,取出上一个元素arr[preIndex]
,如果上一个元素大于当前元素则将其移动到其下一个位置,重复此过程。这就相当于将current
左侧比current
大的元素全部右移一位,给current
腾出位置。
1 | private static void insertSort(int[] arr) { |
插入排序利用了初始数组部分有序的特点,因此适合于局部有序的情况。
平均时间复杂度为O(n^2)
,对于已经有序的数组,时间复杂度为O(n)
。空间复杂度为O(1)
。
三. 希尔排序
希尔排序是基于插入排序的改进排序算法。对于大规模乱序数组,插入排序很慢,因为它只会移动相邻的元素。希尔排序为了加快速度改进了插入排序,交换不相邻的元素以对数组进行局部排序,并最终用插入排序对局部有序的数组进行排序。
设置h
作为间隔,对于每个h
,用插入排序将h
个子数组进行独立的排序,只需将插入排序中移动元素的距离由1改为h
即可。
希尔排序比插入排序高效的原因是其权衡了子数组的规模和有序性。排序之初,子数组都很小,排序后子数组是局部有序的,这都有利于插入排序。
1 | private static void shellSort(int[] arr) { |
在我个人本地随机数组测试中,当数据规模大于等于10000时,希尔排序比插入排序近似快了一倍,且随着数据规模的扩大,速度差距也跟着扩大。
希尔排序的平均复杂度为O(n^1.3)
,最坏时间复杂度为O(n^2)
,最好时间复杂度为O(n)
,空间复杂度为O(1)
。
四. 归并排序
归并排序是建立在归并基础上的排序算法。归并时将元素复制到辅助数组aux
中,再把归并结果放回到原数组中。
自顶向下的归并排序
自顶向下的归并排序是分治思想的典型示例。要对子数组arr[left, right]
进行排序,先将其分为arr[left...mid]
和arr[mid+1...right]
两部分,分别通过递归调用将他们单独排序,最后将排序好的子数组归并成最终的排序结果。
1 | //用到的辅助数组 |
自底向上的归并排序
自底向上的归并排序使用的是迭代的形式,先归并小数组,再成对归并得到的子数组。
初始时设置步长sz
为1,因为是成对归并,所以每次步长长度增长一倍即sz = sz + sz
。
枚举数组左边界lo
,lo
从0开始,每次增长一对sz
的大小,将这一对sz
大小的区间进行归并。
1 | public static void mergeSortIterator(int[] arr) { |
归并排序的时间复杂度为O(nlogn)
, 空间复杂度为O(n)
。
在归并排序中使用了全局辅助数组aux
,更好的做法是把aux
作为mergeSort
方法的局部变量,并把它作为参数每次传递给merge
方法。
五. 快速排序
快速排序也是一种分治的排序算法,它将一个数组分成两个子数组,将两部分独立的排序。快速排序与归并排序是互补的:归并排序将数组分成两部分分别排序,并将有序的子数组归并来将整个数组排序;而快速排序则是当两个子数组都有序时整个数组就有序了。归并排序的递归调用发生在处理整个数组之前;快速排序的递归调用发生在处理整个数组之后。
快速排序通过递归调用切分来排序。先排定一个基准元素,一般是arr[left]
,然后从数组左端开始扫描直到找到一个比他大的元素,再从数组右端进行扫描直到找到一个比他小的元素。交换他们的位置,直到左右指针相遇,再将基准元素与相遇位置的元素交换位置。
1 | public static void quickSort(int[] arr) { |
快速排序的平均时间复杂度为O(nlogn)
,最坏时间复杂度为O(n^2)
,空间复杂度为O(nlogn)
。
六. 堆排序
堆是一种近似二叉树的结构,一般用一维数组表示,且子节点的键值或索引总小于(或者大于)他的父节点。若按升序排列,则把数组换成最大堆。重复从最大堆取出数值最大的结点(把根结点和最后一个结点交换,把交换后的最后一个结点移出堆),并让残余的堆维持最大堆性质。
对于起始索引为0的数组来说,节点索引为i
的节点:
- 左子节点为
2*i+1
; - 右子节点为
2*i+2
; - 父节点为
floor((i-1)/2)
。
堆中最大值总是位于根节点。
1 | public static void heapSort(int[] arr) { |
堆排序的时间复杂度为O(nlogn)
。