秒杀TOPK问题(附代码)

题目

给定一个很大的文件(1T?10T),里面每一行存储着一个用户的ID(IP?IQ?),你的电脑只有2G内存,请找出其中出现频率最高的十个ID

介绍

TopK问题是近年来实战考的最多最多最多的问题了

其实答案也比较简单,对于

  • 单机
  • 内存有限
  • 文件过大

这样的环境,使用以下的思路进行解决就行了

  • 按行读取大文件
  • Hash分文件
  • 读取单个小文件
  • 使用map计数
  • 维护小顶堆

创建用例

首先自己搞一些用例用来测试

秒杀TOPK问题(附代码)_第1张图片

这儿是创建了十万条字符串,因为这样数据量不多不少,再多运行时间有点长

秒杀TOPK问题(附代码)_第2张图片

生成的文件大小如图

秒杀TOPK问题(附代码)_第3张图片

生成的数据如图

按行读取和分文件

秒杀TOPK问题(附代码)_第4张图片

这一段的代码应该没什么问题?

逻辑为

  • 按行读取
  • 每1000行打印一下进程
  • 根据Hashcode值投桶
  • 以追加模式写入到指定的文件

这样,理论情况下,我们就能得到1000个包含着相同hashcode取模值的字符串

也就是说,所有相同的字符串也在一个文件中

秒杀TOPK问题(附代码)_第5张图片

计数

单个文件是系统可以存下的,那么久将这些字符串计数,将键值放入到一个hashmap中即可

秒杀TOPK问题(附代码)_第6张图片

秒杀TOPK问题(附代码)_第7张图片

维护小顶堆

一次遍历完成之后,就可以将计数放入小顶堆中,这样只用维护一个k大小的小顶堆,就能完成排序

秒杀TOPK问题(附代码)_第8张图片

输出

最后输出小顶堆中的内容即可

秒杀TOPK问题(附代码)_第9张图片

秒杀TOPK问题(附代码)_第10张图片

更优解

如果可以多机就可以使用hadoop,将文件拆分成多个,map单独计数,reduce时统一计数,然后还是使用小顶堆也可以求出topk

但是面试官一般会不允许这样操作XD

附录

TopK用例生成

import java.io.*;
import java.util.*;

class TopKUseCase {
    public static void main(String[] args) throws IOException {
        try {
            final int divNum = 1000;
            int fileSize =100000;
            File outputfile = new File("d:\\bigdata.txt");
            StringBuilder stringBuilder = new StringBuilder();
            PrintWriter output = new PrintWriter(outputfile);
            for (int i = 0; i < fileSize; i++) {
                stringBuilder.append("user" + (int) (Math.random() * 10000) + "who" + System.lineSeparator());
            }
            if (!outputfile.exists())
                outputfile.createNewFile();
            output = new PrintWriter(outputfile);
            output.println(stringBuilder);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

TopK代码

import java.awt.*;
import java.io.*;
import java.util.*;

class TopK {
    public static void main(String[] args) throws IOException {
        final int divNum = 1000; // 分成多少个文件提前按照给定的条件确定
        // 按行读取大文件
        File inputFile = new File("d:\\bigdata.txt");
        FileInputStream inputStream = new FileInputStream(inputFile);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String str = null;
        // 输出的小文件
        File outputfile;
        BufferedWriter output;
        // 计数用来打印日志
        int times = 0;
        System.out.println("分文件开始");
        // 创建目录
        File menu = new File("D:\\div");
        if (!menu.exists())
            menu.mkdirs();
        // 循环读取大文件
        while ((str = bufferedReader.readLine()) != null) {
            times++;
            if (times % 1000 == 0) {
                System.out.println(times + "次了" + str);
            }
            // 根据Hashcode值看到底投到哪个桶
            int order = str.hashCode() & divNum;
            outputfile = new File("D:\\div\\d" + order + "file.txt");
            if (!outputfile.exists())
                outputfile.createNewFile();
            // 注意此处第二个参数要设置为true,表示添加模式
            output = new BufferedWriter(new FileWriter(outputfile, true));
            output.write(str);
            output.newLine();
            output.flush();
        }
        System.out.println("分文件完毕");
        // 创建好小顶堆和hash表
        String[] strArray;
        Map<String, Integer> map;
        Queue<Map.Entry<String, Integer>> queue = new PriorityQueue<>(10, new Comparator<Map.Entry<String, Integer>>() {
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                return o1.getValue() - o2.getValue();
            }
        });
        // 对每一个存在的小文件进行遍历
        for (int i = 0; i <= divNum; i++) {
            System.out.println("第" + i + "个");
            inputFile = new File("D:\\div\\d" + i + "file.txt");
            if (!inputFile.exists())
                continue;
            // 一次读取整个文件 这儿是为了方便 如果实在内存不足就按行读取
            str = readToString(inputFile);
            strArray = str.split(System.lineSeparator());
            map = new HashMap<>();
            // 使用hash表计数
            for (String string : strArray) {
                try {
                    map.put(string, map.get(string) + 1);
                } catch (Exception e) {
                    map.put(string, 1);
                }
            }
            // 维护小顶堆
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (queue.size() < 10) {
                    queue.add(entry);
                    continue;
                }
                if (entry.getValue() > queue.peek().getValue()) {
                    queue.poll();
                    queue.add(entry);
                }
            }
        }
        // 输出小顶堆
        Iterator<Map.Entry<String, Integer>> iterator = queue.iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Integer> entry = iterator.next();
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }

    // 一次读取全部文件
    public static String readToString(File file) {
        Long filelength = file.length();     //获取文件长度
        byte[] filecontent = new byte[filelength.intValue()];
        try {
            FileInputStream in = new FileInputStream(file);
            in.read(filecontent);
            in.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new String(filecontent);//返回文件内容,默认编码
    }
}

你可能感兴趣的:(java,后台)