### 问题
有一个整数数组,请你根据快速排序的思路,找出数组中第K大的数。
给定一个整数数组a,同时给定它的大小n和要找的K(K在1到n之间),请返回第K大的数,保证答案存在。
输入样例
[1,3,5,2,2],5,3
输出样例
2
JavaCode
import org.junit.Test;
public class FindKth {
public int findKth(int[] a, int n, int K) {
return find(a, 0, n-1, K);
}
//递归寻找数组中第K大的元素
private int find(int[] a, int low, int high, int K) {
int pivot = partition(a, low, high);
if(pivot + 1 < K)//中轴位置少于K个,在右子数组中继续查找
return find(a, pivot+1, high, K);
else if(pivot + 1 > K)//中轴位置大于K个,在左子数组中继续查找
return find(a, low, pivot-1, K);
else//中轴元素正好是第K大的元素
return a[pivot];
}
//将数组划分为两部分,左边较大,右边较小
private int partition(int[] a, int low, int high) {
// 将数组首元素作为每一轮比较的基准
int pivotValue = a[low];
while (low < high) {
// 从右往左扫描,直到遇到比基准元素小的元素
while (low < high && a[high] <= pivotValue)
--high;
// 将右子数组中不合格的元素放到左边不合格元素的位置(原元素已经移走)
a[low] = a[high];
// 从左往右扫描,直到遇到比基准元素大的元素
while (low < high && a[low] >= pivotValue)
++low;
// 将左子数组中不合格的元素放到左边不合格元素的位置(原元素已经移走)
a[high] = a[low];
}
// 将基准元素放到中间位置
a[low] = pivotValue;
// 返回数组的中轴位置
return low;
}
@Test
public void test() {
int[] a = new int[]{1,2,3,3,4,5,6,6};
int n=8;
int k=6;
System.out.println(findKth(a, n, k));
}
}
说明
数组的K-th问题可以等价于top-K问题,其最典型的解法之一就是利用快排进行“剪枝“,因为快排在每一轮排序之后,能够将数组分成左右两部分,且左边部分的所有元素都大于右边部分的所有元素(假设是降序排列),每次都可以直接砍掉一部分元素,从而在下一轮排序中不需要考虑那些被砍掉的元素了,大大地较少了不必要的元素比较和交换。另外,如果要求最小的K个元素,这也可以采用类似的做法。常见排序算法的介绍可以参考排序算法代码总结。
由于快排的效率是不确定的,其时间复杂度最理想情况下是 O(nlogn) ,而最差为O(n^2)。后来大牛们提出了BFPRT算法,通过精心设计的 pivot 选取方法来改进快排,使得在最坏情形下也能保证线性时间复杂度!具体介绍可以参考NOALGO博客;
top-K问题另一种典型的解法是使用堆排序,可以始终维护一个K个节点的小根堆,K个节点就是当前已知的最大的K个元素,然后遍历数组中剩余的元素,如果大于根节点,则用这个元素替换掉根节点,再调整堆使其重新成为小根堆,这种解法的复杂度为O(nlogk),且适合于海量数据的外部排序。另外,top-K问题还可以采用部分选择排序等方法,具体的分析和讨论可以参考程序员编程艺术:第三章、寻找最小的k个数。
昨天的网易内推在线笔试题中有一道问答题就是考察的top-K问题。具体问题是:网易云音乐需要统计在3小时、1天、1周时间内被用户播放次数最多的K首歌曲,请设计相应的数据结构和数据库模型。这是一道非常不错的应用题,但我当时答得不太好,顺便再吐槽一下牛客网的考试系统!后来想想,我觉得这题不仅考察了top-K问题,还考察了基于top-K的应用拓展。对于最基础的每3小时的top-K歌曲榜单,可以采用上述的方式解决,对于1天的top-K歌曲榜单,我们可以直接基于已经统计好的当天的8张top-K榜单进行合并排序,也就是从8*K个中再选出前K个元素,这样数据量就少了很多了,但是需要注意这里的8张基础榜单中肯定会有重复元素,所以我们需要对不同时段榜单中的同名歌曲的播放次数先进行合并,这可以使用HashMap来实现;类似的,对于1周的top-K歌曲榜单,我们可以基于一周之内的7张top-K榜单进行合并排序。数据库设计方面,只需要每3小时统计一份完整的歌曲播放记录表,待排序之后得到一张top-K榜单并持久化到数据库中,这个时间段内完整的歌曲播放记录表就可以删除了,类似的,可以每天(周)统计、排序并记录一张当天(周)的top-K榜单。