思考
- 为什么插入排序比冒泡排序更受欢迎?
- 如何用快排思想在O(n)内查找第K大元素?
- 如何根据年龄给100万用户数据排序?(线性排序)
- 如何实现一个通用的、高性能的排序函数?(排序优化)
前言
我最近在系统整理一些 Java 后台方面的面试题和参考答案,有找工作需求的童鞋,欢迎关注我的 Github 仓库,如果觉得不错可以点个 star 关注 :
如何分析一个排序算法?
是否原地排序 | 是否稳定 | 最好、最坏、平均时间复杂度 | 空间复杂度 | 是否基于比较 | |
---|---|---|---|---|---|
冒泡排序 | 是 | 是 | O(n)、 O(n^2)、 O(n^2) | O(1) | 是 |
插入排序 | 是 | 是 | O(n)、 O(n^2)、 O(n^2) | O(1) | 是 |
选择排序 | 是 | 否 | O(n^2)、 O(n2)、O(n2) | O(1) | 是 |
希尔排序 | 是 | 否 | O(n)、O(n2)、O(n1.3) | O(1) | 是 |
快速排序 | 是 | 否 | O(nlogn)、O(n^2)、O(nlogn) | O(logn)~O(n) | 是 |
归并排序 | 否 | 是 | O(nlogn)、O(nlogn)、O(nlogn) | O(n) | 是 |
计数排序 | 否 | 是 | O(n+k)、O(n+k)、O(n+k),k 是数据范围 | O(n+k) | 否 |
桶排序 | 否 | 是 | O(n)、O(n)、O(n) | O(N+M),N表示待排数据个数,M表示桶个数 | 否 |
基数排序 | 否 | 是 | O(nk)、O(nk)、O(n*k),k 是维度 | O(n+k) | 否 |
堆排序 | 是 | 否 | O(nlogn)、O(nlogn)、O(nlogn) | O(1) | 是 |
排序算法的执行效率
- 1、最好情况、最坏情况、平均情况时间复杂度
- 2、时间复杂度的系数、常数 、低阶
- 时间复杂度反映数据规模 n 很大时的一个增长趋势,常忽略系数、常数、低阶
- 实际软件开发中,数据规模可能比较小,比较同一阶时间复杂度的排序算法时,不能忽略系数、常数、低阶
- 3、比较次数和交换(或移动)次数
- 基于比较的算法会涉及两种操作:元素比较大小、元素交换或移动
- 分析排序算法执行效率时,应该要考虑比较次数(或移动)次数
排序算法的内存消耗
- 原地排序(Sorted in place):特指空间复杂度为 O(1) 的排序算法
- 算法的内存消耗可以通过空间复杂度来衡量
排序算法的稳定性
- 稳定的排序算法: 指待排序的序列中存在值相等的元素时,经过排序后,相等元素之间原有的先后顺序不变
如何选择合适的排序算法?
- 对小规模数据进行排序,可以选择时间复杂度是 O(n^2) 的算法
- 对大规模数据进行排序,时间复杂度是 O(nlogn) 的算法更加高效
- 为了兼顾任意规模数据的排序,一般都会首选时间复杂度是 O(nlogn) 的排序算法来实现排序函数
如何优化快速排序?
- 快速排序出现 O(n^2)时间复杂度的主要原因是因为分区节点选择不够合理
- 最理想的分区节点是:被分区点分开的两个分区中,数据的数量差不多
- 方法
- 1、三数取中法(五数取中、十数取中等):从区间的首、尾、中间,分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点
- 2、随机法:每次从要排序的区间中,随机选择一个元素作为分区点
- 3、等等
解答思考题
-
1. 为什么插入排序比冒泡排序更受欢迎?
从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。
// 冒泡排序中数据的交换操作: if (a[j] > a[j+1]) { // 交换 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; } // 插入排序中数据的移动操作: if (a[j] > value) { a[j+1] = a[j]; // 数据移动 } else { break; }
-
2. 如何用快排思想在O(n)内查找第K大元素?
比如,4, 2, 5, 12, 3 这样一组数据,第 3 大元素就是 4。
我们选择数组区间 A[0…n-1] 的最后一个元素 A[n-1] 作为 pivot,对数组 A[0…n-1] 原地分区,这样数组就分成了三部分,A[0…p-1]、A[p]、A[p+1…n-1]。
如果 p+1=K,那 A[p] 就是要求解的元素;如果 K>p+1, 说明第 K 大元素出现在 A[p+1…n-1] 区间,我们再按照上面的思路递归地在 A[p+1…n-1] 这个区间内查找。同理,如果 K<p+1,那我们就在 A[0…p-1] 区间查找。
我们再来看,为什么上述解决思路的时间复杂度是 O(n)?
第一次分区查找,我们需要对大小为 n 的数组执行分区操作,需要遍历 n 个元素。第二次分区查找,我们只需要对大小为 n/2 的数组执行分区操作,需要遍历 n/2 个元素。依次类推,分区遍历元素的个数分别为、n/2、n/4、n/8、n/16.……直到区间缩小为 1。
如果我们把每次分区遍历的元素个数加起来,就是:n+n/2+n/4+n/8+…+1。这是一个等比数列求和,最后的和等于 2n-1。所以,上述解决思路的时间复杂度就为 O(n)。 -
3. 如何根据年龄给100万用户数据排序?(线性排序)
假设年龄的范围最小 1 岁,最大不超过 120 岁。可以遍历这 100 万用户,根据年龄将其划分到这 120 个桶里,然后依次顺序遍历这 120 个桶中的元素。这样就得到了按照年龄排序的 100 万用户数据。
-
4. 如何实现一个通用的、高性能的排序函数?(排序优化)
1.数据量不大时,可以采取用时间换空间的思路
2.数据量大时,优化快排分区点的选择
3.防止堆栈溢出,可以选择在堆上手动模拟调用栈解决
4.在排序区间中,当元素个数小于某个常数是,可以考虑使用O(n^2)级别的插入排序
5.用哨兵简化代码,每次排序都减少一次判断,尽可能把性能优化到极致
参考链接
- 《数据结构与算法之美》专栏 王争