排序算法
面向各种排序算法,
基本:【冒泡(Bubble),插入(Insertion)】;
常用:【归并(Merge),快速(Quick),拓扑(Topological)】;
其他:【堆(Heap)、桶(Bucket)】;
归并、快速、拓扑的思想是解决绝大部分排序问题的关键,堆和桶排序经常可以在线性时间复杂度内解决问题。
冒泡
实现
1 | void sort(int[] nums) { |
分析
空间复杂度
假设数组的元素个数是 n,由于在整个排序的过程中,我们是直接在给定的数组里面进行元素的两两交换,所以空间复杂度是 O(1)。
时间复杂度
给定的数组按照顺序已经排好
- 在这种情况下,我们只需要进行 n−1 次的比较,两两交换次数为 0,时间复杂度是 O(n)。这是最好的情况。
给定的数组按照逆序排列
- 在这种情况下,我们需要进行 n(n-1) / 2 次比较,时间复杂度是 O($n^2$)。这是最坏的情况。
给定的数组杂乱无章
- 在这种情况下,平均时间复杂度是 O($n^2$)。
由此可见,冒泡排序的时间复杂度是 O($n^2$)。它是一种稳定的排序算法。(稳定是指如果数组里两个相等的数,那么排序前后这两个相等的数的相对位置保持不变。)
插入
实现
1 | void sort(int[] nums) { |
分析
空间复杂度
假设数组的元素个数是 n,由于在整个排序的过程中,是直接在给定的数组里面进行元素的两两交换,空间复杂度是 O(1)。
时间复杂度
给定的数组按照顺序已经排好
- 只需要进行 n-1 次的比较,两两交换次数为 0,时间复杂度是 O(n)。这是最好的情况。
给定的数组按照逆序排列
- 在这种情况下,我们需要进行 n(n-1) / 2 次比较,时间复杂度是 O($n^2$)。这是最坏的情况。
给定的数组杂乱无章
- 在这种情况下,平均时间复杂度是 O($n^2$)。
由此可见,和冒泡排序一样,插入排序的时间复杂度是 O($n^2$),并且它也是一种稳定的排序算法。
E.g.: LeetCode 147 链表插入排序
归并
基本思想:分治
不断分组到很小的数组,然后进行排序合并
实现
1 | void sort(int[] A, int lo, int hi) { |
分析
空间复杂度
由于合并 n 个元素需要分配一个大小为 n 的额外数组,合并完成之后,这个数组的空间就会被释放,所以算法的空间复杂度就是 O(n)。归并排序也是稳定的排序算法。
时间复杂度
归并算法是一个不断递归的过程。
举例:数组的元素个数是 n,时间复杂度是 T(n) 的函数。
解法:把这个规模为 n 的问题分成两个规模分别为 n/2 的子问题,每个子问题的时间复杂度就是 T(n/2),那么两个子问题的复杂度就是 2×T(n/2)。当两个子问题都得到了解决,即两个子数组都排好了序,需要将它们合并,一共有 n 个元素,每次都要进行最多 n-1 次的比较,所以合并的复杂度是 O(n)。由此我们得到了递归复杂度公式:T(n) = 2×T(n/2) + O(n)。
对于公式求解,不断地把一个规模为 n 的问题分解成规模为 n/2 的问题,一直分解到规模大小为 1。如果 n 等于 2,只需要分一次;如果 n 等于 4,需要分 2 次。这里的次数是按照规模大小的变化分类的。
以此类推,对于规模为 n 的问题,一共要进行 log(n) 层的大小切分。在每一层里,我们都要进行合并,所涉及到的元素其实就是数组里的所有元素,因此,每一层的合并复杂度都是 O(n),所以整体的复杂度就是 O(nlogn)。
快速排序
采用了分治的思想