位图/布隆过滤器/海量数据处理方式

简介: 位图、布隆过滤器、海量数据处理解法。

位图

位图的概念

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

直接来看问题:

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

这40亿个数中。

思路:解决问题的方法,可以使用位图来解决。把这40亿个数据映射在位图上,将位图上对应的比特位置为1。然后拿着需要判断的数在位图上看看其对应的比特位是否为1,如果是则存在,否则为0。

具体做法:

使用直接定址法,这40亿个数据的值是几,就把第几个比特位标记为1。因为40亿个整数,大概需要16G内存,而使用比特位,我们只需使用char作为存储在vector上的类型,每一个都是1bit大,因此在vector上开辟2^32大小的空间,表示数据大小范围,一共512M。

[L%@O2FPEY64{20H7U20D61.png

开辟好空间后,开始将每一个数据映射到位图上。每一个char对象为8bit,于是让每一个值先确定自己在哪个char对象上,然后确定映射在哪个比特位上。

x映射的值,在第 x/8 个char对象上。

x映射的值,在第 x%8 个比特位上。

所以,我们可以根据上面的理论,用代码简单实现位图

使用非模板参数N,作为数据的个数。

开辟空间:空间开辟的大小为N /8 +1,因为N个数据,每8个为一组,多开辟一组,避免N不是8的整除。然后初始化为0。即位图上的比特位一开始全是0.

//初始化空间,初始值为0
    bitset()
    {
      _bits.resize((N >> 3) + 1, 0);
    }

image.gif

数据映射位图上的比特位:先计算好数据所在的组别和比特位的位置,然后将其置为1。置为1的操作是让这一个char对象组别的比特位与这个数据的比特位进行或运算。

[{N51IQ00(PF2V4O4$5XAS3.png

void set(size_t x)
    {
      size_t i = x >> 3;//位于哪一个char对象上
      size_t j = x % 8;//位于这个char对象上的哪个比特位上
      _bits[i] |= (1 << j);//通过或运算,将x对应的比特位变为1
    }

image.gif

将某个数据映射的比特位从1变回0:同样的找到这个位置后,然后这一组别的比特位与这个数据的比特取反后进行与运算。

W01~FI}9XSC0JI4BGFBCMRU.png

void reset(size_t x)
    {
      size_t i = x >> 3;
      size_t j = x % 8;
      _bits[i] & = (~(1 << j));//通过与运算,让x对应的比特位变为0
    }

image.gif

判断一共数据是是否存在:同样,先计算出这个数据映射的位置。然后返回这一组别跟这个数据的比特,然后进行与运算,注意不是与等,是不能改变原本位图的比特位的。

[$A5R((9$A(GFKAIQRA6ZW4.png

//判断x是否存在,如果存在返回true
    bool test(size_t x)
    {
      size_t i = x >> 3;
      size_t j = x % 8;
      return _bits[i] & (1 << j);
    }

image.gif

完整代码如下:

namespace my_BitSet
{
  template<size_t N>
  class bitset
  {
  public:
    //初始化空间,初始值为0
    bitset()
    {
      _bits.resize((N >> 3) + 1, 0);
    }
    void set(size_t x)
    {
      size_t i = x >> 3;//位于哪一个char对象上
      size_t j = x % 8;//位于这个char对象上的哪个比特位上
      _bits[i] |= (1 << j);//通过或运算,将x对应的比特位变为1
    }
    void reset(size_t x)
    {
      size_t i = x >> 3;
      size_t j = x % 8;
      _bits[i] & = (~(1 << j));//通过与运算,让x对应的比特位变为0
    }
    //判断x是否存在,如果存在返回true
    bool test(size_t x)
    {
      size_t i = x >> 3;
      size_t j = x % 8;
      return _bits[i] & (1 << j);
    }
  private:
    vector<char> _bits;
  };
}

image.gif

布隆过滤器

位图对于判断大量数据中是否存在某一个数据的情况固然是好,其优点是节省空间和判断速度块。但其缺点是一般要求范围相对集中,如果范围特别分散,那么空间消耗就大了,而且是只针对整型。因此,布隆过滤器降临!

布隆过滤器的概念

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

一般的位图下,每一个数据只跟位图产生一个映射点,而且只能用于整型。但布隆过滤器是每一个数据可以有N个映射点,N个映射点对应于N个哈希函数,这个是我们自己定义的。用哈希函数将非整型转化成整型。

Q3_]AUGEDAE0J(OE(IQT6F3.png

布隆过滤器的长度的计算方式:

使用公式:00BU(]DH]MR%7EJV%YRZ}OM.png

K为哈希函数的个数,m为布隆过滤器长度,n为数据的个数。假设K为3,而ln2约等于0.7,因此m==4.2n。

布隆过滤器的功能支持:

布隆过滤器支持set和test方法,最好不要有将1变回0的操作。因为这样会导致其它数据的判断的误差。如果真的要支持,就用计数的方法,但这种方法不推荐。

简单实现代码如下

这里使用3个哈希函数,分别为:BKDRHash、APHash和DJBHash。使用string为类型。

set方法:

void set(const K& key)
    {
      //通过不同的哈希函数,让同一个数据可以计算出三个不同的位置
      size_t hash1 = HashFunc1()(key) % (N * X);
      size_t hash2 = HashFunc2()(key) % (N * X);
      size_t hash3 = HashFunc3()(key) % (N * X);
      //计算出位置后,使用位图的set方法将位图上对应的比特位进行0变1
      _bs.set(hash1);
      _bs.set(hash2);
      _bs.set(hash3);
    }

image.gif

test方法:

bool test(cost K& key)
    {
      //先逐个位置判断,如果它是0,直接返回false
      size_t hash1 = HashFunc1()(key) % (N * X);
      if (!_bs.test(hash1))
      {
        return false;
      }
      size_t hash2 = HashFunc2()(key) % (N * X);
      if (!_bs.test(hash2))
      {
        return false;
      }
      size_t hash3 = HashFunc3()(key) % (N * X);
      if (!_bs.test(hash3))
      {
        return false;
      }
      //直到最后,说明该数据是存在的,返回true
      return true;
    }

image.gif

整体代码如下:

namespace my_BloomFilter
{
  struct BKDRHash
  {
    size_t operator()(const string& key)
    {
      size_t hash = 0;
      for (auto ch : key)
      {
        hash *= 131;
        hash += ch;
      }
      return hash;
    }
  };
  struct APHash
  {
    size_t operator()(const string& key)
    {
      unsigned int hash = 0;
      int i = 0;
      for (auto ch : key)
      {
        if ((i & 1) == 0)
        {
          hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));
        }
        else
        {
          hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));
        }
        ++i;
      }
      return hash;
    }
  };
  struct DJBHash
  {
    size_t operator()(const string& key)
    {
      unsigned int hash = 5381;
      for (auto ch : key)
      {
        hash += (hash << 5) + ch;
      }
      return hash;
    }
  };
  template<size_t N,
    size_t X = 5,
    class K = string,
    class HashFunc1 = BKDRHash,
    class HashFunc2 = APHash,
    class HashFunc3 = DJBHash>
  class BloomFilter
  {
  public:
    void set(const K& key)
    {
      //通过不同的哈希函数,让同一个数据可以计算出三个不同的位置
      size_t hash1 = HashFunc1()(key) % (N * X);
      size_t hash2 = HashFunc2()(key) % (N * X);
      size_t hash3 = HashFunc3()(key) % (N * X);
      //计算出位置后,使用位图的set方法将位图上对应的比特位进行0变1
      _bs.set(hash1);
      _bs.set(hash2);
      _bs.set(hash3);
    }
    bool test(cost K& key)
    {
      //先逐个位置判断,如果它是0,直接返回false
      size_t hash1 = HashFunc1()(key) % (N * X);
      if (!_bs.test(hash1))
      {
        return false;
      }
      size_t hash2 = HashFunc2()(key) % (N * X);
      if (!_bs.test(hash2))
      {
        return false;
      }
      size_t hash3 = HashFunc3()(key) % (N * X);
      if (!_bs.test(hash3))
      {
        return false;
      }
      //直到最后,说明该数据是存在的,返回true
      return true;
    }
  private:
    std::bitset<N* X> _bs;
  };
}

image.gif

海量数据处理问题

哈希切割

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

超过100G大小的文件,肯定不能直接放到内存中,而是通过将它切割,分成很多份。那么如何去切割呢?是平均分成100份,每一份1G这样吗?

如果平均切割,那么会导致的问题是:如果文件中有好几个相同的值,且分布不集中,此时平均切割就很可能使一个IP有很多份在很多小文件中。

因此不能平均切割,需要的是哈希切割。哈希切割就是通过取模,让取模结果相同的数据放到同一份小文件里面。

哈希切割后,通过map来对每一个小文件进行统计。

7IT8F1EYK)W[MY]J4I5CRLG.png

小问题如果超过1G的问题:

①不重复的IP有很多个,map就需要很多节点,因此map是统计不下来的。

②重复的IP有很多个,map可以统计下来,因为节点不多。

解决方法:

先不看什么情况,直接用map统计,如果是第二种情况的话就直接统计下来了。但是第一种情况,会在insert的时候失败,因此可以在失败的时候捕捉异常,接着换哈希函数递归切分再统计即可。

位图的应用

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

只出现一次,那就说明,它在位图中比特位是:01。如果找到该位置发现是00或11或者其它的情况,那就不是。

但一个一般的位图只会出现单个比特,即要么是0,要么是1,不会出现两个比特。这里的方法使用两个位图的结构。即定义两个位图,然后用同一个数据计算出来的同一个位置,分别在这个两个位图上进行0和1的操作。

ARDK9VD}Q9DP{}5(_]XYJOB.png

简单的代码实现:

template<size_t N>
  class twobitset
  {
  public:
    void set(size_t x)
    {
      //初次映射:两个位图对应的比特位都为0,即00
      if (!_bs1.test(x) && !_bs2.test(x)//  00
      {
        _bs2.set(x);//  01
      }
      else if (!_bs1.test(x) && _bs2.test(x) //  01
      {
        //第二次遇到这个数字后,此时是01的,要变成10
        _bs1.set(x); //11
        _bs2.reset(x); // 10
      }
      //如果第三次遇到,也不用管了,第二次遇到的时候就已经不是它了
      //10
      //11
    }
    void PrintOnce()
    {
      for (size_t i = 0; i < N; ++i)
      {
        if (!_bs1.test(i) && _bs2.test(i))  // 01 出现一次
        {
          cout << i << endl;
        }
      }
      cout << endl;
    }
  private:
    bitset<N> _bs1;
    bitset<N> _bs2;
  };
}

image.gif

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

这里提供两种思路:

思路1:先将一个文件的数据映射到位图中,然后用另外一个文件的数据去遍历,得到交集,需要注意去重。

思路2:分布将两文件映射到两个位图,然后通过两位图的与运算判断是否有交集。

3.位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。

这道问题跟第一个问题基本一样,就是让“01”和"10"为需要找到的整数。如果出现"11"以上,那么就不行。

布隆过滤器的应用

1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。

query是一般为一个查询指令,可能是一个网络请求的指令,也可能是一个数据库sql语句。

精确算法找文件交集的思路是:分别给两个文件创建布隆过滤器,然后让它们进行哈希切割,分成一个个小文件。最后通过编号相同的小文件中查找交集。

近似算法的思路是:将一个文件的数据映射到一个布隆过滤器中,然后另外一个文件去查找有没有相同的,有就是交集。这种算法会造成误判。

相关文章
|
6月前
|
人工智能 5G Windows
十分钟私有化部署DeepSeek R1
DeepSeek本地化部署支持下载1.5b、7b、8b、14b、32b等不同参数规模的大模型,适合逻辑推理和计算类问题。普通电脑建议选择1.5b模型以避免AI幻觉。部署需使用Ollama工具下载模型,并通过Chatbox AI等客户端进行配置,确保Ollama运行状态。显卡内存为主要资源占用,各模型占用情况不同,请确保硬盘空间充足。
885 11
|
7月前
|
人工智能 JavaScript 前端开发
字节最新AI 版IDE:用Trae开发网站打包信息追踪插件,国产版Cursor表现如何?
本文介绍了如何使用字节最新推出的AI编程工具Trae,通过零代码方式快速开发一款名为`dist-info`的前端插件。该插件能够将Git信息或自定义内容注入HTML文件中,兼容Webpack和Vite项目。开发者只需在浏览器控制台输入`info`,即可轻松查看代码的相关信息。文章详细描述了插件的背景、开发流程、核心代码实现以及优化建议,并展示了如何借助Trae高效完成项目搭建和代码编写。
778 0
字节最新AI 版IDE:用Trae开发网站打包信息追踪插件,国产版Cursor表现如何?
Vue3级联选择(Cascader)
这是一个可定制的级联选择器组件,支持多级下拉选项。主要属性包括:数据源、文本字段、值字段、后代字段、占位符文本、选择行为、间距、宽度、高度、禁用状态、清除功能、搜索功能及过滤函数等。组件可根据需求灵活配置,并支持动态更新与事件回调。在线预览和详细示例可见[这里](https://themusecatcher.github.io/vue-amazing-ui/guide/components/cascader.html)。
760 2
Vue3级联选择(Cascader)
|
Java API Spring
集成EasyPoi(一个基于POI的Excel导入导出工具)到Spring Boot项目中
集成EasyPoi(一个基于POI的Excel导入导出工具)到Spring Boot项目中
1087 1
|
机器学习/深度学习
探索Transformer在金融行情预测领域的应用——预测银行间回购市场加权价格
文章先发在公众号上来,顺便在这里也写一下,主要思路其实就是模仿盘古天气大模型的方法,来试试能不能用来预测全国银行间市场质押式回购每日的加权平均价格,目前模型主要架构和训练粗略的跑了出来,效果不是太好,目前看了点其他paper,希望尝试利用已经开源的各种大模型做微调。欢迎大家批评指正。
探索Transformer在金融行情预测领域的应用——预测银行间回购市场加权价格
|
存储 运维 监控
深入理解 Linux 文件系统的层次结构
【4月更文挑战第14天】本文将探讨 Linux 操作系统的文件系统层次结构,这是每个系统管理员和开发人员必须掌握的核心知识。我们将从文件系统的顶层目录开始,逐步深入到每个目录的特定用途和重要性,以及它们如何协同工作以支持 Linux 系统的正常运行。
|
搜索推荐 小程序 前端开发
微信小程序|美食推荐系统的设计与实现
微信小程序|美食推荐系统的设计与实现
253 0
|
传感器 数据格式
【STM32】DHT11温湿度模块传感器详解&代码
【STM32】DHT11温湿度模块传感器详解&代码
|
搜索推荐 测试技术 C语言
设计求解AOE网关键路径程序(详细设计,附完整实现代码)
设计求解AOE网关键路径程序(详细设计,附完整实现代码)
|
监控 Shell 开发工具
Debian安装与基本使用:详细指南及常见问题解析
【4月更文挑战第13天】本文档介绍了Debian的安装步骤、基本使用、问题解析及进阶技巧。首先,安装Debian涉及下载ISO镜像,制作启动介质,设置BIOS,然后进行安装过程,包括选择语言、分区、网络配置、软件包选择和用户账户设置。安装完成后,学会基本操作,如命令行使用、软件管理(apt)、系统更新和维护。遇到问题时,解决无线网络、分辨率、输入法和依赖问题。进阶技巧包括自定义Shell环境、使用虚拟化技术(Docker、LXC/LXD)、系统监控与性能调优,以及Git和自动化脚本的高级应用。通过学习这些技巧,可提升在Debian系统上的工作效率。
1864 0