Loading

TopN算法,流式数据获取前N条数据

  背景:由于业务需求,用户想要统计每周,每月,几个月,一年之中的前N条数据。
  根据已有的思路无非就是对全部的数据进行排序,然后取出前N条数据,可是这样的话按照目前最优的排序算法复杂度也在O(nlog(n)),而且如果把所有的数据都放到内存之中排序,数据量太大的话可能不仅仅是慢,还可能因为占用内存过大而导致OOM而产生不可预估的影响。
如果利用分而治之的思想,把所有的数据都存储到磁盘之中,然后数据平均分成M个文件,这样可以利用分批次算出每一个文件之中的前N条数据,然后在合并。但是这样会多次读取磁盘,无形之中增加了多次IO,所以效率也不是很乐观。
  在仔细思考各种排序算法之后,发现我们可以借鉴堆排序算法,采用堆排序,而不采用其他排序算法的原因就是堆排序是分成了两部分。首先构建小/大顶堆,然后在对小/大顶堆进行排序。比如我们可以想要从N条数据中得最大的前Q条数据,那我们就可以构建一个大小为Q的小顶堆,先不对其进行排序,继续读取数据,如果读取的数据小于堆顶数据,直接略过,如果大于对顶数据,则替换堆顶数据,重新构建小/大顶堆。至此,当数据遍历完成,在堆中的所有数据便是最大的前Q条数据,只需对着前十条数据排序即可。这个时间复杂度最大是O(nlog(Q)),最小是O(n)。

 

如有对堆排序不了解的同学,请自行询问度娘,这里不再解释。

堆排序代码参考:https://baike.baidu.com/item/%E5%A0%86%E6%8E%92%E5%BA%8F/2840151?fr=aladdin

 

下面是得到对打的前N条数据的流程图:

 

下面是代码实现:

交换元素代码

/**
 * 交换元素
 *
 * @param arr arr
 * @param a   元素的下标
 * @param b   元素的下标
 * @return void
 * @author liekkas
 */
private void swap(int[] arr, int a, int b) {
    arr[a] = arr[a] ^ arr[b];
    arr[b] = arr[a] ^ arr[b];
    arr[a] = arr[a] ^ arr[b];
}

整个堆排序最关键的地方  以当前节点为根节点构建小顶堆

/**
     * 整个堆排序最关键的地方  以当前节点为根节点构建小顶堆
     *
     * @param array  待组堆
     * @param i      起始结点
     * @param length 堆的长度
     * @return void
     * @author liekkas
     */
    private void adjustMinHeap(int[] array, int i, int length) {
        // 先把当前元素取出来,因为当前元素可能要一直移动
        int temp = array[i];
        for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
            //2*i+1为左子树i的左子树(因为i是从0开始的),2*k+1为k的左子树
            // 让k先指向子节点中最小的节点
            if (k + 1 < length && array[k] > array[k + 1]) {
                //如果有右子树,并且右子树大于左子树
                k++;
            }
            //如果发现结点(左右子结点)大于根结点,则进行值的交换
            if (array[k] < temp) {
                swap(array, i, k);
                // 如果子节点更换了,那么,以子节点为根的子树会受到影响,所以,循环对子节点所在的树继续进行判断
                i = k;
            } else {  //不用交换,直接终止循环
                break;
            }
        }
    }

构建小顶堆

/**
 * 构建小顶堆
 *
 * @param array 待构建数组
 * @return void
 * @author liekkas
 */
private void buildMinHeap(int[] array) {

    //这里元素的索引是从0开始的,所以最后一个非叶子结点array.length/2 - 1
    for (int i = array.length / 2 - 1; i >= 0; i--) {
        //调整堆
        adjustMinHeap(array, i, array.length);
    }
}

堆排序

/**
 * 堆排序
 *
 * @param array 待排序数组
 * @return int[] 已排序数组
 * @author liekkas
 */

private int[] sort(int[] array) {

    buildMinHeap(array);

    // 上述逻辑,建堆结束
    // 下面,开始排序逻辑
    for (int j = array.length - 1; j > 0; j--) {
        // 元素交换,作用是去掉大顶堆
        // 把大顶堆的根元素,放到数组的最后;换句话说,就是每一次的堆调整之后,都会有一个元素到达自己的最终位置
        swap(array, 0, j);
        // 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。
        // 接下来我们需要排序的,就是已经去掉了部分元素的堆了,这也是为什么此方法放在循环里的原因
        // 而这里,实质上是自上而下,自左向右进行调整的
        adjustMinHeap(array, 0, j);
    }
    return array;
}

 

测试代码:

    public static void main(String[] args) {
        HeapSort heapSort = new HeapSort();
        int len = 100000000;
        int topN = 10;
        Random random = new Random();
        int[] arr = new int[len];
        int[] topArr = new int[topN];
        //生成随机数组
        for (int i = 0; i < len; i++) {
            arr[i] = random.nextInt(1000000000);

        }

        //初始化数组
        System.arraycopy(arr, 0, topArr, 0, topN);

        System.out.println("==============>初始化完成");
        long start = System.currentTimeMillis();

        heapSort.buildMinHeap(topArr);
        for (int i = 0; i < len; i++) {
            if (arr[i] > topArr[0]) {
                topArr[0] = arr[i];
                heapSort.buildMinHeap(topArr);
            }
        }

        int[] sort = heapSort.sort(topArr);

        long end = System.currentTimeMillis();
        for (int i : sort) {
            System.out.println(i);
        }

        System.out.println("time:" + (end - start) + "ms");
    }

测试结果:

 

posted @ 2020-12-16 17:17  Philosophy  阅读(597)  评论(0编辑  收藏  举报