这是一个比较典型的海量数据处理的问题,在优先内存情况下进行数据排序、判断是否存在等。
主要考察面试者对大数据量计算的经验和思考方式。同时也考察面试者是否有对一些数据库、数据处理中间件原理有过深入的了解。
一般来说有以下几个方案:
使用Java的list、hashSet提供的方法:list.contains()
、set.contains()
时间复杂度分析:
内存空间占用分析:
需要注意的是,这只是粗略的估计。实际的内存占用可能会受到 Java 虚拟机的优化和内存管理策略的影响。此外,还要考虑 List 对象本身的内存开销,以及其他可能的因素(如对象引用、空间对齐等)对内存占用的影响。因此,实际内存占用可能会略高于这个估算值。
hashSet的空间计算:100,000,000(整数数量) × 8(字节 / 整数) = 800,000,000(字节)
由于 hashSet 使用了hashMap数据结构,还需要考虑额外的内存开销用于存储散列桶、链表或红黑树等信息。这个开销通常是每个元素 3~4 倍左右,取决于具体实现和负载因子等因素。 假设每个元素的额外开销为 2 倍,那么每个整数占用的内存空间为:4 字节 × 2 = 8 字节。
显然时间复杂度只有hashSet能胜任,对于内存空间早已超出给定的100M限制。
使用bitmap算法降低内存空间,且时间复杂度为O(1)
java.util.BitSet
是java中bitmap算法的实现,可以初始化一个2亿的BitSet,并把1到2亿通过bitSet.set()
方法设置进去,并通过bitSet.get()
方法判断期望值是否存在。
内存空间占用分析:2,0000,0000(位) ÷ 8(位 / 字节) ÷ 1024(字节 / KB) ÷ 1024(KB/MB) ≈ 23.84 MB
显然bitmap在时间复杂度和空间占用上都完全满足需求。
上述方案2中,存在一个问题:我们要初始化2个亿的bit数组去承载1个亿的数据需求,有点高射炮打蚊子的感觉。
工作中我们并不能百分之百确定一个长期运营的产品用户产生的数据量有多大,同时业务需求也不会只是在海量数据里找出一个值那么简单。通常会有如下几个场景:
上面的3个业务场景中,在使用bitMap除了要初始化一个较大的数组外,还存在一个致命的硬伤:由于id为稀疏整数,会浪费内存空间,效率非常低。
稀疏整数,即向bitMap的数组中存放不连续的整数,如:1,50001,20,910321。如果这四个数字使用bitMap存储,我们要初始化至少910321个bit数组元素,只有这四个位为1,其他位为0,空间被白白浪费。
解决这种问题的一个很好的思路是,尝试对数据集中的整数进行压缩,比如将32位整数拆分成高16位和低16位,将高16位相同的数字放在一起(一个container),低16位形成一个bitmap(本质上也是个bit数组可称作bucket)。
可以简单理解成一个hashmap下挂了bitmap,这样对32位整形进一步压缩。对于稀疏的数据集,大幅度降低内存占用。
这样的基本思路已经有了现成的实现,即RoaringBitmap
,其开源实现不止局限于java,也有C++、go等多种语言。官网地址:https://roaringbitmap.org。
他主要有以下优点:
java.lang.BitSet
接口相同,简单明了,同时功能比BitSet
多当然,他也有缺点:
每个container算法实现不同,如果想深入钻进去看源码或官方的论文,还是比较难理解的。