1 前言
转自作者huan-yong
原文链接 top-K问题详解
腾讯面试被面试官“羞辱”之后,脸红如饮酒,抓紧时间学一波。
2 思路解析
什么是top-k问题
top-K 问题是一类经典的问题,它能解决许多海量数据处理相关的问题,例如在1亿个数据中找出访问次数前1000的热点数据,在海量搜索字符串中找出搜索频率排在前十的搜索字符串等等。
思路有哪些
我们可以将这类问题分为三个方向考虑:
1.将输入内容(假设用数组存放)进行 完全排序
,从中选出排在前K的元素即为所求。有了这个思路,我们可以选择相应的排序算法进行处理,目前来看 快速排序
, 堆排序
和 归并排序
都能达到O(nlogn)的时间复杂度。
2.对输入内容进行 部分排序
,即只对前K大的元素进行排序(这K个元素即为所求)。此时我们可以选择冒泡排序或选择排序进行处理,即每次冒泡(选择)都能找到所求的一个元素。这类策略的时间复杂度是O(Kn)。
3.对输入内容 不进行排序
,显而易见,这种策略将会有更好的性能开销。我们此时可以选择两种策略进行处理:
-
利用小根堆维护一个大小为K的数组,目前该小根堆中的元素是排名前K的数,其中根是最小的数。此后,每次从原数组中取一个元素与根进行比较,如大于根的元素,则将根元素替换并进行堆调整(下沉),即保证小根堆中的元素仍然是排名前K的数,且根元素仍然最小;否则不予处理,取下一个数组元素继续该过程。该算法的时间复杂度是O(nlogK),一般来说企业中都采用该策略处理top-K问题,因为该算法不需要一次将原数组中的内容全部加载到内存中,而这正是海量数据处理必然会面临的一个关卡。
-
利用快速排序的分划函数找到分划位置K,则其前面的内容即为所求。该算法是一种非常有效的处理方式,时间复杂度是O(n)(证明可以参考算法导论书籍)。对于能一次加载到内存中的数组,该策略非常优秀。
代码
快排思想
public class Main {
public static void topK(int[] a,int k)
{
int len = a.length;
if(len <= k) //数组元素个数小于k,则不需要处理
return ;
int low = 0;
int high = len;
int j = partition(a,low,high); //找到划分位置j
while(j!=k) //划分位置不是k则继续处理
{
if(k > j) //k在分划点后面部分
low = j+1;
else
high = j; //k在分划点前面部分
j = partition(a,low,high);
}
}
public static int partition(int[] a,int low,int high) //分划函数
{
if(high <= low)
return low;
int i=low;
int j=high;
while(i<j)
{
i++;
while(i<high && a[i]>a[low]) //从前往后找小于等于a[low]的元素
i++;
j--;
while(j>=low && a[j]<a[low]) //从后往前找大于等于a[low]的元素
j--;
if(i<j) //交换
{
int swap = a[i];
a[i] = a[j];
a[j] = swap;
}
}
int swap = a[low];
a[low] = a[j];
a[j] = swap;
return j; //返回分划点
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[] a = new int[n];
for(int i=0;i<n;i++) //输入数组
a[i] = scan.nextInt();
int k = scan.nextInt(); //输入K
if(n>k)
topK(a,k);
if(k>=n)
k = n;
for(int i=0;i<k;i++) //输出前K大的数
System.out.print(a[i]+" ");
System.out.println();
}
}