找往期文章包括但不限于本期文章中不懂的知识点:
个人主页: 我要学编程(ಥ_ಥ)-CSDN博客
所属专栏:数据结构(Java版)****
位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,整数,数据无重复的场景。通常是用来判断某个数据存不存在的。
例如:现在有一组 int 类型的数据1~10,怎么判断是否全部被储存起来了?
按照正常的处理的话,应该是把这10个数据全部存储到数组中,然后再遍历数据即可知道。这其中用到的内存是40个字节;如果用位图来处理,就是这样:一个字节有8个比特位,用比特位来标识是否存在,那么就只需要用两个字节即可(12个比特位)。
位图的模拟实现
java
public class BitSet {
public byte[] elem;
public int usedSize; // 记录存放元素的个数
public BitSet() {
elem = new byte[2]; // 默认给16个比特位
}
public BitSet(int n) {
elem = new byte[n]; // 给8n个比特位
}
// 把数据对应的位置置为1
public boolean set(int val) {
if (val < 0) {
System.out.println("插入元素坐标异常");
return false;
}
// 先找到对应的下标 和 下标对应的位置
int index = val / 8;
int bitIndex = val % 8;
if (index >= elem.length) {
elem = Arrays.copyOf(elem, 2 * elem.length);
}
elem[index] |= 1 << bitIndex;
// 或者下面这种写法
// elem[index] = (byte) (elem[index] | (1 << bitIndex));
usedSize++;
return true;
}
// 判断一个元素是否存在
public boolean get(int val) {
if (val < 0) {
System.out.println("查找元素坐标异常");
return false;
}
// 先找到对应的下标 和 下标对应的位置
int index = val / 8;
int bitIndex = val % 8;
// 这里只能判断是否为0
if ((elem[index] & (1 << bitIndex) )!= 0) {
return true;
}
return false;
}
// 将val值对应的位置置为0
public boolean reSet(int val) {
if (val < 0) {
System.out.println("重置元素坐标异常");
return false;
}
// 先找到对应的下标 和 下标对应的位置
int index = val / 8;
int bitIndex = val % 8;
elem[index] &= ~(1 << bitIndex);
return true;
}
public int getUsedSize() {
return usedSize;
}
}
位图的模拟实现主要就是:利用位运算记录元素。
从上面的模拟实现,我们也可以得出几个结论:
1、位图只能记录元素是否存在,不能取出元素和存储元素;
2、位图可以用来去除重复的元素;
3、位图可以对集合求交集和并集、差集等;
3、位图还可以用来排序。
排序的话,就是遍历位图的数组,通过比特位从低到高的方式,看看其是否为1,是就按照权位输出排序。
代码实现:
java
public void sort() {
for (int i = 0; i < elem.length; i++) {
for (int j = 0; j < 8 ; j++) {
if ((elem[i] & (1 << j))!= 0) {
System.out.print(i*8+j+" ");
}
}
}
}
注意:源码的位图使用的是 long 类型的数组。
布隆过滤器
日常生活中,包括在设计计算机软件时,我们经常要判断一个元素是否在一个集合中。比如在字处理软件中,需要检查一个英语单词是否拼写正确(也就是要判断它是否在已知的字典中);在FBI,一个嫌疑人的名字是否已经在嫌疑名单上;在网络爬虫里,一个网址是否被访问过等等。最直接的方法就是将集合中全部的元素存在计算机中,遇到一个新元素时,将它和集合中的元素直接比较即可。
一般来讲,计算机中的集合是用哈希表来存储的。它的好处是快速准确,缺点是费存储空间。当集合比较小时,这个问题不显著,但是当集合巨大时,哈希表存储效率低的问题就显现出来了。
比如说,一个像Yahoo,Hotmail和 Gmai那样的公众电子邮件(email)提供商,总是需要过滤来自发送垃圾邮件的人(spamer)的垃圾邮件。一个办法就是记录下那些发垃圾邮件的email地址。由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。因此,为了解决这个问题,就提出了布隆过滤器的概念。
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 "某样东西一定不存在或者可能存在",它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。其实就是哈希表和位图的结合。
同一个数据经过三个不同的哈希函数进行处理记录到位图中。
布隆过滤器的模拟实现
首先,得有三个或者多个哈希函数。我们可以通过随机数来得到不同的值再乘上哈希值即可。
java
class HashFunction {
public Random random = new Random();
public int[] hash(String str) {
int len = str.length();
int[] ret = new int[3];
for (int i = 0; i < ret.length; i++) {
// 生成0~15的随机数
ret[i] = str == null ? 0 : str.hashCode() * len * random.nextInt(15);
}
return ret;
}
}
剩下的其实就是对位图的一个应用。
java
public class BloomFilter {
public BitSet bitSet;
public HashFunction hashFunction;
public int usedSize;
public BloomFilter() {
bitSet = new BitSet();
hashFunction = new HashFunction();
}
public boolean set(String str) {
if (str == null) {
return false;
}
int[] index = hashFunction.hash(str);
for (int i = 0; i < index.length; i++) {
bitSet.set(index[i]);
}
usedSize++;
return true;
}
public boolean contains(String str) {
if (str == null) {
return false;
}
int[] index = hashFunction.hash(str);
for (int i = 0; i < index.length; i++) {
// 存在一定的误判率
if (!bitSet.get(index[i])) { // 有一个不存在则不存在
return false;
}
}
// 只有所有的都存在,才可能存在
return true;
}
public int getUsedSize() {
return usedSize;
}
}
注意:
1、布隆过滤器虽然可以节省很多的空间,并且实现高效率的查找,但是在查找的过程中会出现一定的误判率。因为不同的字符串通过三个不同的哈希函数可能会计算出相同的下标和比特位,因此,就导致了误判。
2、正因为误判的出现,致使我们不能随便删除布隆过滤器重记录的元素,因为删除的元素记录,不一定是我们需要的。
布隆过滤器优点
1、增加和查询元素的时间复杂度为:O(K),(K为哈希函数的个数,一般比较小),与数据量大小无关。通过K个哈希函数计算处下标存放在位图中。
2、哈希函数相互之间没有关系,方便硬件并行运算。
3、布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。
4、在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。
5、数据量很大时,布隆过滤器可以表示全集,其他数据结构不能。就是因为其底层使用的位图。
6、使用同一组散列函数的布隆过滤器可以进行交、并、差运算。因为同一个字符串使用相同的哈希函数计算出来的下标是一样的。
布隆过滤器缺陷
1.有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)。
2.不能获取元素本身。只是知道是否存在。
3.一般情况下不能从布隆过滤器中删除元素。
4.如果采用计数方式删除,可能会存在计数回绕问题。就是我们在C语言阶段学习的数字轮问题。例如:char类型的 127 + 1 = -128,导致了这种回绕问题。
与海量数据相关的面试题
哈希切割
题目:给一个超过100G大小的log file,log中存着 IP地址,设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?
思路:100G的内存,在我们普通的电脑上肯定是不可能运行的。因此,只能把数据变小。怎么边呢?把数据均匀切割吗?不行的,因为我们是要找出出现次数最多的。因此可以把相同的 IP地址存放到一起,也就是把数据切割成若干份,然后遍历文件,将相同的 IP地址存放到一起,并记录下来,最后再去找次数最多的 和 top K的IP。
这里运用的就是哈希切割。把数据相同的分割出来放置一起。
位图应用
1、给定100亿个整数(int类型),设计算法找到只出现一次的整数?
思路:100亿个整数,经过转换大约是38G的内存。直接用线性表这种数据结构去存储肯定是不行的,内存太大了。因此,我们就想到了用位图来解决。int 类型的整数,范围是42亿,因此申请42亿个比特位即可。经过转换,只需大约500MB即可解决。
因此,用一个位图来记录第一次出现的元素,如果重复出现的话,就用第二个位图来记录下来。当我们去遍历时,就只需要看第一个位图中有的元素,而第二个位图中没有的元素即可。
2、给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
思路:通过前面我们得知:100亿个整数,经过转换大约是38G的内存,普通的数据结构是不行的,因此只能用位图来解决。而位图只需大约500MB。因此分别遍历存储到两个位图之后,我们采取 & 的方式求得交集。
拓展:并集 ------> | 求得; 差集 ------> ^ 求得。
3、位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
思路:用一个位图记录出现一次的数字,用一个位图记录出现过两次的数字。如果两个位图都为1或者0,那么就不符合要求:一次也没出现或者出现次数在3次及以上。
布隆过滤器
1、给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
思路:交集用的就是位图与上一致。近似算法就是用布隆过滤器来处理。因为其有一定的误判率。精确算法就是直接用位图来处理。
2、何扩展BloomFilter使得它支持删除元素的操作
思路一:如果直接将该元素所对应的二进制比特位置0,那么可能会导致另外的元素也会被删除。 一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。但也有确定会导致出现数字轮的回绕问题。
思路二:用两个布隆过滤器来记录。一个专门用来记录插入的元素,一个专门用来记录删除的元素。如果在查找时确定这个元素在第一个布隆过滤器中且第二个不存在,即证明这个元素存在。
好啦!本期 数据结构之位图与布隆过滤器 的学习之旅就到此结束啦!我们下一期再一起学习吧!