【海量数据算法】10亿int型数,统计只出现一次的数
一、题目
10
亿int
整型数,以及一台可用内存为1GB
的机器,时间复杂度要求O(n)
,统计只出现一次的数。
二、分析
首先分析多大的内存能够表示10
亿的数呢?一个int
型占4
字节,10
亿就是40
亿字节(很明显就是4GB
),也就是如果完全读入内存需要占用4GB
,而题目只给1GB
内存,显然不可能将所有数据读入内存。
我们先不考虑时间复杂度,仅考虑解决问题。那么接下来的思路一般有两种。
- 位图法 :用一个
bit
位来标识一个int
整数。 - 分治法 :分批处理这
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^32
) ,tmp
中的每个元素在内存在占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
)。上例中我们如何确定8
在tmp[0]
中的32
个位中的哪个位,这种情况直接mod
上32
就ok
,又如整数8
,在tmp[0]
中的第8 mod
上32
等于8
,那么整数8
就在tmp[0]
中的第八个bit
位(从右边数起)。
然后我们怎么统计只出现一次的数呢?每一个数出现的情况我们可以分为三种:0
次、1
次、大于1
次。也就是说我们需要用2
个bit
位才能表示每个数的出现情况。此时则三种情况分别对应的bit
位表示是:00
、01
、11
我们顺序扫描这10
亿的数,在对应的双bit
位上标记该数出现的次数。最后取出所有双bit
位为01
的int
型数就可以了。
2.1.2 Bitmap拓展
位图(Bitmap
)算法思想比较简单,但关键是如何确定十进制的数映射到二进制bit
位的map
图。
优点:
- 运算效率高,不许进行比较和移位;
- 占用内存少,比如
N=10000000
;只需占用内存为N/8=1250000Byte=1.25M
缺点: 所有的数据不能重复。即不可对重复的数据进行排序和查找。
建立了Bit-Map
之后,就可以方便的使用了。一般来说Bit-Map
可作为数据的查找、去重、排序 等操作。
比如以下几个例子:
- 在3亿个整数中找出重复的整数个数,限制内存不足以容纳3亿个整数
对于这种场景可以采用2-BitMap
来解决,即为每个整数分配2bit
,用不同的0
、1
组合来标识特殊意思,如00
表示此整数没有出现过,01
表示出现一次,11
表示出现过多次,就可以找出重复的整数了,其需要的内存空间是正常BitMap
的2
倍,为:3亿*2/8/1024/1024=71.5MB
。
具体的过程如下: 扫描着3
亿个整数,组BitMap
,先查看BitMap
中的对应位置,如果00
则变成01
,是01
则变成11
,是11
则保持不变,当将3
亿个整数扫描完之后也就是说整个BitMap
已经组装完毕。最后查看BitMap
将对应位为11
的整数输出即可。
- 对没有重复元素的整数进行排序
对于非重复的整数排序BitMap
有着天然的优势,它只需要将给出的无重复整数扫描完毕,组装成为BitMap
之后,那么直接遍历一遍Bit
区域就可以达到排序效果了。
举个例子:对整数4
、3
、1
、7
、6
进行排序:
直接按Bit
位输出就可以得到排序结果了。
- 已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数
8
位最多99 999 999
,大概需要99m
个bit
,大概10
几m
字节的内存即可。可以理解为从0-99 999 999
的数字,每个数字对应一个Bit
位,所以只需要99M
个Bit==1.2MBytes
,这样,就用了小小的1.2M
左右的内存表示了所有的8
位数的电话。
- 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
)和归并排序两种方案。
哈希分桶的思想是先遍历一遍,按照hash
分N
桶(比如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();
}
}