目录
- 前言
- 实现
- 完整代码
- 参考资料
前言
40亿电话号码如何快速去重?我们往往会想到bitmap
数据结构中的 Bitmap 是一种位图索引非常高效的数据结构,用于存储处理大规模数据的位信息,其中每个位对应于一个元素,如果位为1,则表示该元素存在于集合中,否则表示不存在。如果要表示一个包含 10 个元素的数据集,可以创建一个包含 10 位的位数组。
Bitmap 支持插入和查找。插入操作将对应位置的位从 0 设置为 1,将元素添加到数据集中。查找操作通过检查相应位置的位来确定元素是否存在于数据集中。如果位为 1,表示元素存在;如果为 0,表示元素不存在。我们把数字遍历一遍计算放到数组后,就已经是顺序存放的了,遍历取到的就是已经排序后的结果。
Bitmap 非常高效,时间复杂度是O(n)。这是因为位操作本身非常快,并且不受数据集大小的限制。并且Bitmap空间占用非常小,可以大大减小内存消耗。
Bitmap数据结构在搜索引擎、数据库、网络协议等领域都有广泛的应用:
布隆过滤器(Bloom Filter):可以用于快速检查一个元素是否属于一个大型数据集的概率数据结构。
数据库索引:Bitmap 索引可以用于加速数据库查询操作。
网络流量分析:Bitmap 可以用于跟踪网络流量中的 IP 地址、用户 ID 等信息。
此外,Bitmap 适用于离散的、小范围的整数数据。对于连续范围或具有大范围整数的数据集,Bitmap 可能会变得非常大,这种情况下,其他数据结构比如哈希表或树可能更合适。
优点:
运算效率高,不需要进行比较和移位。
空间占用少。
缺点:
所有的数据不能重复。即不可对重复的数据进行排序和查找。
只有当数据比较密集时才有优势。
实现
给定一个bit数组:
private long[] bits;
当我们需要插入一个数 num,需要计算这个 num在整个bit数组中应该在哪个位置:
/**
* 得到long[]的index
* index表示num中包含多少个64bit可以被整除
*/
public int getIndex(long num){
// num/64
return (int) num / BIT_SIZE;
}
/**
* 得到num在64bit数组上的分布
* position表示num中整除了64bit后还余下多少bit
*/
public long getPosition(long num){
// num%64
return (long) num % BIT_SIZE;
}
我们需要知道一个num能映射出多少个bit。
首先long类型相当于64bit,num整除64,计算num中有多少个long,得到 index。而后再num%64,取整除64后的余数,得到position。这两者加起来其实就是num的总bit数,他们共同能够索引到num能映射到的bit位。
我们有插入函数如下:
/**
* 添加num
* 根据num包含多少个long确定在bitmap中的index,根据num÷取余确定在bitmap中除不尽的bit占用
*/
public void add(long num){
int index = getIndex(num);
long position = getPosition(num);
// 将1左移position位后,position那个位置就是1,
// 然后数组的index位置做与运算,这样index索引中的position位置就替换成1了
bits[index] = bits[index] | (1 << position);
}
只要确定的num的bit占用,我们把数组中对应bit位置置为1即可,这个位置就唯一代表我们插入的num。查询也是同理。
/**
* 判断指定数字num是否存在
*/
public boolean contains(int num){
int index = getIndex(num);
long position = getPosition(num);
// 将1左移position后,position那个位置就是1,然后和以前的数据做与运算,判断是否为0即可
return (bits[index] & 1 << position) != 0;
}
完整代码
我们添加了测试代码,首先电话号码是11位的数字,但是你会发现对于java来说11位数字太长太大了,没办法直接存。我们把11位的电话号码拆分成前7位和后4位两部分,分成两个bitmap来分别判断,当电话号码的前7位也存在重复,后4位也存在重复,则认为这个电话号码是重复的。
完整代码如下:
BitMap.java
package org.example.bitmap;
import java.util.BitSet;
import java.util.Random;
/**
* 位图
*/
public class BitMap {
private long[] bits;
private int BIT_SIZE =64;
public BitMap() {
bits = new long[93750000];
}
public BitMap(int n) {
bits = new long[n];
}
/**
* 添加num
* 根据num包含多少个long确定在bitmap中的index,根据num÷取余确定在bitmap中除不尽的bit占用
*/
public void add(long num){
int index = getIndex(num);
long position = getPosition(num);
// 将1左移position位后,position那个位置就是1,
// 然后数组的index位置做与运算,这样index索引中的position位置就替换成1了
bits[index] = bits[index] | (1 << position);
}
/**
* 判断指定数字num是否存在
*/
public boolean contains(int num){
int index = getIndex(num);
long position = getPosition(num);
// 将1左移position后,position那个位置就是1,然后和以前的数据做与运算,判断是否为0即可
return (bits[index] & 1 << position) != 0;
}
/**
* 重置num在位图的索引位置
*/
public void clear(long num){
int index = getIndex(num);
long position = getPosition(num);
// 对1进行左移position个位置,然后取反,最后与byte[index]进行与操作
bits[index] &= ~(1 << position);
}
/**
* 得到long[]的index
* index表示num中包含多少个64bit可以被整除
*/
public int getIndex(long num){
// num/64
return (int) num / BIT_SIZE;
}
/**
* 得到num在64bit数组上的分布
* position表示num中整除了64bit后还余下多少bit
*/
public long getPosition(long num){
// num%64
return (long) num % BIT_SIZE;
}
public int cardinality() {
int sum = 0;
for (int i = 0; i < bits.length; i++) {
sum += Long.bitCount(bits[i]);
}
return sum;
}
public static void main(String[] args) {
// 假设有40亿手机号
long numberOfPhoneNumbers = 4_000_000_000L;
// 存储前7位,最多10,000,000个可能的组合
BitMap first7DigitsSet = new BitMap(10_000_000);
// 存储后4位,最多10,000个可能的组合
BitMap last4DigitsSet = new BitMap(10_000);
long time = System.currentTimeMillis();
// 模拟手机号数据,将已存在的手机号设置为true
for (long i = 0; i < numberOfPhoneNumbers; i++) {
// 手机号
String phoneNumber = generateRandomPhoneNumber();
// 提取手机号的前7位和后4位
String first7Digits = phoneNumber.substring(0, 7);
String last4Digits = phoneNumber.substring(7);
// 将前7位转换为长整数作为位索引
long first7DigitsIndex = Long.parseLong(first7Digits);
// 将后4位转换为整数作为位索引
int last4DigitsIndex = Integer.parseInt(last4Digits);
// 检查前7位 后4位是否重复
if (first7DigitsSet.contains((int) first7DigitsIndex) && last4DigitsSet.contains(last4DigitsIndex)) {
// 打印重复的号码
StringBuilder sb = new StringBuilder(first7Digits);sb.append(last4Digits);
System.out.println("重复的号码:" + sb.toString());
} else {
// 保存不重复的电话号码
first7DigitsSet.add((int) first7DigitsIndex);
last4DigitsSet.add(last4DigitsIndex);
}
}
time = System.currentTimeMillis() - time;
int cardinality = first7DigitsSet.cardinality() + last4DigitsSet.cardinality();
System.out.println(cardinality);
System.out.println(time);
}
/**
* 随机电话号码生成
*/
private static String generateRandomPhoneNumber() {
Random random = new Random();
// 随机生成国家和地区代码(假设国家代码为+1,地区代码为区号)
String countryCode = "+1";
// 随机生成三位区号
String areaCode = String.format("%03d", random.nextInt(1000));
// 随机生成手机号码三位前缀、四位后缀
String prefix = String.format("%03d", random.nextInt(1000));
String suffix = String.format("%04d", random.nextInt(10000));
// 拼接生成的手机号码
String phoneNumber = countryCode + areaCode + prefix + suffix;
return phoneNumber;
}
}
我们直接for循环把40亿个电话生成放入bitmap中,如果bitmap中已存在就打印,不存在则插入。
运行结果如下:
40亿太多了没等运行完,执行过程中控制台一直在打印,其实速度非常快
参考资料
https://blog.csdn.net/caox_nazi/article/details/95340537
https://blog.csdn.net/jj89929665/article/details/123539866