【海量数据算法】如何在10亿数中找出前100大的数(TopN)

一、题目

在10亿数字的文件中找到最大的前100个数字。

二、分析

2.1 排序方法

快速选择(Quick Select)和快速排序(Quick Sort)两个算法的作者都是Hoare,并且思想也非常接近:选取一个基准元素pivot,将数组切分(partition)为两个子数组,比pivot大的扔左子数组,比pivot小的扔右子数组,然后递推地切分子数组。Quick Select不同于Quick Sort之处在于其没有对每个子数组做切分,而是对目标子数组做切分。其次,Quick SelectQuick Sort一样,是一个不稳定的算法;pivot选取直接影响了算法的好坏,最坏情况下的时间复杂度达到了O(n2)

Quick SelectJava实现如下:

public static long quickSelect(long[] nums, int start, int end, int k) {
    if (start == end) {
        return nums[start];
    }
    int left = start;
    int right = end;
    long pivot = nums[(start + end) / 2];
    while (left <= right) {
        while (left <= right && nums[left] > pivot) {
            left++;
        }
        while (left <= right && nums[right] < pivot) {
            right--;
        }
        if (left <= right) {
            long temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
            left++;
            right--;
        }
    }
    if (start + k - 1 <= right) {
        return quickSelect(nums, start, right, k);
    }
    if (start + k - 1 >= left) {
        return quickSelect(nums, left, end, k - (left - start));
    }
    return nums[right + 1];
}

根据快速排序划分的思想

(1) 递归对全部数据分红[a,b)b(b,d]两个区间,(b,d]区间内的数都是大于[a,b)区间内的数
(2) 对(b,d]重复(1)操做,直到最右边的区间个数小于100个。注意[a,b)区间不用划分
(3) 返回上一个区间,并返回此区间的数字数目。接着方法仍然是对上一区间的左边进行划分,分为[a2,b2)b2(b2,d2]两个区间,取(b2,d2]区间。若是个数不够,继续(3)操做,若是个数超过100的就重复1操做,直到最后右边只有100个数为止。

实例代码

public class QuickSelect {
    private static int len = 10;

    /**
     * 多次调用快速排序,讲数据中最大的10位数移动至最右边
     *
     * @param array
     * @param left
     * @param right
     */
    public static void quickSort(int[] array, int left, int right) {
        int pivot;
        if (left < right) {
            pivot = partition(array, left, right);//取出枢轴
            //如果pivot右边的输大于10个,再次重复操作(1)
            if (right - pivot + 1 > len) {
                quickSort(array, pivot + 1, right);
            } else if (right - pivot + 1 < len) {//pivot右边的数小于10个,执行操作(2)
                len = len - (right - pivot + 1);
                quickSort(array, left, pivot + 1);
            }
        }
    }

    /**
     * pivotValue作为枢轴,较之小的元素排序后在其左,较之大的元素排序后在其右
     *
     * @param array
     * @param left
     * @param right
     * @return
     */
    public static int partition(int[] array, int left, int right) {
        int pivot = array[left];
        while (left < right) {
            while (left < right && array[right] >= pivot) {
                --right;
            }
            //讲比枢轴小的元素移到低端,此时right位相当于空,等待地位比pivot大的数补上
            array[left] = array[right];
            while (left < right && array[left] <= pivot) {
                ++left;
            }
            //将比枢轴大的元素移到高端,此时left位相当于空,等待高位比pivot小的数补上
            array[right] = array[left];
        }
        //当left == right,完成一趟快速跑排序,此时left位相当于空,等待pivotkey补上
        array[left] = pivot;
        return left;
    }

    public static void main(String[] args) {
        int[] array = new int[100];
        for (int i = 0; i < array.length; i++) {
            array[i] = (int)(Math.random() * 1000);
        }
        print(array);
        quickSort(array,0,array.length -1);

        for(int i=array.length-10;i<array.length;i++){
            System.out.print(array[i] + " ");
        }
        System.out.println("-----------------");
        print(array);
    }

    public static void print(int[] data) {
        for(int i = 0; i < data.length; i++) {
            System.out.print(data[i] + " ");
        }
        System.out.println();
    }
}

2.2 堆排序法

针对一般的top K问题,一般都会默认K很小,所以一般的top K问题,可以选择使用堆来解决。

