大数据Top K问题

大数据下的2个Top K场景:

1、 1亿个数字中找出最大或最小的前100个数字(考虑判重和内存空间限制)?

思路: 考虑将数字分散存储在不同文件中, 然后依次读取各个文件, 例如1~10000存到1.txt、 2~20000存到2.tx、以此类推;

        如果不判重,可以将前100个数字做成最大堆或最小堆的数据结构(找最大的Top100用最小堆, 找最小的Top100用最大堆), 然后依次遍历所有数字, 符合条件时替换根节点后并重新构建堆。堆的缺点是允许有重复数字!!!

        如果要判重,则创建一个100个空间的空数组, 遍历1亿个数字依次插入值(按照升序), 使用二分查找算法判断新值在数组中的位置并插入, 该数组最多容纳100个值。 当有101个值时先判重, 如果重复继续向后遍历, 如果值大于数组最小值则插入到指定位置,数组第一个元素移出数组, 因为数组是连续的,所有可以用内存拷贝方式赋值,只有新插入的值要单独赋值到对应的下标(原理类似于android.util.SparseArray)。   因内存拷贝可认为不占用时间, 该思路的总会时间复杂度是O(1亿*log100), log100是二分查找的复杂度。

  

2、 1亿个搜索关键词找出次数最多的前100个搜索词(考虑内存空间限制)? 

思路: 核心是“分治”、“归并”和哈希,  第一次遍历将关键词散列到不同的文件中(散列算法是性能关键,哈希函数的性能直接影响散列的结果, 尽量避免数据倾斜), 同一个关键词一定会散列到同一个文件, 理想情况是所有关键词均匀散列到不同的文件中(即分治思想,将大文件分解为小问题)。

       读取每个文件并记录各关键词的次数, 做个冒泡排序, 从每个文件中排序出前100的关键词;

       取第一个文件的记录结果, 和第二个文件做合并, 即200个结果中排序出前100个关键词, 然后依次合并第三个、第四个。。。。第N个文件(将子结果合并为总结果)。


PS:   大道同源, 我想到2个类似的场景。

1、 电脑里有众多的文件,但我们还是能从众多文件中找到你想要的。 这是为什么呢?  因为使用了文件夹, 按照文件类型保存在各个层级的目录里。 这个目录层级跟”在1亿个搜索词中找出频率最高的100个“的解决思路是一样的, 即将关键词分散存储到不同的文件里(也可以使用层级目录, 目录越深分散的粒度越细, 到底分几个文件/目录层级其实是时间空间的权衡)

2、 数据库用户表里要存储几亿个记录,该怎么办? 跟上面管理电脑文件的问题类似, 可以按地域、年龄、姓名等等因素将数据存储到N张表里, 术语叫做”横向切割“


名词解释:

1、”数据倾斜“是大数据里的一个概念,就是数据集中到几个文件、处理器, 没均匀分散到各个处理单元。说白了就是忙的忙死,闲的闲死。

2、”分治“就是将大问题分解为小问题, 处理每个小问题并得到结果, 然后将所有子结果汇总成最终结果。

3、”横向切割“是数据库的一个概念, 就是将表记录分散存储到不同的表里,每个表里的记录都是完整的。 还有个“纵向切割”,就是将分拆表的列为多个表, 即一条记录要存到多张表里且每个表只存几个属性。 

4、“哈希”即散列, 就是通过哈希值找到存储位置, 理想情况下时间复杂度O(1)。 


