寻找最大的K个数

问题描述

在面试中,有下面的问答:

问:有很多个无序的数,我们姑且假定它们各不相等,怎么选出其中最大的若干个数呢?

答:可以这样写:int array[100] ……

问:好,如果有更多的元素呢?

答:那可以改为:int array[1000] ……

问:如果我们有很多元素,例如1亿个浮点数,怎么办?

答:个,十,百,千,万……那可以写:float array [100 000 000] ……

问:这样的程序能编译运行么?

答:嗯……我从来没写过这么多的0 ……

分析与解法

【解法一】

  当学生们信笔写下float array [10000000],他们往往没有想到这个数据结构要如何在电脑上实现,是从当前程序的栈(Stack)中分配,还是堆(Heap),还是电脑的内存也许放不下这么大的东西?

  我们先假设元素的数量不大,例如在几千个左右,在这种情况下,那我们就排序一下吧。在这里,快速排序或堆排序都是不错的选择,他们的平均时间复杂度都是O(N * log2N)。然后取出前K个,O(K)。总时间复杂度O(N * log2N)+ O(K) = O(N * log2N)。

  你一定注意到了,当K=1时,上面的算法也是O(N * log2N)的复杂度,而显然我们可以通过N-1次的比较和交换得到结果。上面的算法把整个数组都进行了排序,而原题目只要求最大的K个数,并不需要前K个数有序,也不需要后N-K个数有序。

  怎么能够避免做后N-K个数的排序呢?我们需要部分排序的算法,选择排序和交换排序都是不错的选择。把N个数中的前K大个数排序出来,复杂度是O(N * K)。

  那一个更好呢?O(N * log2N)还是O(N * K)?这取决于K的大小,这是你需要在面试者那里弄清楚的问题。在K(K < = log2N)较小的情况下,可以选择部分排序。

算法如下:

  1. package chapter2shuzizhimei.findMaxK;
  2. /**
  3. * 寻找最大的K个数
  4. * 【解法一】快速排序
  5. * @author DELL
  6. *
  7. */
  8. public class FindMaxK1 {
  9. //快速排序的一次划分
  10. public static int partition(float a[], int first, int last){
  11. float temp;
  12. int i,j;
  13. temp = a[first];
  14. i = first;
  15. j = last;
  16. while(i<j){
  17. while(i<j&&a[j]<=temp){
  18. j--;
  19. }
  20. if(i<j) a[i++] = a[j];
  21. while(i<j&&a[i]>=temp){
  22. i++;
  23. }
  24. if(i<j) a[j--] = a[i];
  25. }
  26. a[i] = temp;
  27. return i;
  28. }
  29. //快速排序
  30. public static void quickSort(float a[], int first, int last){
  31. if(first>=last)
  32. return;
  33. int i = partition(a, first, last);
  34. quickSort(a, first, i-1);
  35. quickSort(a, i+1, last);
  36.  
  37. }
  38. //输出最大的K个数
  39. public static void maxK(float a[], int k){
  40. if(k>a.length){
  41. System.out.println("k的值有误,不能大于数组长度!");
  42. return;
  43. }
  44. quickSort(a,0,a.length-1);
  45. System.out.print("最大的"+k+"个数为:");
  46. for(int i=0;i<k;i++){
  47. System.out.print(a[i]+" ");
  48. }
  49. System.out.println();
  50. }
  51. public static void main(String[] args) {
  52. float a[] = {9,5,4,3,5,6,7,1,3};
  53. maxK(a,3);
  54. }
  55.  
  56. }

程序运行结果如下:

  1. 最大的3个数为:9.0 7.0 6.0
  1. package chapter2shuzizhimei.findMaxK;
  2. /**
  3. * 寻找最大的K个数
  4. * 【解法一】冒泡法(不全排序)
  5. * @author DELL
  6. *
  7. */
  8. public class FindMaxK2 {
  9.  
  10. //输出最大的K个数
  11. public static void maxK(float a[], int k){
  12. if(k>a.length){
  13. System.out.println("k的值有误,不能大于数组长度!");
  14. return;
  15. }
  16. int i,j;
  17. float temp;
  18. System.out.print("最大的"+k+"个数为:");
  19. //采用冒泡排序的思想
  20. for(i=0;i<k;i++){
  21. for(j=1;j<a.length-i;j++){
  22. if(a[j]<a[j-1]){
  23. temp = a[j-1];
  24. a[j-1] = a[j];
  25. a[j] = temp;
  26. }
  27. }
  28. System.out.print(a[a.length-i-1]+" ");
  29. }
  30. }
  31. public static void main(String[] args) {
  32. float a[] = {9,5,4,3,5,6,7,1,3};
  33. maxK(a,3);
  34. }
  35.  
  36. }

