【海量数据算法】10亿int型数,统计只出现一次的数

一、题目

10亿int整型数,以及一台可用内存为1GB的机器,时间复杂度要求O(n),统计只出现一次的数。

二、分析

首先分析多大的内存能够表示10亿的数呢?一个int型占4字节,10亿就是40亿字节(很明显就是4GB),也就是如果完全读入内存需要占用4GB,而题目只给1GB内存,显然不可能将所有数据读入内存。

我们先不考虑时间复杂度,仅考虑解决问题。那么接下来的思路一般有两种。

  1. 位图法 :用一个bit位来标识一个int整数。
  2. 分治法 :分批处理这10亿的数。

一种是位图法,int整型数是4字节(Byte),也就是32位(bit),如果能用一个bit位来标识一个int整数那么存储空间将大大减少。
另一种是分治法,内存有限,分批读取处理。

2.1 位图法(Bitmap)

位图法是基于int型数的表示范围这个概念的,用一个bit位来标识一个int整数,若该位为1,则说明该数出现;若该位为0,则说明该数没有出现。一个int整型数占4字节(Byte),也就是32位(bit)。那么把所有int整型数字表示出来需要2^32 bit的空间,换算成字节单位也就是2^32/8 = 2^29 Byte,大约等于512MB

// 插播一个常识
2^10 Byte = 1024 Byte = 1KB
2^30 Byte = (2^10)^3 Byte = 1024 * 1024 * 1024 Byte = 1GB

这下就好办了,只需要用512MB的内存就能存储所有的int的范围数。

2.1.1 具体方案

