今天我偶然刷到了一篇文章,“华为二面:一个文件里面有5亿个数据,一行一个,没有重复的,进行排序”。不知道又是哪个无良媒体瞎起的标题,夺人眼球。

不过说归说,这题听着就很高大上,5亿个数据排序,想想就很爽。常用的内排序算法有很多,比如我们熟悉的冒泡排序、插入排序、快速排序等等。所谓内排序,意思就是在内存里进行排序,不需要占用外存。

对于内部排序算法,在数据量比较小的情况下,还是可以玩玩的,但像上面说的,5亿个数据,先不说时间要多少,来算算需要多少空间存储这些数据 5 * 10 ^ 8 * 32 = (1.6 * 10 ^ 10)bit = (1.6 * 10 ^ 10) / (1024 * 1024 * 8) = 1920M = 1.875G,快接近2G了,面试官就是这么坏,他只给你1G的内存,那玩个毛啊,内存都不够,连数据都装不下,何来排序。

这时候就有必要学习一下位图法(BitMap)了。

1、什么是位图算法

1.1 基本思想

BitMap的核心思想就是用一个bit位来记录0和1两种状态,将具体数据映射到比特数组的具体某一位上,这个bit位设置为0表示该数不存在,设置为1表示该数存在。由于BitMap使用bit来记录数据,所以大大节省了存储空间,比如上面5亿个数据,如果使用bit来记录,只需要(1.875 / 32)G不到60兆内存即可。

利用这个特性,BitMap可以用来处理大量数据的排序、查询以及去重等,BitMap在用户群做交集和并集运算的时候也有极大的便利

举个栗子,比如对于数列[2, 3, 6, 4, 1],在BitMap里是如何存储的呢?

(1)先申请1字节(8 bit)的空间,这样就可以表示8个数,下标范围从0 ~ 7。

(2)然后根据数列将对应的bit位进行设置。对于2,则把下标为2的bit设置为1,依次类推,最终结果如下

这样如果我们需要得到有序数组的话,只需要从下标0开始遍历,如果bit位的值为1,则把下标输出,即可得到有序数列。

那如果数据很大呢?比如50,那0 ~ 7的下标肯定不够,这时候就需要扩充了,算起来好像需要8个这样的bit数组才行,50应该存放在第8个bit数组下标为0的地方。那中间岂不是还剩下6个空着没用?这是后面要讨论的问题了。注意:即使你只需要用到一个bit,也要至少申请8个bit,这是为了内存对齐。

1.2 Map映射

从上面的介绍可以知道,BitMap的思想还是挺简单的,关键就是如何确定10进制到2进制的关系映射图。因为你可以用int(32bit)或者long(64bit)来进行映射。

如果使用int进行映射,假设我们要排序的数有N个,那么需要申请的内存空间大小就是int[(N - 1) / 32 + 1],映射关系如下:

a[0]:0 ~ 31

a[1]:32 ~ 63

a[2]:64 ~ 95

.......

1.3 数据和下标的转换

搞定了数据结构,接下来就是怎么把数据转换到我们申请的bit数组中。我们用“三步走”战略

(1)确定数据在对应数组a中的下标

十进制数0 - 31,对应于数组元素a[0],十进制数32 - 63在数组元素a[1]中,所以可以很容易的知道,一个数num在数组a[num / 32]中,即下标为num / 32。

(2)确定数据在对应数字a[i]中的下标

十进制数1在a[0]的下标为1,十进制数31在a[0]中下标为31,十进制数32在a[1]中下标为0。 在十进制0-31就对应0-31,而32-63则对应也是0-31,即给定一个数num可以通过模32求得在对应数组a[i]中的下标 num % 32。

(3)存放

经过上面两步,我们知道一个数num存放在a[num / 32]的num % 32下标上。我可以通过移位操作将对应位置1

a[n >> 5] |= 1 << (n & 0x1F)

n >> 5不用多解释,大家都会,n & 0x1F 保留n的后五位,相当于 n % 32。

1.4 实现一个简单的BitMap

具备了上面的知识,我们可以很容易的实现一个简陋的BitMap

import java.util.Arrays;