程序运行结果如下:

  1. 最大的3个数为:9.0 7.0 6.0

  在下一个解法中,我们会通过避免对前K个数排序来得到更好的性能。

【解法二】

  回忆一下快速排序,快排中的每一步,都是将待排数据分做两组,其中一组的数据的任何一个数都比另一组中的任何一个大,然后再对两组分别做类似的操作,然后继续下去……

  在本问题中,假设N个数存储在数组S中,我们从数组S中随机找出一个元素X,把数组分为两部分Sa和Sb。Sa中的元素大于等于X,Sb中元素小于X。

  这时,有两种可能性:

  1. Sa中元素的个数小于K,Sa中所有的数和Sb中最大的K-|Sa|个元素(|Sa|指Sa中元素的个数)就是数组S中最大的K个数。

  2. Sa中元素的个数大于或等于K,则需要返回Sa中最大的K个元素。

  这样递归下去,不断把问题分解成更小的问题,平均时间复杂度O(N * log2K)。代码如下:

  1. package chapter2shuzizhimei.findMaxK;
  2. /**
  3. * 寻找最大的K个数
  4. * 【解法二】递归划分
  5. * @author DELL
  6. *
  7. */
  8. public class FindMaxK3 {
  9. //快速排序的一次划分
  10. public static int partition(float a[], int first, int last){
  11. float temp;
  12. int i,j;
  13. temp = a[first];
  14. i = first;
  15. j = last;
  16. while(i<j){
  17. while(i<j&&a[j]<=temp){
  18. j--;
  19. }
  20. if(i<j) a[i++] = a[j];
  21. while(i<j&&a[i]>=temp){
  22. i++;
  23. }
  24. if(i<j) a[j--] = a[i];
  25. }
  26. a[i] = temp;
  27. return i;
  28. }
  29.  
  30. //输出最大的K个数
  31. public static void maxK(float a[], int first, int last, int k){
  32. if(k>a.length){
  33. System.out.println("k的值有误,不能大于数组长度!");
  34. return;
  35. }
  36. if(k<=0)
  37. return;
  38. int i = partition(a,first,last);
  39. if(i-first+1==k){
  40. for(int j=first;j<first+k;j++){
  41. System.out.print(a[j]+" ");
  42. }
  43. }
  44. if(i-first+1>k)
  45. maxK(a,first,i,k);
  46. if(i-first+1<k){
  47. for(int j=first;j<i+1;j++){
  48. System.out.print(a[j]+" ");
  49. }
  50. maxK(a,i+1,last,k-(i-first+1));
  51. }
  52. }
  53. public static void main(String[] args) {
  54. float a[] = {9,5,4,3,5,6,7,1,3};
  55. int k = 5;
  56. System.out.print("最大的"+k+"个数为:");
  57. maxK(a,0,a.length-1,k);
  58. System.out.println();
  59. }
  60.  
  61. }

程序运行结果如下:

  1. 最大的5个数为:9.0 7.0 6.0 5.0 5.0

