位图与布隆过滤器

简介: 位图与布隆过滤器



一、位图的引入

我们先来看一道题。

题目描述:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。

思路一:遍历

拿到这道题,我们最先想到的方法就是直接遍历了。从头到尾遍历一遍,一定可以判断出一个数是否在其中。时间复杂度为 o(N)。

思路二:排序+二分查找

时间复杂度为 排序 o(N*logN) + 二分查找 o(logN)。

思路三:位图

上面两种方法很常用,但是在面对40亿个数据时,无论是空间复杂度还是时间复杂度都很大。那么有没有比它们更加优秀的方法呢?

这里,我们就可以用位图去帮助我们解决这个问题:数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。


二、概念

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用

来判断某个数据存不存在的。

如下图就是一个位图


三、位图的实现

在标准库里也是实现了位图的。

上图只截取了它的三个主要操作。下面我们分别来讲一讲它的三个操作。

1、set

set的作用是将x对应的比特位置设为1,表示其存在。相当于插入的作用。

void set(size_t x)
{
  size_t i = x / 8;
  size_t j = x % 8;
  _bits[i] |= (1 << j);
}

2、reset

reset的作用是将x对应的比特位置设为0,用来表示删除。

void reset(size_t x)
{
  size_t i = x / 8;
  size_t j = x % 8;
  _bits[i] &= ~(1 << j);
}

3、test

test的作用是用来查看x在不在。

bool test(size_t x)
{
  size_t i = x / 8;
  size_t j = x % 8;
  return _bits[i] & (1 << j);
}

4、位图实现

#pragma once
#include<iostream>
#include<vector>
using namespace std;
namespace zdl
{
  template<size_t N>
  class bitset
  {
  public:
    bitset()
    {
      _bits.resize(N / 8 + 1, 0);
    }
    void set(size_t x)
    {
      size_t i = x / 8;
      size_t j = x % 8;
      _bits[i] |= (1 << j);
    }
    void reset(size_t x)
    {
      size_t i = x / 8;
      size_t j = x % 8;
      _bits[i] &= ~(1 << j);
    }
    bool test(size_t x)
    {
      size_t i = x / 8;
      size_t j = x % 8;
      return _bits[i] & (1 << j);
    }
  private:
    vector<char> _bits;
  };
  void test01()
  {
    bitset<100> bs;
    bs.set(8);
    bs.set(9);
    bs.set(20);
    cout << bs.test(8) << endl;
    cout << bs.test(17) << endl;
        bs.reset(8);
    cout << bs.test(8) << endl;
  }
}


四、位图的应用

我们直接来看相关的题。

1、给定 100 亿个整数,设计算法找到只出现一次的整数。

我们首先来分析一下:我们需要从100亿个整数中,找到只出现一次的整数,数据量很大,那么我们就可以考虑使用位图的思想。而且这些数有三种状态,即:出现0次,1次以及1次以上。所以说我们只要保存一个数是否是三种状态的一种。但是,位图只能有两种状态:存在或者不存在。那么我们应该怎么解决这个问题呢?

这里我们就可以使用两个位图来帮助我们存储三种状态。如下图:

template<size_t N>
class twobitset
{
public:
  void set(size_t x)
  {
    if (!_bs1.test(x) && !_bs2.test(x))//00
    {
      _bs2.set(x);//01
    }
    else if (!_bs1.test(x) && _bs2.test(x))//01
    {
      _bs1.set(x);
      _bs2.reset(x);//10
    }
    //10不变
  }
private:
  bitset<N> _bs1;
  bitset<N> _bs2;
};

2、给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件的交集?

思路:将两个文件中的整数分别映射到两个位图中,然后将两个位图按位与,得到一个位图,如果这个位图的某个位上是1,那这个就是交集。


五、布隆过滤器

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?

1、布隆过滤器的引入

针对上面的问题,我们有如下的解决方法:

1、用哈希表存储用户记录,缺点:浪费空间。