public class BitSet {
private int[] bits; private final static int ADDRESS_BITS_PER_WORD = 5;
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; /**
* 无参构造器
* 默认构造容量为32bit的数组,即数组长度为1
*/
public BitSet() {
bits = new int[(BITS_PER_WORD - 1) >> ADDRESS_BITS_PER_WORD + 1];
} /**
* 有参构造器
* @param nbits 数字的个数
*/
public BitSet(int nbits) {
bits = new int[(nbits - 1) >> ADDRESS_BITS_PER_WORD + 1];
} public int[] getBits() {
return bits;
} /**
* 把num映射到bits数组中
* @param num
*/
public void set(int num) {
// num在数组中的下标
int index = num >> ADDRESS_BITS_PER_WORD;
// TODO:要检查数组是否需要扩容
bits[index] |= 1 << (num & 0x1F);
} /**
* 判断bits数组中对应位的值
* @param bitIndex
* @return
*/
public boolean get(int bitIndex) throws Exception {
if (bitIndex < 0) {
throw new Exception();
}
// 把输入的下标进行转换,对应数组某个值的某个位置
int index = bitIndex >> ADDRESS_BITS_PER_WORD;
return (bitIndex < bits.length) && ((bits[index] & (1 << (bitIndex & 0x1F))) != 0);
} public static void main(String[] args) throws Exception {
// 5亿个数
BitSet bitSet = new BitSet(1_0000_0000);
// 目标数组
int[] arr = {2, 98, 76, 56, 100, 762, 16, 95};
Arrays.stream(arr).forEach(num -> {
bitSet.set(num);
}); // 判断某个数在数组中是否存在
System.out.println(bitSet.get(100)); // true
System.out.println(bitSet.get(200)); // false
System.out.println(bitSet.get(762)); // true // 输出排序后的数组
int[] res = bitSet.getBits();
int count = 0;
for (int i = 0; i < res.length; i++) {
// 按位输出
for (int j = 0; j < 32; j++) {
// 为1表示该数存在
if (((res[i] >> j) & 1) == 1) {
arr[count++] = i * 32 + j;
}
}
}
// 2 16 56 76 95 98 100 762
Arrays.stream(arr).forEach(System.out::println);
}
}

我们可以输出看看它的内部怎么存储的