【解法三】

  寻找N个数中最大的K个数,本质上就是寻找最大的K个数中最小的那个,也就是第K大的数。可以使用二分搜索的策略来寻找N个数中的第K大的数。对于一个给定的数p,可以在O(N)的时间复杂度内找出所有不小于p的数。假如N个数中最大的数为Vmax,最小的数为Vmin,那么这N个数中的第K大数一定在区间[Vmin, Vmax]之间。那么,可以在这个区间内二分搜索N个数中的第K大数p。伪代码如下:

  1. while(Vmax - Vmin > delta)
  2. {
  3. Vmid = Vmin + (Vmax - Vmin) * 0.5;
  4. if(f(arr, N, Vmid) >= K)
  5. Vmin = Vmid;
  6. else
  7. Vmax = Vmid;
  8. }

  伪代码中f(arr, N, Vmid)返回数组arr[0, …, N-1]中大于等于Vmid的数的个数。

  上述伪代码中,delta的取值要比所有N个数中的任意两个不相等的元素差值之最小值小。如果所有元素都是整数,delta可以取值0.5。循环运行之后,得到一个区间(Vmin, Vmax),这个区间仅包含一个元素(或者多个相等的元素)。这个元素就是第K大的元素。整个算法的时间复杂度为O(N * log2(|Vmax - Vmin| /delta))。由于delta的取值要比所有N个数中的任意两个不相等的元素差值之最小值小,因此时间复杂度跟数据分布相关。在数据分布平均的情况下,时间复杂度为O(N * log2(N))。

完整代码如下:

  1. package chapter2shuzizhimei.findMaxK;
  2. /**
  3. * 寻找最大的K个数
  4. * 【解法三】寻找最大的K个数中最小的那个
  5. * 二分搜索策略
  6. * @author DELL
  7. *
  8. */
  9. public class FindMaxK4 {
  10. /**
  11. * 计算数组a中大于等于Vmid的数的个数
  12. * @param a
  13. * @param Vmid
  14. * @return
  15. */
  16. public static int f(int a[], double Vmid){
  17. int count=0;
  18. for(int i=0;i<a.length;i++){
  19. if(a[i]>=Vmid)
  20. count++;
  21. }
  22. return count;
  23. }
  24.  
  25. //输出最大的K个数
  26. public static void maxK(int a[], int k){
  27. if(k>a.length){
  28. System.out.println("k的值有误,不能大于数组长度!");
  29. return;
  30. }
  31. double Vmax,Vmin;
  32. double Vmid;
  33. Vmax = a[0];
  34. Vmin = a[0];
  35. //寻找数组中最大和最小的元素
  36. for(int i=1;i<a.length;i++){
  37. if(a[i]>Vmax)
  38. Vmax = a[i];
  39. if(a[i]<Vmin)
  40. Vmin = a[i];
  41. }
  42. while(Vmax-Vmin>0.5){
  43. Vmid =Vmin + (Vmax - Vmin)*0.5;
  44. if(f(a,Vmid)>=k)
  45. Vmin = Vmid;
  46. else
  47. Vmax = Vmid;
  48. }
  49. System.out.print("最大的"+k+"个数为:");
  50. for(int i=0;i<a.length;i++){
  51. if(a[i]>=Vmin)
  52. System.out.print(a[i]+" ");
  53. }
  54. System.out.println();
  55. }
  56. public static void main(String[] args) {
  57. int a[] = {9,5,4,3,5,6,7,1,3};
  58. int k = 3;
  59. maxK(a, k);
  60. }
  61.  
  62. }

程序运行结果如下:

  1. 最大的3个数为:9 6 7

  在整数的情况下,可以从另一个角度来看这个算法。假设所有整数的大小都在[0, 2m-1]之间,也就是说所有整数在二进制中都可以用m bit来表示(从低位到高位,分别用0, 1, …, m-1标记)。我们可以先考察在二进制位的第(m-1)位,将N个整数按该位为1或者0分成两个部分。也就是将整数分成取值为[0, 2m-1-1]和[2m-1, 2m-1]两个区间。前一个区间中的整数第(m-1)位为0,后一个区间中的整数第(m-1)位为1。如果该位为1的整数个数A大于等于K,那么,在所有该位为1的整数中继续寻找最大的K个。否则,在该位为0的整数中寻找最大的K-A个。接着考虑二进制位第(m-2)位,以此类推。思路跟上面的浮点数的情况本质上一样。

  对于上面两个方法,我们都需要遍历一遍整个集合,统计在该集合中大于等于某一个数的整数有多少个。不需要做随机访问操作,如果全部数据不能载入内存,可以每次都遍历一遍文件。经过统计,更新解所在的区间之后,再遍历一次文件,把在新的区间中的元素存入新的文件。下一次操作的时候,不再需要遍历全部的元素。每次需要两次文件遍历,最坏情况下,总共需要遍历文件的次数为2 * log2(|Vmax - Vmin|/delta)。由于每次更新解所在区间之后,元素数目会减少。当所有元素能够全部载入内存之后,就可以不再通过读写文件的方式来操作了。

  此外,寻找N个数中的第K大数,是一个经典问题。理论上,这个问题存在线性算法。不过这个线性算法的常数项比较大,在实际应用中效果有时并不好。

