欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

10亿条数据找出最大的1000个数

程序员文章站 2024-03-15 22:30:54
...

方法1:全部排序(适合大内存)

先利用快拍全部排序,在提取最大的1000个数。单机+单核+足够大内存复杂度O(nlogn)。

方法2:局部淘汰法

先排序个10000个数据maxs数组,然后之后的数据和最小的相比较,大于最小的说明之前的最小值不是最大的10000个数。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即10000。在maxs数组插入新的数据之后的排序必须要优化,否则会造成很大的不必要的排序。

方法3:分治法 (适合小内存)

将一亿个数据分为100分,没分100万数据。依次取每组的前10000大的数据,最后通过分治进行merge操作。

方法4:Hash法

没接触过

方法5:堆排序(最优)

思想类似于方法2,构建小根堆,剩下的元素和最小的(堆顶)进行比较,如果小于最小的,说明肯定不是最大的10000个数。 为什么要构建小根堆? 如果是大根堆的话,与堆顶元素的比较,不管是大于还是小于,都难以取舍,既不能判断该数是否是10000个最大数之一。

show you the code //快排的代码见第6题

package a_a_problems;

import Utils.ArrayUtils;
import g_quicksortupdate.QuickSort;
import g_quicksortupdate.QuickSort2;
import g_quicksortupdate.QuickSort33;

import java.util.Arrays;

/**
 * Created by tangwc on 2017/8/20.
 */
public class Top10000 {

    /**
     * 方法1:利用快排全部排序,内存要求高,单线程
     */
    public static int[] top1000AllSort(int[]arr){
        QuickSort33.quickSort(arr);

        return Arrays.copyOf(arr, 10000);
    }

    /**
     * 方法2:先找10000个数排序,之后的数与其最小的数进行比较,
     * 如果小于则跳过,大于则替换,再排序。遍历完之后剩下的10000size的数组就是最大数组
     */
    public static int[] top1000First10000(int[] arr){
        int index = 10000;
        int[] maxs = Arrays.copyOf(arr, index);
        QuickSort33.quickSort(maxs);//利用快排先将10000有序方便查找最小值与之后的数进行比较
        for(int i = 10000;i<arr.length;i++) {
            if(arr[i]>maxs[0]){
                maxs[0] = arr[i];
                //QuickSort33.quickSort(maxs);//如果用快排将会耗费大量的时间比较原本有序的序列,应该用优化过的插入排序
            }
        }
        return maxs;
    }
    //方法2的改进,提高性能
    public static int[] top1000First10000_2(int[] arr){
        int index = 10000;
        int[] maxs = Arrays.copyOf(arr, index);
        QuickSort33.quickSort(maxs);
        for(int i = 10000;i<arr.length;i++) {
            if(arr[i]>maxs[0]){
                maxs[0] = arr[i];
                insertSort(maxs);//改进后的插入排序
            }
        }
        return maxs;
    }
    //传过来的除了第一个数之外都有序
    private static void insertSort(int[] arr){
        for(int i = 0;i<arr.length-1;i++){
            if(arr[i]>arr[i+1]){
                int temp = arr[i];
                arr[i] = arr[i+1];
                arr[i+1] = temp;
            }else
                break;
        }
    }

    /**
     * 方法3:分治的思想,分为100组,每组100万条,取每组的前10000条maxs,进行merge,最后返回的maxs就是最大的10000个数
     * 与方法1相比,1亿条数据排序效率低于100个100万数据的排序。 从结果来看,效率并没有提高,可能是快排还可以进一步优化。
     */
    public  static int[] top10000partition(int[]arr){
        return fun(arr,0,arr.length-1);
    }

    private static int[] fun(int[]arr,int left,int right){
        if (left + 1000000 <= right) {//100万返回前10000大的数组
            QuickSort2.quickSort2(arr,left,right);
            int l = right-left;
            int[] m = new int[l];
            for (int i = 0;i<l;i++){
                m[i] = arr[left+i];
            }
            return m;
        }else{
            int[] maxs1 = fun(arr,left,left+1000000);
            int[] maxs2 = fun(arr,left+1000001,right);
            return merge(maxs1, maxs2);
        }
    }
    //取两个数组的前10000个数据
    private static int[] merge(int[] arr1,int[] arr2){
        int i = 0,a1 = 0,a2 = 0;
        int[] mergemax = new int[10000];
        while(i<10000){
            if(arr1[a1]>arr2[a2]){
                mergemax[i++] = arr1[a1++];
            }
            else{
                mergemax[i++] = arr2[a2++];
            }
        }
        return mergemax;
    }