bits[0][0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
bits[1][0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
bits[2][0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
bits[3][0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
bits[4][0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
......

那么位图算法有没有一些对应的开源实现呢?毕竟自己写肯定不如大神写的好啊,答案是有。JDK的BitSet和Google的EWAHCompressedBitmap,Redis里也提供了类似的一些命令,主要有以下几个:SETBIT, GETBIT, BITCOUNT, BITOP, BITPOS,BITFIELD。

BitSet从JDK1.0开始就存在,是对BitMap算法的简单实现,而EWAHCompressedBitmap对BitMap的存储空间做了优化。比如我要存储的两个数1和100000,按照上面的思路,1和100000中间空了好多位置,但为了存储100000,必须要开足够长的数组才行,那么中间就会浪费很多位置。在这种数据分布极度不均匀的情况下BitMap的空间利用率是很低的。EWAHCompressedBitmap实现就对这种情况作了优化。

这篇文章先不分析JDK的BitSet和Google的EWAHCompressedBitmap的源码,放到下篇文章来。扯远了,现在还是回到面试题吧。:)

2、面试题怎么解决

其实通过上一节的学习,对于5亿个数据如何进行排序已经显而易见了。别说5亿个,50亿个都没问题。

3、BitMap的应用

由以上内容可得知,在数据量越大的情况下,BitMap节省空间的效果就越显著。所以BitMap很适合用来进行大量数据的排序、去重、查找,包括在线活跃用户的统计,用户签到等

对5亿个没有重复元素的数据进行排序

上面代码里就有这个例子,只需要把数装进Bit数组里,然后按顺序输出即可,美得一批。但要注意是没有重复元素的情况下。

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

这种情况需要使用2-BitMap来解决,即用两个bit来表示一个数,定义00表示该数没有出现,01表示出现一次,11表示出现多次。

那么在存入的时候,如果是对应位是00,则变为01,01则变为11,11不变。全部存入完毕后,遍历,如果是11说明该数重复。

统计不同电话号码的个数

如果电话号码是8位数,则范围0 - 99999999,每个数对应一个bit位,大约只需要1.2M即可。

交集、并集的运算

因为BitMap使用位来存储数据,所以自然位具有高效位运算的特性。比如在统计用户兴趣标签的时候,一个bits1数组代表兴趣a,所以存储的就是对a感兴趣的用户,一个bits2数组代表兴趣b,存储的是对b感兴趣的用户。那么如果你想知道既对a感兴趣也对b感兴趣的用户,那么只需要两个数组进行&运算即可。如果想知道对a或对b感兴趣的用户,那么只需要两个数组进行|运算即可。

4、BitMap的优缺点

4.1 优点

(1)运算效率高。

(2)占用内存少。

4.2 缺点

(1)对重复数据无法进行排序。

(2)数据碰撞。比如将字符串映射到 BitMap 的时候会有碰撞的问题,那就可以考虑用 Bloom Filter 来解决,Bloom Filter 使用多个 Hash 函数来减少冲突的概率。

(3)数据稀疏时浪费空间。比如上面举的例子,存入(1, 100000),只有两个数,但我们不得不开足够大的空间来存放100000,这就造成了中间很多空间的浪费,可以通过引入 Roaring BitMap 来解决。

5、面试题的另一种解法

归根结底,BitMap还是全部使用内存来进行排序的。如果面试官不讲武德,把内存条拔了怎么办?或者他只给你很少的内存。这时候就要外排序上场了。

外排序,字面意思,就是要借用外部的空间来完成排序。谁让你不给我足够的内存呢。通俗一点,就是在内存极少的情况下,利用分治策略,使用外存保存中间结果,最后用多路归并进行排序

5.1 基本思想

5.2 怎么分

(1)内存中维护一个核心缓冲区memBuffer,将大文件按行读入,直到memBuffer满了或者大文件已经读完,然后对memBuffer里的数据进行内排序(选择合适的内排序算法),排序后将结果写入磁盘文件sort_file1.txt。

(2)清空memBuffer,重复执行步骤1。

(3)大文件处理完毕后,会得到n个有序的子文件。

5.3 怎么合

现在有了n个有序的文件,关键怎么把它们合并成一个有序的文件。可以利用如下原理进行归并排序:一个n个元素的有序集合

那么对于m个小集合:S1,S2,S3,...,Sm,所有小集合中的最小值是

举个栗子,对于下面的集合

S1 = {1, 4, 8, 9}

S2 = {3, 6, 11, 13}

S3 = {2, 7, 10, 12}

分别拿出三个集合中的最小值,则min(1, 3, 2) = 1,1是在集合S1中的,那么把1写入大文件,同时S1中拿出下一个数4,则min(4, 3, 2) = 2,2是在集合S3中的,那么把2写入大文件,同时S3中拿出下一个数7,依次类推,直到小文件的值全部取出。

6、总结

本文从一道面试题入手,学习了位图BitMap算法,了解了它的原理已经对它进行了简单的实现,同时列举了BitMap的一些使用场景,最后回到面试题,讲解了如何利用BitMap和外排序进行解决。

下篇文章:JDK的BitSet和Google的EWAHCompressedBitmap源码对比与分析。

从一道高大上的面试题来学习位图算法BitMap的更多相关文章

  1. 一道SQL的面试题之联想

    一道SQL的面试题之联想 本人工作在一家小型的民营企业,主要从事业务系统的日常维护,二次开发,菜鸟一枚.周五经理准备面试两个开发人员,据简历,都还比较不错,让经理产生了想法,于是准备了一套面试题目,给 ...

  2. 一道简单的面试题,难倒各大 Java 高手!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 最近栈长在我们的<Java技术栈知识星球>上分享的一道 Java 实战面试题,很有意思,现在拿出来和大家分享下, ...

  3. 【Android】一道Android OpenGL笔试题

    一道Android OpenGL笔试题 SkySeraph May. 5th 2016 Email:skyseraph00@163.com 更多精彩请直接访问SkySeraph个人站点:www.sky ...

  4. 解析js中作用域、闭包——从一道经典的面试题开始

    如何理解js中的作用域,闭包,私有变量,this对象概念呢? 就从一道经典的面试题开始吧! 题目:创建10个<a>标签,点击时候弹出相应的序号 先思考一下,再打开看看 //先思考一下你会怎 ...

  5. [置顶] 小白学习KM算法详细总结--附上模板题hdu2255

    KM算法是基于匈牙利算法求最大或最小权值的完备匹配 关于KM不知道看了多久,每次都不能完全理解,今天花了很久的时间做个总结,归纳以及结合别人的总结给出自己的理解,希望自己以后来看能一目了然,也希望对刚 ...

  6. 学习cordic算法所得(流水线结构、Verilog标准)

    最近学习cordic算法,并利用FPGA实现,在整个学习过程中,对cordic算法原理.FPGA中流水线设计.Verilog标准有了更加深刻的理解. 首先,cordic算法的基本思想是通过一系列固定的 ...

  7. 学习排序算法(一):单文档方法 Pointwise

    学习排序算法(一):单文档方法 Pointwise 1. 基本思想 这样的方法主要是将搜索结果的文档变为特征向量,然后将排序问题转化成了机器学习中的常规的分类问题,并且是个多类分类问题. 2. 方法流 ...

  8. 从 SGD 到 Adam —— 深度学习优化算法概览(一) 重点

    https://zhuanlan.zhihu.com/p/32626442 骆梁宸 paper插画师:poster设计师:oral slides制作人 445 人赞同了该文章 楔子 前些日在写计算数学 ...

  9. 吴裕雄 python 机器学习——集成学习AdaBoost算法回归模型

    import numpy as np import matplotlib.pyplot as plt from sklearn import datasets,ensemble from sklear ...

随机推荐

  1. MVC三层架构的功能的简要说明

    MVC 介绍 MVC: Model 模型 ​ View 视图 ​ Controller 控制器 M (Model) : 模型 功能 DAO层 : 对数据库进行操作(CRUD) Service层 : 处 ...

  2. 【TensorFlow】Win7下使用Object Detection API 训练自己的数据集,并视频实时检测

    说明: 图片:自己开的摄像头,截取的图片.选择了200张图片.下面会有截取的脚本. 使用labelImg工具进行图片进行标注.产生PascalVOC格式的XML文件.[labelImg工具的安装和使用 ...

  3. idea下不能运行main 函数

    解决办法: 需要修改.idea/workspace.xml 在<component name="PropertiesComponent">里添加<property ...

  4. mysql安装_图文详细安装步骤_让你轻松安装并使用(超详细步骤)

    mysql的下载就不用说了,自行到官网下载..(本人下载的是mysql5.0版本) 下面开始正式安装 1.双击mysql_setup.exe后,直接点击Next 2.选择"I accept ...

  5. 初步了解web

    ------------------------1.Web应用程序的main方法在哪里------------------------Tomcat:从启动到运行首先,我们是通过执行 Tomcat 的s ...

  6. ArcGIS JS API使用PrintTask打印地图问题解决汇总

    环境:来源于工作过程,使用的API是  arcgis js 3.*  3系API,4.*暂时没测试: 1.数据与打印服务跨域情况下,不能打印问题. 一般情况下,我们发布的数据服务和打印服务是在一台服务 ...

  7. SQL Server强制使用特定索引 、并行度、锁

    SQL Server强制使用特定索引 .并行度 修改或删除数据前先备份,先备份,先备份(重要事情说三遍) 很多时候你或许为了测试.或许为了规避并发给你SQL带来的一些问题,常常需要强制指定目标sql选 ...

  8. FFmpeg应用实践之命令查询

    0. 前言 FFmpeg 中常用的工具有三个,分别是多媒体编解码工具ffmpeg.多媒体内容分析工具ffprobe和多媒体播放器ffplay.本文介绍的指令都是与编解码工具 ffmpeg 相关的. 学 ...

  9. 游戏视野系统算法 (FOV using recursive shadowcasting)

    原理 http://www.roguebasin.com/index.php?title=FOV_using_recursive_shadowcasting python代码实现 http://www ...

  10. 从零搭建springboot服务03-redis消息订阅

    愿历尽千帆,归来仍是少年 1.所需依赖 <!-- Redis依赖 --> <dependency> <groupId>org.springframework.boo ...