那么接下来我们只需要申请一个int数组长度为int tmp[**N/32+1**]即可存储完这些数据,其中N代表要进行查找的总数(这里也就是2^32tmp中的每个元素在内存在占32位可以对应表示十进制数0~31,所以可得到BitMap表:

  • tmp[0]:可表示0~31
  • tmp[1]:可表示32~63
  • tmp[2]可表示64~95
  • ~~

假设这10亿int数据为:6,3,8,32,36,……,那么具体的BitMap表示为:

(1). 如何判断int数字放在哪一个tmp数组中:将数字直接除以32取整数部分(x/32),例如:整数8除以32取整等于0,那么8就在tmp[0]上;

(2). 如何确定数字放在32个位中的哪个位:将数字mod32取模(x%32)。上例中我们如何确定8tmp[0]中的32个位中的哪个位,这种情况直接mod32ok,又如整数8,在tmp[0]中的第8 mod32等于8,那么整数8就在tmp[0]中的第八个bit位(从右边数起)。

然后我们怎么统计只出现一次的数呢?每一个数出现的情况我们可以分为三种:0次、1次、大于1次。也就是说我们需要用2bit位才能表示每个数的出现情况。此时则三种情况分别对应的bit位表示是:000111

我们顺序扫描这10亿的数,在对应的双bit位上标记该数出现的次数。最后取出所有双bit位为01int型数就可以了。

2.1.2 Bitmap拓展

位图(Bitmap)算法思想比较简单,但关键是如何确定十进制的数映射到二进制bit位的map图。

优点:

  1. 运算效率高,不许进行比较和移位;
  2. 占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M

缺点: 所有的数据不能重复。即不可对重复的数据进行排序和查找。

建立了Bit-Map之后,就可以方便的使用了。一般来说Bit-Map可作为数据的查找、去重、排序 等操作。

比如以下几个例子:

  1. 在3亿个整数中找出重复的整数个数,限制内存不足以容纳3亿个整数

对于这种场景可以采用2-BitMap来解决,即为每个整数分配2bit,用不同的01组合来标识特殊意思,如00表示此整数没有出现过,01表示出现一次,11表示出现过多次,就可以找出重复的整数了,其需要的内存空间是正常BitMap2倍,为:3亿*2/8/1024/1024=71.5MB

具体的过程如下: 扫描着3亿个整数,组BitMap,先查看BitMap中的对应位置,如果00则变成01,是01则变成11,是11则保持不变,当将3亿个整数扫描完之后也就是说整个BitMap已经组装完毕。最后查看BitMap将对应位为11的整数输出即可。

  1. 对没有重复元素的整数进行排序

对于非重复的整数排序BitMap有着天然的优势,它只需要将给出的无重复整数扫描完毕,组装成为BitMap之后,那么直接遍历一遍Bit区域就可以达到排序效果了。

举个例子:对整数43176进行排序:

直接按Bit位输出就可以得到排序结果了。

  1. 已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数

8位最多99 999 999,大概需要99mbit,大概10m字节的内存即可。可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99MBit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话。

  1. 2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数

bit-map扩展一下,用2bit表示一个数即可:0表示未出现;1表示出现一次;2表示出现2次及以上,即重复,在遍历这些数的时候,如果对应位置的值是0,则将其置为1;如果是1,将其置为2;如果是2,则保持不变。或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个2bit-map,都是一样的道理。

最后放一个使用Byte[]数组存储、读取bit位的示例代码,来自利用位映射原理对大数据排重

class BitmapTest {

    private static final int CAPACITY = 1000000000;//数据容量

    // 定义一个byte数组缓存所有的数据
    private byte[] dataBytes = new byte[1 << 29];

    public static void main(String[] args) {
        BitmapTest ms = new BitmapTest();

        byte[] bytes = null;

        Random random = new Random();
        for (int i = 0; i < CAPACITY; i++) {
            int num = random.nextInt();
            System.out.println("读取了第 " + (i + 1) + "\t个数: " + num);
            bytes = ms.splitBigData(num);
        }
        System.out.println("");
        ms.output(bytes);
    }


    /**
     * 读取数据,并将对应数数据的 到对应的bit中,并返回byte数组
     * @param num 读取的数据
     * @return byte数组  dataBytes
     */
    private byte[] splitBigData(int num) {

        long bitIndex = num + (1l << 31);         //获取num数据对应bit数组(虚拟)的索引
        int index = (int) (bitIndex / 8);         //bit数组(虚拟)在byte数组中的索引
        int innerIndex = (int) (bitIndex % 8);    //bitIndex 在byte[]数组索引index 中的具体位置

        System.out.println("byte[" + index + "] 中的索引:" + innerIndex);

        dataBytes[index] = (byte) (dataBytes[index] | (1 << innerIndex));
        return dataBytes;
    }

    /**
     * 输出数组中的数据
     * @param bytes byte数组
     */
    private void output(byte[] bytes) {
        int count = 0;
        for (int i = 0; i < bytes.length; i++) {
            for (int j = 0; j < 8; j++) {
                if (!(((bytes[i]) & (1 << j)) == 0)) {
                    count++;
                    int number = (int) ((((long) i * 8 + j) - (1l << 31)));
                    System.out.println("取出的第  " + count + "\t个数: " +  number);
                }
            }
        }
    }

}

2.2 分治法

分治法目前看到的解决方案有哈希分桶(Hash Buckets归并排序两种方案。

哈希分桶的思想是先遍历一遍,按照hashN桶(比如1000桶),映射到不同的文件中。这样平均每个文件就10MB,然后分别处理这1000个文件,找出没有重复的即可。一个相同的数字,绝对不会夸文件,有hash做保证。

import java.io.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * @author xiaer
 * @date 2022/7/28 11:06
 */
public class ApartData {

    private static final String FILE_NAME = "/Users/xiaer/apart/data.txt";
    private static final String FOLDER = "/Users/xiaer/apart2";
    private static final Integer HASH_NUM = 1000;
    private static final Integer MAX_NUM = 10000000;

    /**
     * 生成文件
     */
    public static void generateIpsFile() {
        File file = new File(FILE_NAME);
        try {
            FileWriter fileWriter = new FileWriter(file);
            for (int i = 0; i < MAX_NUM; i++) {
                fileWriter.write(generateIp());
            }
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static String generateIp() {
        return "192.168." + (int) (Math.random() * 255) + "."
                + (int) (Math.random() * 255) + "\n";
    }

    /**
     * 分割
     */
    public static void divideIpsFile() {
        File file = new File(FILE_NAME);
        Map<String, StringBuilder> map = new HashMap<>();
        int count = 0;
        try {
            FileReader fileReader = new FileReader(file);
            BufferedReader br = new BufferedReader(fileReader);
            String ip;
            while ((ip = br.readLine()) != null) {
                String hashIp = hash(ip);
                if (map.containsKey(hashIp)) {
                    StringBuilder sb = map.get(hashIp);
                    sb.append(ip).append("\n");
                    map.put(hashIp, sb);
                } else {
                    StringBuilder sb = new StringBuilder(ip);
                    sb.append("\n");
                    map.put(hashIp, sb);
                }
                count++;
                if (count == MAX_NUM / HASH_NUM) {
                    Iterator<String> it = map.keySet().iterator();
                    while (it.hasNext()) {
                        String fileName = it.next();
                        File ipFile = new File(FOLDER + "/" + fileName + ".txt");
                        FileWriter fileWriter = new FileWriter(ipFile, true);
                        StringBuilder sb = map.get(fileName);
                        fileWriter.write(sb.toString());
                        fileWriter.close();
                    }
                    count = 0;
                    map.clear();
                }
            }
            br.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * hash取模
     *
     * @param ip
     * @return
     */
    private static String hash(String ip) {
        long numIp = ipToLong(ip);
        return String.valueOf(numIp % HASH_NUM);
    }

    private static long ipToLong(String strIp) {
        long[] ip = new long[4];
        int position1 = strIp.indexOf(".");
        int position2 = strIp.indexOf(".", position1 + 1);
        int position3 = strIp.indexOf(".", position2 + 1);

        ip[0] = Long.parseLong(strIp.substring(0, position1));
        ip[1] = Long.parseLong(strIp.substring(position1 + 1, position2));
        ip[2] = Long.parseLong(strIp.substring(position2 + 1, position3));
        ip[3] = Long.parseLong(strIp.substring(position3 + 1));
        return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3];
    }

    /**
     * 计算
     */
    public static void calculate() {
        File folder = new File(FOLDER);
        File[] files = folder.listFiles();
        if (files == null || files.length == 0) {
            return;
        }
        FileReader fileReader;
        BufferedReader br;
        HashMap<String, Integer> map = new HashMap<>();
        for (File file : files) {
            try {
                fileReader = new FileReader(file);
                br = new BufferedReader(fileReader);
                String ip;
                Map<String, Integer> tmpMap = new HashMap<>();
                while ((ip = br.readLine()) != null) {
                    if (tmpMap.containsKey(ip)) {
                        int count = tmpMap.get(ip);
                        tmpMap.put(ip, count + 1);
                    } else {
                        tmpMap.put(ip, 0);
                    }
                }
                fileReader.close();
                br.close();
                count(tmpMap, map);
                tmpMap.clear();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        HashMap<String, Integer> finalMap = new HashMap<>();
        count(map, finalMap);
        Iterator<String> it = finalMap.keySet().iterator();
        while (it.hasNext()) {
            String ip = it.next();
            System.out.println("result IP : " + ip + " | count = " + finalMap.get(ip));
        }
    }

    /**
     * 统计ip出现次数的最大值
     *
     * @param pMap
     * @param resultMap
     */
    private static void count(Map<String, Integer> pMap, Map<String, Integer> resultMap) {
        Iterator<Map.Entry<String, Integer>> it = pMap.entrySet().iterator();
        int max = 0;
        String resultIp = "";
        while (it.hasNext()) {
            Map.Entry<String, Integer> entry = it.next();
            if (entry.getValue() > max) {
                max = entry.getValue();
                resultIp = entry.getKey();
            }
        }
        resultMap.put(resultIp, max);
    }

    public static void main(String[] args) {
        //生成ip文件
        generateIpsFile();
        //分割
        divideIpsFile();
        //计算
        calculate();
    }
}

参考文章

posted @ 2022-04-26 21:45  夏尔_717  阅读(329)  评论(0编辑  收藏  举报