堆有个重要的性质:每个结点的值均不大于其左右孩子结点的值,则堆顶元素即为整个堆的最小值。JDKPriorityQueue实现了堆这个数据结构堆,通过指定comparator字段来表示小顶堆或大顶堆,默认为自然序(natural ordering)。

小顶堆解决Top K问题的思路:小顶堆维护当前扫描到的最大K个数,其后每一次扫描到的元素,若大于堆顶则入堆,然后删除堆顶;依此往复,直至扫描完所有元素。

Java实现第K大整数代码如下:

public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> minQueue = new PriorityQueue<>(k);
    for (int num : nums) {
        if (minQueue.size() < k || num > minQueue.peek())
            minQueue.offer(num);
        if (minQueue.size() > k)
            minQueue.poll();
    }
    return minQueue.peek();
}

求第K大的数,这里没有说明K的范围,那么最坏情况下,K == N/2,无论维护一个top K的小顶堆还是维护一个top(N - K)的大顶堆,都需要占用O(N/2)的内存,而对于海量数据而言,这显示是一笔非常大的开销。

手动实现堆

先取出前100个数,维护一个100个数的最小堆,遍历一遍剩余的元素,在此过程当中维护堆就能够了。具体步骤以下:

step1:取前m个元素(例如m=100),创建一个小顶堆。保持一个小顶堆得性质的步骤,运行时间为O(lgm);创建一个小顶堆运行时间为m*O(lgm)=O(m lgm);
step2:顺序读取后续元素,直到结束。每次读取一个元素,若是该元素比堆顶元素小,直接丢弃

若是大于堆顶元素,则用该元素替换堆顶元素,而后保持最小堆性质。最坏状况是每次都须要替换掉堆顶的最小元素,所以须要维护堆的代价为(N-m)*O(lgm);

最后这个堆中的元素就是前最大的10W个。时间复杂度为O(N lgm)

补充:这个方法的说法也能够更简化一些:

假设数组arr保存100个数字,首先取前100个数字放入数组arr,对于第101个数字k,若是k大于arr中的最小数,则用k替换最小数,对剩下的数字都进行这种处理。

实例代码

public class TopN {

    // 父节点
    private int parent(int n) {
        return (n - 1) / 2;
    }

    // 左子节点
    private int left(int n) {
        return 2 * n + 1;
    }

    // 右子节点
    private int right(int n) {
        return 2 * n + 2;
    }

    // 构建堆
    private void buildHeap(int n, int[] data) {
        for(int i = 1; i < n; i++) {
            int t = i;
            // 调整堆
            while(t != 0 && data[parent(t)] > data[t]) {
                int temp = data[t];
                data[t] = data[parent(t)];
                data[parent(t)] = temp;
                t = parent(t);
            }
        }
    }

    // 调整data[i]
    private void adjust(int i, int n, int[] data) {
        if(data[i] <= data[0]) {
            return;
        }
        // 置换堆顶
        int temp = data[i];
        data[i] = data[0];
        data[0] = temp;
        // 调整堆顶
        int t = 0;
        while( (left(t) < n && data[t] > data[left(t)])
                || (right(t) < n && data[t] > data[right(t)]) ) {
            if(right(t) < n && data[right(t)] < data[left(t)]) {
                // 右子节点更小,置换右子节点
                temp = data[t];
                data[t] = data[right(t)];
                data[right(t)] = temp;
                t = right(t);
            } else {
                // 否则置换左子节点
                temp = data[t];
                data[t] = data[left(t)];
                data[left(t)] = temp;
                t = left(t);
            }
        }
    }

    // 寻找topN,该方法改变data,将topN排到最前面
    public void findTopN(int n, int[] data) {
        // 先构建n个数的小顶堆
        buildHeap(n, data);
        // n往后的数进行调整
        for(int i = n; i < data.length; i++) {
            adjust(i, n, data);
        }
    }

    // 打印数组
    public void print(int[] data) {
        for(int i = 0; i < data.length; i++) {
            System.out.print(data[i] + " ");
        }
        System.out.println();
    }
}

2.3 分治法

先把10亿个数分成100份,每份1000w个数,然后在1000w个数中分别找出最大的100个数,最后在100*100个数中找出最大的100个。这里我想可以用分布式的处理,多台主机才会更快。

具体代码可以参考10亿int型数,统计只出现一次的数

参考文章

posted @ 2022-04-24 11:28  夏尔_717  阅读(898)  评论(0编辑  收藏  举报