    /**
     * 最小堆法,思想和方法2相似。只不过使用二叉堆的结构存储maxs,效率更高一点。
     * 并且!!!!:这种方法的拓展性更好,如果源数据量变化,或者需要提取的数据变化,只需要修改很小的地方就可以;
     */
    public static int[] top10000minHeap(int[] arr){
       int[] maxs = new int[10000];
       //构建小根堆,为什么是小根堆,因为堆顶是最小的,插入的数如果比最小的还小,
        // 肯定不是最大的10000之一。如果是大根堆,小于和大于堆顶没有任何意义
        maxs = Arrays.copyOf(arr,10000);
       buildHeap(maxs);
       //遍历数组进行插堆下滤,小于堆顶直接跳过,大于堆顶则插入
        for(int i = 10000;i<arr.length;i++){
            if(arr[i]>maxs[0]){
                maxs[0] = arr[i];
                percDown(maxs,0);//重新构建小顶堆
            }
        }
        //层级遍历堆
       return maxs;
    }
    private static void buildHeap(int[] arr){
        int len = arr.length;
        for (int i = len/2;i>=0;i--){
            percDown(arr,i);
        }
    }
    //hole是需要下滤的元素的index而不是元素本身
    private static void percDown(int[]arr,int hole){
        int len = arr.length;
        int insert = arr[hole];
        int child;
        for (;hole*2+1<len;hole = child){
            child = hole*2+1;
            if(child != len-1&&arr[child+1]<arr[child]){
                child++;
            }
            if(arr[child]<insert){
                arr[hole] = arr[child];
            }else
                break;
        }
        arr[hole] = insert;
        //此时的hole就是之前的child,如果跳进过循环,则hole==child。
        // 如果没有跳进过循环,则表示hole*2+1 = child会越界
    }


    public static void main(String[] args) {
        int[] arr = ArrayUtils.arrGenerator(100000000, 10000000);
        long start = System.nanoTime();


        //int[] ints = top1000AllSort(arr);//14998ms
        //int[] ints = top1000First10000(arr);//*,内存不够用。因为多次快排进行了不必要的比较。
        //int[] ints = top1000First10000_2(arr);//526ms
        //int[] ints = top10000partition(arr);//16563ms,分治的效率并没提高。
        int[] ints = top10000minHeap(arr);//105ms 效率最高,输出的数据并不单调,因为二叉堆同一层的元素大小关系并不确定,如果需要单调。
        //可以使用带下标和层数的层级遍历。

        long end = System.nanoTime();
        System.out.println(Arrays.toString(ints));
        System.out.println(ArrayUtils.isMono(ints));
        System.out.println("cost"+(end-start)/1000000+"ms");
    }
}

实际运行: 实际上,最优的解决方案应该是最符合实际设计需求的方案,在时间应用中,可能有足够大的内存,那么直接将数据扔到内存中一次性处理即可,也可能机器有多个核,这样可以采用多线程处理整个数据集。 下面针对不容的应用场景,分析了适合相应应用场景的解决方案。 (1)单机+单核+足够大内存 如果需要查找10亿个查询次(每个占8B)中出现频率最高的10个,考虑到每个查询词占8B,则10亿个查询次所需的内存大约是10^9 * 8B=8GB内存。如果有这么大内存,直接在内存中对查询次进行排序,顺序遍历找出10个出现频率最大的即可。这种方法简单快速,使用。然后,也可以先用HashMap求出每个词出现的频率,然后求出频率最大的10个词。 (2)单机+多核+足够大内存 这时可以直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑同(1)类似,最后一个线程将结果归并。 该方法存在一个瓶颈会明显影响效率,即数据倾斜。每个线程的处理速度可能不同,快的线程需要等待慢的线程,最终的处理速度取决于慢的线程。而针对此问题,解决的方法是,将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,知道所有数据处理完毕,最后由一个线程进行归并。 (3)单机+单核+受限内存 这种情况下,需要将原数据文件切割成一个一个小文件,如次啊用hash(x)%M,将原文件中的数据切割成M小文件,如果小文件仍大于内存大小,继续采用Hash的方法对数据文件进行分割,知道每个小文件小于内存大小,这样每个文件可放到内存中处理。采用(1)的方法依次处理每个小文件。 (4)多机+受限内存 这种情况,为了合理利用多台机器的资源,可将数据分发到多台机器上,每台机器采用(3)中的策略解决本地的数据。可采用hash+socket方法进行数据分发。

在大规模数据处理环境下,作业效率并不是首要考虑的问题,算法的扩展性和容错性才是首要考虑的。因此上述方法中堆排序的方法最优

转载于:https://my.oschina.net/vsimple/blog/1517635