2、用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。(不同的字符串,转化成整型后,可能会映射到相同的位置

而解决这类问题的最好方法,就是使用布隆过滤器。将哈希与位图结合,即布隆过滤器。

2、概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

根据上图,我们可以简单总结一下布隆过滤器的思想:同一个数据,根据不同的哈希函数转换成不同的整型,来映射到不同的位。也就是说同一个数据会映射多个位。这样就可以区分不同的字符串了。当然,也不排除不同字符串任然会映射到完全相同位置的可能。但是,哈希函数越多,可能性越小。

我们首先根据大佬们总结出来的算法,来确定几个哈希函数(这里我们确定三个)。

struct BKDRHash
{
    size_t operator()(const string& s)
    {
        // BKDR
        size_t value = 0;
        for (auto ch : s)
        {
            value *= 31;
            value += ch;
        }
        return value;
    }
};
struct APHash
{
    size_t operator()(const string& s)
    {
        size_t hash = 0;
        for (long i = 0; i < s.size(); i++)
        {
            if ((i & 1) == 0)
            {
                hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
            }
            else
            {
                hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
            }
        }
        return hash;
    }
}
struct DJBHash
{
    size_t operator()(const string& s)
    {
        size_t hash = 5381;
        for (auto ch : s)
        {
            hash += (hash << 5) + ch;
        }
        return hash;
    }
};

3、实现

* 基本框架

template<size_t N,
        class K = string,
        class HashFunc1 = BKDRHash,
        class HashFunc2 = APHash,
        class HashFunc3 = DJBHash>
class BloomFilter
{
public:
private:
  const static size_t _ratio = 5;
  bitset<_ratio* N> _bits;
};

* 插入:set

void Set(const K& key)
{
  size_t hash1 = Hash1()(key) % (_ratio * N);
  _bits.set(hash1);
  size_t hash2 = Hash2()(key) % (_ratio * N);
  _bits.set(hash2);
  size_t hash3 = Hash3()(key) % (_ratio * N);
  _bits.set(hash3);
}

* 查找:test

bool Test(const K& key)
{
  size_t hash1 = Hash1()(key) % (_ratio * N);
  if (!_bits.test(hash1))
    return false;
  size_t hash2 = Hash2()(key) % (_ratio * N);
  if (!_bits.test(hash2))
    return false;
  size_t hash3 = Hash3()(key) % (_ratio * N);
  if (!_bits.test(hash3))
    return false;
  return true;
}

* 删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

4、布隆过滤器的优点

1、增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关。

2、哈希函数相互之间没有关系,方便硬件并行运算。

3、布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。

4、在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。

5、布隆过滤器的缺点

1、有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)。

2、不能获取元素本身。

3、一般情况下不能从布隆过滤器中删除元素。

4、如果采用计数方式删除,可能会存在计数回绕问题。


六、哈希切割

我们直接来看一个问题

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?如何找到top K的IP?

思路:读取每个ip,算哈希值 i = hash(ip) % 100,这个ip进入第i个小文件(相同的ip,一定进入相同的文件),利用 map<string, int>,对每个小文件统计次数。

若要寻找top K 的 ip,建一个K个值为<ip, cout>的小堆,去解决top K问题。

目录
相关文章
|
存储 数据处理 C++
哈希的应用--位图和布隆过滤器(上)
位图 1. 位图概念 位图(Bitset)是一种数据结构,用于表示一组布尔值,其中每个元素通常对应于一个位或一个二进制值,可以存储0或1。位图在计算机科学和计算机工程中经常用于各种应用,特别是
|
5月前
|
C++
位图和布隆过滤器:位图
位图和布隆过滤器:位图
|
6月前
|
存储 监控 算法
【C++】哈希(位图、布隆过滤器)
【C++】哈希(位图、布隆过滤器)
|
6月前
|
存储 算法 C++
【C++高阶(六)】哈希的应用--位图&布隆过滤器
【C++高阶(六)】哈希的应用--位图&布隆过滤器
|
6月前
|
算法 Go 数据库
数据结构/C++:位图 & 布隆过滤器
数据结构/C++:位图 & 布隆过滤器
43 0
|
存储 算法 Linux
哈希的应用--位图和布隆过滤器(下)
布隆过滤器 在上面我们用位图很好的解决了多重整数高效查询的问题,那么我们在面对字符串时,该如何解决呢? 1. 布隆过滤器的提出 布隆过滤器(Bloom Filter)是由布隆在1970年提出的,它是一种空间效率高、查询速度快的数据结构,主要用于判断一个元素是否属于一个集合。布隆过滤器的提出解决了在大规模数据集中进行
|
6月前
|
存储 Serverless
位图和布隆过滤器
位图和布隆过滤器
|
6月前
|
存储 算法 Linux
C++ 哈希的应用【位图】
C++ 哈希的应用【位图】
60 0
|
存储 机器学习/深度学习 算法
C++位图和布隆过滤器
C++位图和布隆过滤器
|
存储 数据采集 缓存
【C++】位图/布隆过滤器+海量数据处理
【C++】位图/布隆过滤器+海量数据处理
120 1
【C++】位图/布隆过滤器+海量数据处理