在100W个数中获得最大/小的50个数

来源:互联网 发布:飞鱼直播软件下载 编辑:程序博客网 时间:2024/05/24 07:25
        前年有个朋友在南大考研复试时被老师一个题目问住了,“如何在100W个数中选最大/小的50个?”当时他的回答是快速排序。那么快排的平均比较次数是m*nlogn(log以2为底,m是个常数),所以总共的比较次数大约是100W*20m(2的10次方大约为1000,所以2的20次方大约是100W)。
        当时我的想法是用插入排序维护1个容量为50的数组。这个数组保存着最小的50个数,每次比较都把最大的那个数给抛弃。当随着比较次数的增多,从概率上讲,越到后面的元素越有可能只需比较1次(因为这个数会比数组中最大的数更大)。但因为这个涉及到概率问题,所以很难计算平均比较次数。但可以算得最坏比较次数大约是50*100W,即5000W次。当时也懒得实验一下我的想法效果如何,就没继续实验了。
        后来,有个两个同学告诉我正确解法应该是堆排序。构建好初始堆后每次取出最小的数最多只需log(n)的比较,所以总共比较次数不会大于m*50*log(100W),即m*50*20=1000次(其实最后一次取出最小元素无需再维护堆这个数据结构,实际上应该是49*20*m次)。但是他们忽略了一点,构建初始堆的复杂度为O(N),所以实际上总的比较次数应该是s*100W+1000*m(s和m是常数)。
        当然,如果直接暴力点用选择排序的变形也是可以的。用选择排序直接选出最小的50个,比较次数为(100w-1)+(100w-2)+...+100w(-50),省略后面一堆尾巴,最后比较次数大约是5000w次。
         前段时间由于需要完成一个TOP K的功能,所以封装了一个TOP K的数据结构(class TopK<T>,即用插入排序维护一个容量为K的数组,这个数组保存的是最大或最小的K个元素。T是泛型)。今天正好来验证我当时的想法。不过效果很不理想,我猜测是频繁类型转换以及拆箱装箱导致的。(使用时只能用TopK<Integer>。由于JAVA的泛型和C++以及C#的泛型不同,JAVA的泛型其实就是Object+自动类型转换)。最后不使用封装重新写了一遍这个数据结构,效果好了许多。
        至于堆排序,本来使用JAVA自带的优先队列PriorityQueue
。效果差得令人发指。我稍微看了下源代码,也没搞懂为什么会这么慢。感觉即使是频繁的类型转换和拆箱装箱也不应该差这么多。最后从网上找了堆排序,效果挺好。(我一开始就指定了优先队列的容量,所以不是扩容的问题)
    以下是实验结果,第一幅图是100W数据的实验结果,第二幅是1000W数据的实验结果。





快速排序用的是JAVA自带的Arrays.sort。效果不好可能是频繁的递归以及赋值花销过大。实验证明,我的算法比较次数大概是1.015*n,而堆排序大概是1.882*n。至于交换次数,我的方法比堆排序不在一个数量级上。
     


0 0