【解法四】

  我们已经得到了三个解法,不过这三个解法有个共同的地方,就是需要对数据访问多次,那么就有下一个问题,如果N很大呢,100亿?(更多的情况下,是面试者问你这个问题)。这个时候数据不能全部装入内存(不过也很难说,说知道以后会不会1T内存比1斤白菜还便宜),所以要求尽可能少的遍历所有数据。

不妨设N > K,前K个数中的最大K个数是一个退化的情况,所有K个数就是最大的K个数。如果考虑第K+1个数X呢?如果X比最大的K个数中的最小的数Y小,那么最大的K个数还是保持不变。如果X比Y大,那么最大的K个数应该去掉Y,而包含X。如果用一个数组来存储最大的K个数,每新加入一个数X,就扫描一遍数组,得到数组中最小的数Y。用X替代Y,或者保持原数组不变。这样的方法,所耗费的时间为O(N * K)。

  进一步,可以用容量为K的最小堆来存储最大的K个数。最小堆的堆顶元素就是最大K个数中最小的一个。每次新考虑一个数X,如果X比堆顶的元素Y小,则不需要改变原来的堆,因为这个元素比最大的K个数小。如果X比堆顶元素大,那么用X替换堆顶的元素Y。在X替换堆顶元素Y之后,X可能破坏最小堆的结构(每个结点都比它的父亲结点大),需要更新堆来维持堆的性质。更新过程花费的时间复杂度为O(log2K)。

