前言
今天无意看到一个面试题,觉的很有意思。
我理解的就是有100w条数据,从中抽走2条,然后问抽走的是哪两个。一开始自己也想了许多,什么二分法了,循环前后数相减不等于2的就是删除的数据。不过很快自己就推翻自己了,如果人家是拿走相邻的两个呢?如果人家不拿走2个,拿走5个6个呢?一开始也是自己想了很多很多,不过觉的都很烂。直到我发现了 java.util 里的 BitSet 类。我去很好的一次性的,解决了这个问题,话不多说先上代码。
代码操作
public static void main(String[] args) {
//获得系统的时间,单位为毫秒
long startTime = System.currentTimeMillis();
//造出100w条数据
List<Integer> initLists = new ArrayList<>();
for (int i = 1; i <= 1000000; i++) {
initLists.add(i);
}
//随机在100w里抽取5条
Random random = new Random();
List<Integer> randomLists = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
randomLists.add(random.nextInt(1000000));
}
System.out.println("我是随机剔除数" + randomLists);
//100w里去除这5条
for (Integer rand : randomLists) {
initLists.remove(rand);
}
//获取被去除的5个数
List<Integer> removeNumbers = findRemoveNumber1(initLists, 1000000);
System.out.println("被剔除数" + randomLists);
long endTime = System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间" + (endTime - startTime) + "毫秒");
}
/**
* 找出剔除数.
*
* @param numbers 现在所剩数据
* @param count 原先数据总数
* @return
*/
private static List<Integer> findRemoveNumber1(List<Integer> numbers, int count) {
List<Integer> removeList = new ArrayList<>();
//将现有数加入BitSet
BitSet bitSet = new BitSet(count);
for (Integer number : numbers) {
bitSet.set(number);
}
//遍历查找
for (int i = 1; i <= count; i++) {
if (!bitSet.get(i)) {
removeList.add(i);
}
}
return removeList;
}
/**
* 找出剔除数.
*
* @param numbers 现在所剩数据
* @param count 原先数据总数
* @return
*/
private static List<Integer> findRemoveNumber2(List<Integer> numbers, int count) {
List<Integer> removeList = new ArrayList<>();
//获取剔除的数量
int removeCount = count - numbers.size();
//将现有数加入BitSet
BitSet bitSet = new BitSet(count);
for (Integer number : numbers) {
bitSet.set(number - 1);
}
//定义
int lastRemoveIndex = 0;
for (int i = 1; i <= removeCount; i++) {
lastRemoveIndex = bitSet.nextClearBit(lastRemoveIndex);
removeList.add(++lastRemoveIndex);
}
return removeList;
}
方法1
方法2
两个方法的运行时间基本没有差别。
重点 BitSet 类
- BitSet的原理
Java BitSet可以按位存储,计算机中一个字节(byte)占8位(bit);
而BitSet是位操作的对象,值只有0或1(即true 和 false),内部维护一个long数组,初始化只有一个long segement,所以BitSet最小的size是64;随着存储的元素越来越多,BitSet内部会自动扩充,一次扩充64位,最终内部是由N个long segement 来存储;
默认情况下,BitSet所有位都是0即false;
正如上述方案来说:
皇冠人群是一个BitSet,其中1\3\5\63\65\67\69\127对应位为1;即橙色部分;
活跃人群也是一个BitSet,其中5\65\68\127对应位为1;即橙色部分;
而64个位为一个long数组,因此64对应的位就被分配到第2个long数组;
- BitSet的应用场景
海量数据去重、排序、压缩存储
常见的应用是那些需要对海量数据进行一些统计工作的时候,比如日志分析、用户数统计等等
如统计40亿个数据中没有出现的数据,将40亿个不同数据进行排序等。
现在有1千万个随机数,随机数的范围在1到1亿之间。现在要求写出一种算法,将1到1亿之间没有在随机数中的数求出来
- BitSet的基本操作
and(与)、or(或)、xor(异或)
- BitSet的优缺点
优点:
l 按位存储,内存占用空间小
l 丰富的api操作
缺点:
l 线程不安全
l BitSet内部动态扩展long型数组,若数据稀疏会占用较大的内存
- BitSet为什么选择long型数组作为内部存储结构
JDK选择long数组作为BitSet的内部存储结构是出于性能的考虑,在and和or的时候减少循环次数,提高性能;
因为BitSet提供and和or这种操作,需要对两个BitSet中的所有bit位做and或者or,实现的时候需要遍历所有的数组元素。使用long能够使得循环的次数降到最低,所以Java选择使用long数组作为BitSet的内部存储结构。
举个例子:
当我们进行BitSet中的and, or, xor操作时,要对整个bitset中的bit都进行操作,需要依次读出bitset中所有的word,如果是long数组存储,我们可以每次读入64个bit,而int数组存储时,只能每次读入32个bit。
- BitSet源码解析
参考JunitTest断点查看代码,了解BitSet每个方法的实现逻辑
附:
源码解析博文:http://www.cnblogs.com/lqminn/archive/2012/08/30/2664122.html
Java移位基础知识:https://www.cnblogs.com/hongten/p/hongten_java_yiweiyunsuangfu.html
简单理解:BitSet是位操作的对象,值只有0或1即false和true,内部维护了一个long数组,初始只有一个long,所以BitSet最小的size是64,当随着存储的元素越来越多,BitSet内部会动态扩充,最终内部是由N个long来存储,这些针对操作都是透明的。
用1位来表示一个数据是否出现过,0为没有出现过,1表示出现过。使用用的时候既可根据某一个是否为0表示,此数是否出现过。
当我们new BitSte(10) 时 里面默认都是false,当我们set(6) 时 6的位置 就变成了true;