下面开撸堆排序算法和代码:

       堆是个完全二叉树, 节点先从上到下后从左到右都是满的, 说白了叶子节点可以不满且非叶节点都有。 用Java数组形容的话, 数组下标从0到N-1。 如果节点i有左子树,那么左子树的下标是2i+1;如果节点i有右子树,那么右子树的下标是2i+2;如果有父节点,对应下标是(i-1)/2取整。

       堆分为最大堆和最小堆,也可以叫大根堆和小根堆; 最大堆的定义是任意子树根节点大于或等于子节点,所以根节点是最大值; 最小堆的定义是任意子树根节点小于或等于子节点, 所以根节点是最小值。

       堆排序思想是先构造堆,然后做N次循环, 每次交互根节点和最后一个值(最后节点是递减的)重新构建堆。

       构造堆是堆排序的核心, 算法是从最后一个非叶节点(即N/2)开始,先从右到左后从下到上依次判断交换值。详见堆排序动画: http://www.benfrederickson.com/heap-visualization/


 使用最小堆找出最大的前10个数字:

 //得到最大的前length个数字, 使用最小堆数据结果,理论上可能有重复数字
    public static void topNums(final int length)throws Exception{
        Scanner scanner=new Scanner(new FileInputStream("input.txt"));

        long array[]=new long[length];
        for(int i=0; i<array.length; i++){
            array[i]=scanner.nextLong();
        }

        buildMinHeap(array); //构造小顶堆

        while (scanner.hasNextLong()){
            long value = scanner.nextLong();

            if(value < array[0]) {
                continue;   //根节点是数组最小值, 如果当前值比最小值还小就跳过
            }

            array[0] = value;   //丢掉根节点,即替换array[0]
            ajustHeap(array, length,0);  //重新构造小根堆
        }

        //将一个小根堆进行排序,用堆排序思想
        heapSort(array);

        for(int i=0; i<length; i++)
            System.out.println(array[i]);

        scanner.close();
    }



    public static void main(String[]args)throws Exception{
        randomData();  //生成随机数并保持到文件里
        long start=System.currentTimeMillis();
        topNums(10);   //从文件中找出最大的前10个数字
        long end=System.currentTimeMillis();
        System.out.println(end-start);


        //验证堆排序是否正确的测试数据
        long[] array = {1, 3, -1, -5, 5, 10, 20, -10, 100, 30, 11, 16};
        System.out.println("原始数组:");
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
        heapSort(array);
        System.out.println("排序后:");
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }

    }

    //得到从大到小的降序
    public static void heapSort(long[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        buildMinHeap(array);  //构造堆

        for (int i = array.length - 1; i >= 1; i--) {
            long temp = array[i];
            array[i] = array[0];
            array[0] = temp;

            ajustHeap(array, i, 0);  //重新调整第0个下标节点
        }
    }

    //构造小根堆
    private static void buildMinHeap(long[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        int half = array.length / 2;
        for (int i = half; i >= 0; i--) {
            ajustHeap(array, array.length, i);
        }
    }

    //构造小顶堆, 按照分治的思想递归
    private static void ajustHeap(long[] array, int heapSize, int index) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;

        int smallest = index;
        if (left < heapSize && array[left] < array[index]) {
            smallest = left;
        }

        if (right < heapSize && array[right] < array[smallest]) {
            smallest = right;
        }

        if (index != smallest) {
            long temp = array[smallest];
            array[smallest] = array[index];
            array[index] = temp;

            ajustHeap(array, heapSize, smallest);
        }
    }

    //随机生成100000个数字并保存到文件里
    public static void randomData()throws Exception{//随机100万数据

        File file=new File("input.txt");
        if(!file.exists())
            file.createNewFile();

        PrintStream printStream=new PrintStream(new FileOutputStream(file));

        Random random=new Random(System.currentTimeMillis());

        for(int i=0;i<1000000;i++){
            long k=Math.abs(random.nextLong());
            printStream.println(k);
            //printStream.println(k);  //连续写2个相同值, 验证使用堆得到的top k结果有重复数字
        }
        printStream.close();
    }

       其中adjustHeap函数是核心函数, 思想是找出当前节点、左子节点、右子节点的最大或最小值, 如果当前节点已经是最大或最小则退出, 否则交换当前节点和子节点的值,然后递归处理子节点, 这是“分治法“的体现。

构造堆结果也可以使用非递归的方式实现:

    //构造最大堆的非递归实现
    public void adjustHeapExt(long[] array, int parent, int length) {
        long temp = array[parent];
        int child = 2 * parent + 1;  //左子节点

        while (child < length) {
            if (child + 1 < length && array[child] < array[child + 1]) {
                child++;   //右子节点比左子节点更大
            }
            
            if (temp >= array[child]) {
                break;  //当前节点最大则退出
            }
            
            array[parent] = array[child];  //替换根节点的值
            
            parent = child;  //向下遍历
            child = 2 * child + 1;  //从左子节点开始比较
        }
        array[parent] = temp;   //存储原根节点值
    }

  补充一下: 1亿个数字找出top 100的二分查找原理, 对于有序数组先比较array[0], 然后二分查找并数组前移、后移加上赋值。  


摘自SparsArray.java

 /**
     * 二分查找数据中是否存在值
     * @param array,数组,升序数组
     * @param value,要查找的值
     * @return  找到就返回数组下标, 找不到就返回负数(需要插入的位置)
     */
    static int binarySearch(long[] array, long value) {
        int lo = 0;
        int hi = array.length - 1;

        while (lo <= hi) {
            final int mid = (lo + hi) >>> 1;
            final long midVal = array[mid];

            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;  // value found
            }
        }
        return ~lo;  // value not present
    }

     小结, 对于大数据第一步是要"分治”,  “分治”算法是影响性能的关键。

猜你喜欢

转载自blog.csdn.net/brycegao321/article/details/79629155
今日推荐