图2-1是一个堆,用一个数组h[]表示。每个元素h[i],它的父亲结点是h[i/2],儿子结点是h[2 * i + 1]和h[2 * i + 2]。每新考虑一个数X,需要进行的更新操作伪代码如下:

  1. if(X > h[0])
  2. {
  3. h[0] = X;
  4. p = 0;
  5. while(p < K)
  6. {
  7. q = 2 * p + 1;
  8. if(q >= K)
  9. break;
  10. if((q < K - 1) && (h[q + 1] < h[q]))
  11. q = q + 1;
  12. if(h[q] < h[p])
  13. {
  14. t = h[p];
  15. h[p] = h[q];
  16. h[q] = t;
  17. p = q;
  18. }
  19. else
  20. break;
  21. }
  22. }

  因此,算法只需要扫描所有的数据一次,时间复杂度为O(N * log2K)。这实际上是部分执行了堆排序的算法。在空间方面,由于这个算法只扫描所有的数据一次,因此我们只需要存储一个容量为K的堆。大多数情况下,堆可以全部载入内存。如果K仍然很大,我们可以尝试先找最大的K'个元素,然后找第K'+1个到第2 * K'个元素,如此类推(其中容量K'的堆可以完全载入内存)。不过这样,我们需要扫描所有数据ceil (K/K')次。

完整代码如下:

  1. package chapter2shuzizhimei.findMaxK;
  2. /**
  3. * 寻找最大的K个数
  4. * 【解法四】
  5. * 采用堆遍历一遍数组
  6. * @author DELL
  7. *
  8. */
  9. public class FindMaxK5 {
  10.  
  11. //输出最大的K个数
  12. public static void maxK(float a[], int k){
  13. if(k>a.length){
  14. System.out.println("k的值有误,不能大于数组长度!");
  15. return;
  16. }
  17. float b[] = new float[k];
  18. float temp;
  19. //建小根堆
  20. b[0] = a[0];
  21. for(int i=1;i<k;i++){
  22. int j = i-1;
  23. if(a[i]>=b[j/2]){
  24. b[j+1]=a[i];
  25. }else{
  26. int p = j/2;
  27. int q = p/2;
  28. b[j+1]=b[p];
  29. b[p] = a[i];
  30. while(p!=0&&b[p]<b[q]){
  31. temp = b[q];
  32. b[q] = b[p];
  33. b[p] = temp;
  34. p = q;
  35. q = p/2;
  36. }
  37. }
  38. }
  39. for(int i=k;i<a.length;i++){
  40. if(a[i]>b[0]){
  41. b[0]=a[i];
  42. //调整堆
  43. int p = 0;
  44. while(p<k){
  45. int q = 2*p+1;
  46. if(q>=k)
  47. break;
  48. if(q<k-1&&b[q+1]<b[q])
  49. q = q+1;
  50. if(b[p]>b[q]){
  51. temp = b[p];
  52. b[p] = b[q];
  53. b[q] = temp;
  54. p=q;
  55. }else{
  56. break;
  57. }
  58. }
  59. }
  60. }
  61. System.out.print("最大的"+k+"个数为:");
  62. for(int i=0;i<k;i++){
  63. System.out.print(b[i]+" ");
  64. }
  65. System.out.println();
  66. }
  67. public static void main(String[] args) {
  68. float a[] = {9,5,4,3,5,6,7,1,3};
  69. int k = 5;
  70. maxK(a, k);
  71. }
  72.  
  73. }

程序运行结果如下:

  1. 最大的5个数为:5.0 6.0 5.0 9.0 7.0

【解法五】

  上面类快速排序的方法平均时间复杂度是线性的。能否有确定的线性算法呢?是否可以通过改进计数排序、基数排序等来得到一个更高效的算法呢?答案是肯定的。但算法的适用范围会受到一定的限制。

  如果所有N个数都是正整数,且它们的取值范围不太大,可以考虑申请空间,记录每个整数出现的次数,然后再从大到小取最大的K个。比如,所有整数都在(0, MAXN)区间中的话,利用一个数组count[MAXN]来记录每个整数出现的个数(count[i]表示整数i在所有整数中出现的个数)。我们只需要扫描一遍就可以得到count数组。然后,寻找第K大的元素:

代码清单2-14

  1. for(sumCount = 0, v = MAXN - 1; v >= 0; v--)
  2. {
  3. sumCount += count[v];
  4. if(sumCount >= K)
  5. break;
  6. }
  7. return v;

  极端情况下,如果N个整数各不相同,我们甚至只需要一个bit来存储这个整数是否存在。

  当实际情况下,并不一定能保证所有元素都是正整数,且取值范围不太大。上面的方法仍然可以推广适用。如果N个数中最大的数为Vmax,最小的数为Vmin,我们可以把这个区间[Vmin, Vmax]分成M块,每个小区间的跨度为d =(Vmax - Vmin)/M,即 [Vmin, Vmin+d], [Vmin + d, Vmin + 2d],……然后,扫描一遍所有元素,统计各个小区间中的元素个数,跟上面方法类似地,我们可以知道第K大的元素在哪一个小区间。然后,再对那个小区间,继续进行分块处理。这个方法介于解法三和类计数排序方法之间,不能保证线性。跟解法三类似地,时间复杂度为O((N+M)* log2M(|Vmax - Vmin|/delta))。遍历文件的次数为2 * log2M(|Vmax - Vmin|/delta)。当然,我们需要找一个尽量大的M,但M取值要受内存限制。

  在这道题中,我们根据K和N的相对大小,设计了不同的算法。在实际面试中,如果一个面试者能针对一个问题,说出多种不同的方法,并且分析它们各自适用的情况,那一定会给人留下深刻印象。

  注:本题目的解答中用到了多种排序算法,这些算法在大部分的算法书籍中都有讲解。掌握排序算法对工作也会很有帮助。

第2章 数字之魅——寻找最大的K个数的更多相关文章

  1. 03寻找最小的k个数

    题目描述:查找最小的k个元素         题目:输入n个整数,输出其中最小的k个.         例如输入1,2,3,4,5,6,7和8这8个数字,则最小的4个数字为1,2,3和4. 1:最简单 ...

  2. 算法系列:寻找最大的 K 个数

    Copyright © 1900-2016, NORYES, All Rights Reserved. http://www.cnblogs.com/noryes/ 欢迎转载,请保留此版权声明. -- ...

  3. 算法练习:寻找最小的k个数

    参考July的文章:http://blog.csdn.net/v_JULY_v/article/details/6370650 寻找最小的k个数题目描述:查找最小的k个元素题目:输入n个整数,输出其中 ...

  4. 编程之美2.5:寻找最大的K个数

    编程之美2.5:寻找最大的K个数 引申:寻找第k大的数: 方法一: // 选择第k大的数(通过改进快速排序来实现) public static void SelectShort(int[] array ...

  5. O(N)的时间寻找最大的K个数

    (转:http://www.cnblogs.com/luxiaoxun/archive/2012/08/06/2624799.html) 寻找N个数中最大的K个数,本质上就是寻找最大的K个数中最小的那 ...

  6. 寻找最大的k个数问题

    这是编程之美书第2.5节的一道题目. 各种解法: 解法一,用nlgn复杂度的排序算法对数组进行从大到小排序,取前K个.但这方法做了两件不必要做的事:它对想得到的K个数进行了排序,对不想得到的n-K个数 ...

  7. 编程之法:面试和算法心得(寻找最小的k个数)

    内容全部来自编程之法:面试和算法心得一书,实现是自己写的使用的是java 题目描述 输入n个整数,输出其中最小的k个. 分析与解法 解法一 要求一个序列中最小的k个数,按照惯有的思维方式,则是先对这个 ...

  8. 【编程之美】2.5 寻找最大的k个数

    有若干个互不相等的无序的数,怎么选出其中最大的k个数. 我自己的方案:因为学过找第k大数的O(N)算法,所以第一反应就是找第K大的数.然后把所有大于等于第k大的数取出来. 写这个知道算法的代码都花了2 ...

  9. 寻找最大的K个数(下)

    接着昨天的写,里面的代码包含昨天的 #include <iostream> using namespace std; #define N 50 //初始化数组 , , , , , , , ...

随机推荐

  1. 白盒测试之gmock入门篇

    一.gmock是什么 gmock是google公司推出的一款开源的白盒测试工具.gmock是个很强大的东西,测试一个模块的时候,可能涉及到和其他模块交互,可以将模块之间的接口mock起来,模拟交互过程 ...

  2. 关于FireFox下 CSS3 transition 与其他浏览器的差异

    最近一个项目,动画效果全靠CSS3来做,用得比较多的transition,发现了一点火狐与其他浏览器的小差异. 首先我们写CSS的时候,一般为属性值为0的属性,我们一般会这样写 #id{ posito ...

  3. Tkinter教程之Text(2)篇

    本文转载自:http://blog.csdn.net/jcodeer/article/details/1811347 '''Tkinter教程之Text(2)篇''''''6.使用tag来指定文本的属 ...

  4. Python字典方法copy()和deepcopy()的区别

    from copy import deepcopy # import deepcopy模块 d = {} d['name'] = ['black', 'guts'] # d = {'name': [' ...

  5. Google App Engine Deployment 相关问题

    1.GAE instance上传成功之后可以正常运行的情况下.在Google账户中“关联的应用和网站”,撤销Google App Engine appcfg对Google账户的访问权限. 再次上传时, ...

  6. 新手指导:教你如何查看识别hadoop是32位还是64位

    问题导读: 1.从哪些地方可以识别hadoop是32位还是64位?2.hadoop本地库在什么位置? 来源:about云 本文链接:http://www.aboutyun.com/thread-127 ...

  7. 内核参数优化/etc/sysctl.conf

    net.nf_conntrack_max = 65536000net.netfilter.nf_conntrack_tcp_timeout_established = 1200net.ipv4.tcp ...

  8. PLSQL存储过程校验身份证

    CREATE OR REPLACE FUNCTION FUN_CHECKIDCARD(PI_AAC002 VARCHAR2)   RETURN VARCHAR2 IS /*************** ...

  9. Java IO (1) - InputStream

    Java IO (1) - InputStream 前言 JavaIO一共包括两种,一种是stream,一种是reader/writer,每种又包括in/out,所以一共是四种包.Java 流在处理上 ...

  10. Javascript/Jquery——简单定时器的多种实现方法

    第一种方法: <script language="javascript"> //使用setInterval间歇调用 (不建议使用该方法) $(function(){ s ...