求数组第k大或前k大问题
想借用此类问题深入理解快排特性。先给结论,再具体展示。
快排具有的特性:
- 快排在每轮排完,能使pivot元素置于最终排序结果的位置上;
- 对于正序排序,每轮排完的pivot元素的左边的元素均小于pivot,位于pivot右边的元素均大于pivot。(逆序排序正好相反)
- 快速排序属于交换排序,交换排序除了快排还有冒泡排序,但是冒泡是稳定的排序算法,但快排是不稳定的!因此不能用原有的快排思想解决要求具有稳定特性的问题(如:有序的全排列问题);
这里有两点要说明:
- 所谓稳定性是指:如果对于原无序数组有两个相同大小的值,排完序后,这两个元素的相对顺序没有发生改变,则该排序算法是稳定的;
- 还要说明的一点是,很多大厂(尤其是微软等外企)对于手写快排的要求是:还要加一些随机pivot的trick。而这实现起来,也无外乎是额外增加了三五行代码的事情(例题中有展现)。
首先来看一下快排算法:
1 |
|
我们来总结一下快排代码需要注意的点:
- 在递归代码中,只有当left < right的时候才会往下执行代码,注意这里没有等于;
- 如果默认将数组的第一个元素设为pivot,请注意pivot = nums[left] 而不是pivot = nums[0],同时在一轮交换操作完成后,还要做一个逆操作;
- 排序规则仅是在partition函数的内层循环有所区别(请注意,这里的排序规则不仅仅是正序或者逆序这两种情况,根据不同的问题条件,会抽象出不同的排序规则,可以见后面的例题);
在熟练掌握原有快排算法后,我们遇到了可以用快排来优化时间的问题时,需要思考两个问题:
- 基于快排什么样的特性,该问题可以使用快排;
- 该问题具有什么样的特点,才会想到使用快排;
一般地,前两个特性可以解决第k大或前k大问题,而通过适当改变排序规则,可以解决本质上是一个排序问题的衍生子问题。
215. 数组中的第K个最大元素
1 |
|
该问题用到了快排的第一个特性;
剑指 Offer 40. 最小的k个数
1 |
|
该问题体现了快排的第二个特性。
剑指 Offer 45. 把数组排成最小的数
1 |
|
该问题修改排序规则之后利用了快排思想。
973. 最接近原点的 K 个点
1 |
|
该问题修改了排序规则,同时体现了快排的前两个特性。
延伸为一种交换思想来优化问题的解决方案:
46. 全排列
1 |
|
1 |
|
- 使用交换思想,定住一个位置的元素,写出来的代码可以优化空间;
- 对于要求按顺序输出全排列的问题,该交换思想并不能保证是有序的。