看算法第四版整理的关于排序算法的部分。
下面的排序思路都默认做升序排序
选择排序
思路
找到数组中最小的元素,将其和数组首元素交换位置。再找到剩下元素中最小的元素,将其和数组第二个元素交换位置,如此往复。
评估
选择排序需要大概 N 2 / 2 N^2 / 2 N2/2次比较,和 N N N次交换。
- 优点
数据移动最少,交换次数和数组的大小是线性关系。 - 缺点
无论输入如何排序时间不会减少。
插入排序
思路
维护一个指针从数组首元素开始每次向右递增一个元素,永远保证指针左侧的数组是有序的。
即每向右移一位,就将该元素与其左侧元素比较,若小则交换位置,直到将其放到正确的位置上。
评估
假设数组长度为N,倒置1数量为T。
插入排序的比较为
T
T
T ~
T
+
N
−
1
T+N-1
T+N−1,交换次数为
T
T
T。
- 优点
倒置越少,排序越快。对部分有序的数组十分高效,也很适合小规模数组。
发扬这两个优点就得到了希尔排序。
希尔排序
思路
对于插入排序,每次只移动一个位置十分低效,希尔排序就是解决这个问题的。
其想法是使数组成为h有序数组,即其任意间隔为h的元素都是有序的。
过程是这样的:首先确定一个递增序列,即h从1到大的一个序列。
递增序列会影响排序时间,但找到一个好的递增序列比较麻烦。
然后从最大的h开始,对每个h,用插入排序将其所有子数组独立地排序。
然后将h改为其序列中更小一个的值。重复。
直至最后h为1时就是一次插入排序。
这个过程中h从大到小,子数组由短变长,逐渐变得有序。
评估
希尔排序高效的原因是它权衡了子数组的规模和有序性。
从最开始的规模十分小但不太有序,到最后虽然规模大但十分有序。这两点不同时很差时使用插入排序不会很差。
- 优点
对于较大数组插入排序不适合,但希尔排序就可以。
且除非数组十分大,否则希尔排序比之后的复杂排序效率低不了太多。
归并排序
思路
利用分治思想,把两个有序的数组合并起来。
评估
比较次数为 1 2 N l g N \frac{1}{2}NlgN 21NlgN ~ N l g N NlgN NlgN
- 优点
最差的时间复杂度也是 O ( N l o g N ) O(NlogN) O(NlogN) - 缺点
空间复杂度为 O ( N ) O(N) O(N)
归并排序分为两种:
- 自顶向下归并
所谓自顶向下,就是从整个数组不断划分到最小的数组,然后递归地归并回去。 - 自底向上归并
所谓自底向上,就是从最小的数组开始两两归并,然后四四归并,直到整体有序。
这种归并代码量较小,且比较适合链表存储的数据(因为是按序访问,而非随机访问)。
快速排序
思路
也使用分治的思想,选择一个元素作为切分点,保证其左侧所有元素都小于等于它,右侧元素都大于等于它。
然后分别对其最侧和右侧两个数组进行排序。
评估
最多需要 N 2 2 \frac{N^2}{2} 2N2次比较,但概率极小,平均只需 N l n N NlnN NlnN次比较。
- 优点
快,且是原地排序,不太需要额外空间。 - 改进
对于有大量重复元素的数组,不用二分而采用三向切分。
所谓三向切分是指,选择一个基数后将数组分为三段,额外维护两个“指针”lt和gt,使得
a r r [ l o , l t − 1 ] < b a s e = a r r [ l t , g t ] < a r r [ g t + 1 , h i ] arr[lo, lt-1] < base = arr[lt, gt] < arr[gt+1, hi] arr[lo,lt−1]<base=arr[lt,gt]<arr[gt+1,hi]
这样可以使得相等的元素不会被递归处理,从而大幅度提高效率,使得时间复杂度减少到 O ( N ) O(N) O(N)。
堆排序
二叉堆
二叉堆这个数据结构是一组能够用堆有序2的完全二叉树排序的元素。
当某个结点比起父结点更大时就需要进行上浮操作,反之则需要下沉。
思路
首先将需要排序的数组构建为堆有序的。
然后利用堆的有序性进行排序(过程类似于选择排序)。
- 构造
最简单的构造方法是从首元素开始对所有元素进行上浮操作。
但由于数组是一颗完全二叉树,即数组中后一半的元素都是叶子结点,因此可以从中间 k = N 2 k=\frac{N}{2} k=2N处开始,对前一半所有元素进行下沉操作。这样就节省了一半的时间。 - 排序
取出最大元素(即数组首元素),于最末尾元素交换,然后将新首部下沉以修复堆。
将待排序数组大小减1并重复,直到排序成功。
评估
比较次数最多为 2 N l g N + 2 N 2NlgN+2N 2NlgN+2N,其中2N来自堆的构造。交换次数是比较数的一半。
要注意构造堆的时间复杂度是 O ( N ) O(N) O(N)
- 优点
唯一能够最优地利用时间和空间的排序。
最慢的时间复杂度也是 O ( N l o g N ) O(NlogN) O(NlogN)(快速排序最差为 O ( N 2 ) O(N^2) O(N2)),且不需要额外空间(有相同最差时间复杂度的归并排序的空间复杂度为 O ( N ) O(N) O(N)) - 缺点
不能很好利用缓存。比较和交换的元素通常不相邻。
总结
算法 | 平均时间复杂度 | 最差时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|
选择排序 | O ( N 2 ) O(N^2) O(N2) | O ( N 2 ) O(N^2) O(N2) | O ( 1 ) O(1) O(1) | × |
插入排序 | O ( N 2 ) O(N^2) O(N2) | O ( N 2 ) O(N^2) O(N2) | O ( 1 ) O(1) O(1) | √ |
希尔排序 | O ( N l o g N ) O(NlogN) O(NlogN) | O ( N 2 ) O(N^2) O(N2) | O ( 1 ) O(1) O(1) | × |
归并排序 | O ( N l o g N ) O(NlogN) O(NlogN) | O ( N l o g N ) O(NlogN) O(NlogN) | O ( N ) O(N) O(N) | √ |
快速排序 | O ( N l o g N ) O(NlogN) O(NlogN) | O ( N 2 ) O(N^2) O(N2) | O ( l o g N ) O(logN) O(logN) | × |
堆排序 | O ( N l o g N ) O(NlogN) O(NlogN) | O ( N l o g N ) O(NlogN) O(NlogN) | O ( 1 ) O(1) O(1) | × |
快速排序的空间复杂度来自于递归调用。