从C语言到C++_32(哈希的应用)位图bitset+布隆过滤器+哈希切割(中)

简介: 从C语言到C++_32(哈希的应用)位图bitset+布隆过滤器+哈希切割

从C语言到C++_32(哈希的应用)位图bitset+布隆过滤器+哈希切割(上):https://developer.aliyun.com/article/1522341

1.3 位图解决海量数据面试题

下面是一些海量数据面试题:


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

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

3. 位图应用变形:1个文件有100亿个int,1G内存,如何找到出现次数不超过两次的所有整数?


这三道题我们一题一题来看:


问题一:给定100亿个整数,如何设计算法找到只出现一次的整数?

       首先这100亿个数据在内存中肯定是放不下的,所以之前学习的存放数据本身的数据结构都用不了,只能用位图。位图的一个比特位只有两种状态来表示数据的有无,这里是要统计次数,所以就要让位图不仅仅只有两种状态。这里可以用KV模型,但是想想还有没有更好的方法?

位图在STL库里有,虽然只是K模型的,但是我们用两个位图就能很好的解决这个问题:

创建两个2^32比特位的位图结构,如上图所示。

  • 两个位图相同下标的两个比特位来表示一个数据的状态。
  • 00表示0次,01表示1次,10表示一次1以上。

完整BitSet.h和two_bitset:

#pragma once
 
#include <iostream>
#include <vector>
#include <bitset>
using namespace std;
 
namespace rtx
{
  template<size_t N>
  class bitset
  {
  public:
    bitset() //构造函数开空间,只需开N / 8 + 1
    {
      //_bits.resize(N / 8 + 1, 0); 
      _bits.resize((N >> 3) + 1, 0); // 即上面注释的,效率快一点点
    }
 
    void set(size_t x)
    {
      size_t i = x >> 3; // 将x映射在位图中的位置计算出来。
      size_t j = x % 8; // //映射到char中第几个比特位
 
      _bits[i] |= (1 << j); //将映射的比特位置1。
    }
 
    void reset(size_t x)
    {
      size_t i = x >> 3; // 将x映射在位图中的位置计算出来。
      size_t j = x % 8; // //映射到char中第几个比特位
 
      _bits[i] &= ~(1 << j); //将映射的比特位置0,这里~是按位取反,不要用到!逻辑取反
    }
 
    bool test(size_t x)
    {
      size_t i = x >> 3; // 将x映射在位图中的位置计算出来。
      size_t j = x % 8; // //映射到char中第几个比特位
 
      return _bits[i] & (1 << j); //与上除了对应比特位是1,其它位都是0的数,得到对应比特位bool值
    }
 
  protected:
    vector<char> _bits;
  };
 
  template<size_t N>
  class two_bitset
  {
  public:
    void set(size_t x)
    {
      bool inset1 = _bs1.test(x); // 测试当前状态
      bool inset2 = _bs2.test(x);
 
      if (inset1 == false && inset2 == false)
      {
        _bs2.set(x); // 00 -> 01
      }
      else if (inset1 == false && inset2 == true)
      {
        _bs1.set(x); // 01 -> 10
        _bs2.reset(x);
      }                // 10 是出现两次或两次以上,不用变
    }
 
    void print_once_num()
    {
      for (size_t i = 0; i < N; ++i)
      {
        if (_bs1.test(i) == false && _bs2.test(i) == true)
        {
          cout << i << endl; // 打印只出现一次的整数
        }
      }
    }
 
  protected:
    bitset<N> _bs1;
    bitset<N> _bs2;
    //std::bitset<N> _bs1;
    //std::bitset<N> _bs2;
  };
}

Test.cpp:

#include "BitSet.h"
 
void test_bitset()
{
  rtx::bitset<100> bs; //上面面试题开范围可以这样开:bitset<-1> bs1;
  bs.set(8);
  bs.set(9);
  bs.set(20);
 
  cout << bs.test(8) << endl;
  cout << bs.test(9) << endl;
  cout << bs.test(20) << endl;
  cout << bs.test(30) << endl << endl;
 
  bs.reset(8);
  bs.reset(20);
 
  cout << bs.test(8) << endl;
  cout << bs.test(9) << endl;
  cout << bs.test(20) << endl;
}
 
void test_two_bitset()
{
  int arr[] = { 3, 4, 5, 2, 3, 4, 4, 4, 4, 12, 77, 65, 44, 4, 44, 99, 33, 33, 33, 6, 5, 34, 12 };
 
  rtx::two_bitset<100> bs;
  for (const auto& e : arr)
  {
    bs.set(e);
  }
 
  bs.print_once_num();
}
 
int main()
{
  //test_bitset();
  test_two_bitset();
 
  return 0;
}


  • 问题二:给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
  • 两个文件都有100亿个整数,必然放不进内存中,所以同样采用位图结构。
  • 每个文件使用一个2^32个比特位的位图,两个文件就是两个位图,占用的内存也就是1GB,符合要求。
  • 两个文件都放进位图,这样就可以去重了,然后将两个位图进行按位与运算,得到的结果中,比特位是1的就是交集。

这里具体的实现就不再写了,要注意体会位图的应用,也就是哈希应用的思想。

  • 问题三:一个文件有100亿个int,1G内存,如何找到出现次数不超过两次的所有整数?
  • 采用的方法是两个位图结构,和问题1一样。
  • 只是这里还需要两个位是11的情况,用来表示3次及以上。

只需要在前面代码增加一种情况的处理即可:


1.4 位图的优缺点

上面就是一些位图的应用,有下面这些要求时应该想到位图:

  • 1. 快速查找某个数据是否在一个集合中
  • 2. 排序 + 去重
  • 3. 求两个集合的交集、并集等
  • 4. 操作系统中磁盘块标记

但是位图有优点也是有缺点的:

优点:节省空间,效率高。(直接定制法,直接开到整形的最大范围就不存在冲突)

缺点:一般要求数据相对集中,否则会导致空间消耗上升。还有一个致命缺点:只能针对整形。

2. 布隆过滤器

       如果我就要使用位图来存放字符串呢?当然也是可以的,只是需要和哈希表一样,将字符串转换成整数。


       如上图所示,将不同的字符串通过hashfunc函数转换成不同的整数,然后将这些整数映射到位图中,从而表示字符串的存在情况。


       但是无论是哪种方式,字符串转换成整数,都有可能让两个不同的字符串转换的整数相同。这就会产生误判的情况,那是判断存在有误判,还是判断不存在有误判,还是都有误判呢?:


位图中存在:不一定真正存在。

如上图中“find”和“insert”转换成的整数都是1234,所以位图中第1234个比特位是1,就可以说“find”和“insert”都存在,但实际上是“insert”存在,而“find”不存在,于是就产生了误判。


位图不存在:必然不存在。

还使用上面的例子,如果位图的第1234个比特位是0,说明“find”和“insert”都不存在。


所以根据位图判断出的结构,不存在是准确的,存在是不准确的。


       有没有办法能提高一下判断的准确率呢?答案是有的,布隆过滤器就可以降低误判率,提高准确率。

2.1 布隆过滤器的概念

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


布隆过滤器:用多个哈希函数,将一个数据映射到位图结构中。

使用两个哈希函数,将同一个字符串转换成两个整数,并且都映射在位图中,如上图所示。

       只有一个字符串在位图中的两个比特位同时为1才能说明该字符串存在。"find"经过哈希函数处理后的两个整数,只有一个是被“insert”映射的,另一个是0,说明“find”不存在。而“insert”经过哈希函数处理后的两个整数,在位图中都有映射,可以说明“insert”存在。


此时降低了误判率:


位图存在:字符串存在的准确率提高,但是仍有不存在的可能。

       字符串“find”经过两个哈希函数处理后得到两个整数,与字符串“insert”得到的两个整数相同的概率,比之前各自有一个整数相同的概率低的多。但是仍然有可能“find”的两个整数和“insert”的两个整数相同,此时就会又出现误判。


位图不存在:必然不存在。


布隆过滤器对于不存在的判断是准确的,并且可以降低存在时的误判率。

布隆过滤器的应用场景:不需要一定准确的场景,比如注册昵称时的存在判断。

       如上图中,一个昵称的数据库是放在服务器中的,这个数据库中昵称的存在情况都放在了布隆过滤器中,当从客户端注册新的昵称时,可以通过布隆过滤器快速判断新昵称是否存在。


这里对存在的准确率要去就没有太高,布隆过滤器显示存在(不准确),就换一个昵称,显示不存在(准确),就注册这个昵称,并放入数据库中。

通过布隆过滤器查找可以提高效率,如果之前去数据库中查找的话,效率就会大大降低。

哈希函数个数和布隆过滤器长度的关系:


       现在知道布隆过滤器是什么了,但是我们到底该创建多少个比特位的位图(布隆过滤器长度),又应该使用多少个哈希函数来映射同一个字符串呢?


布隆过滤器长度长度开得短了误判率就高,开得长了就存在空间浪费的情况,优点就不明显了。


如何选择哈希函数个数和布隆过滤器长度一文中,对这个问题做了详细的研究和论证:

  • 哈希函数个数和布隆过滤器长度以及误判率三者之间的关系曲线。

最后得出一个公式:

  • m:表示布隆过滤器长度。k:表示哈希函数个数。n:表示插入的元素个数。n2约等于0.69。

2.2 布隆过滤器的实现

       首先需要写几个哈希函数来将字符串转换成整形,各种字符串Hash函数一文中,介绍了多种字符串转换成整数的哈希函数,并且根据冲突概率进行了性能比较,有兴趣的可以自行研究一下。这里选择分数较高的3个哈希函数:

1.struct HashBKDR
{
  size_t operator()(const string& key)
  {
    size_t val = 0;
    for (auto ch : key)
    {
      val *= 131;
      val += ch;
    }
 
    return val;
  }
};
 
struct HashAP
{
  size_t operator()(const string& key)
  {
    size_t hash = 0;
    for (size_t i = 0; i < key.size(); i++)
    {
      if ((i & 1) == 0)
      {
        hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
      }
      else
      {
        hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
      }
    }
    return hash;
  }
};
 
struct HashDJB
{
  size_t operator()(const string& key)
  {
    size_t hash = 5381;
    for (auto ch : key)
    {
      hash += (hash << 5) + ch;
    }
 
    return hash;
  }
};
 
template<size_t N, class K = string, class Hash1 = HashBKDR, class Hash2 = HashAP, class Hash3 = HashDJB>
class BloomFilter // N表示准备要映射N个值
{
public:
 
protected:
  const static size_t _ratio = 5; // 根公式算出来,此时哈希函数是3个,所以m = 3n/ln2 约等于4.2 取5
  std::bitset<_ratio* N>* _bits = new std::bitset<_ratio * N>;
  // 库里的bit是放在栈上的,容易栈溢出,所以自己放到堆上(很挫)用自己写的就是放在堆上的
};

size_t N:最多存储的数据个数。

class K:布隆过滤器处理的数据类型,默认情况下是string,也可以是其他类型。

哈希函数:将字符串或者其他类型转换成整形进行映射,缺省值是将字符串转换成整形的仿函数。

set(): 将数据经过3个哈希函数的处理得到3个整数,

然后将这3个整数都映射到位图中来表示这个数据存在。

  void Set(const K& key)
  {
    size_t hash1 = Hash1()(key) % (_ratio * N); // 注意优先级问题,在最后加括号
    size_t hash2 = Hash2()(key) % (_ratio * N);
    size_t hash3 = Hash3()(key) % (_ratio * N);
 
    _bits->set(hash1);
    _bits->set(hash2);
    _bits->set(hash3);
 
  }

test(): 对每一个哈希函数得到的整数所映射的位置进行判断,如果某个位置不存在直接返回false,说明这个字符串不存在,当所有整数所映射的位置都存在,说明这个字符串存在。

  • 判断每个比特位时,判断它不存在,不要判断它存在,因为不存在是准确的,存在是不准确的。
  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; // 可能存在误判
  }


在这思考:布隆过滤器要不要支不支持删除?

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

       “baidu”和“tencent”映射的比特位都有第4个比特位。删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。


面试题: 如何扩展BloomFilter使得它支持删除元素的操作。


       一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器(引用计数),插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。


但是也存在缺陷,无法确认元素是否真正在布隆过滤器中,甚至会有计数回绕。

总的来说,布隆过滤器最好不要支持删除操作。

从C语言到C++_32(哈希的应用)位图bitset+布隆过滤器+哈希切割(下):https://developer.aliyun.com/article/1522353?spm=a2c6h.13148508.setting.17.50c04f0egKUJB6

目录
相关文章
|
29天前
|
安全 编译器 C语言
C++入门1——从C语言到C++的过渡
C++入门1——从C语言到C++的过渡
45 2
|
8天前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
1月前
|
C语言 C++
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在C语言中,`static`关键字主要用于变量声明,使得该变量的作用域被限制在其被声明的函数内部,且在整个程序运行期间保留其值。而在C++中,除了继承了C的特性外,`static`还可以用于类成员,使该成员被所有类实例共享,同时在类外进行初始化。这使得C++中的`static`具有更广泛的应用场景,不仅限于控制变量的作用域和生存期。
45 10
|
1月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
47 2
|
2月前
|
编译器 C++
【C++核心】函数的应用和提高详解
这篇文章详细讲解了C++函数的定义、调用、值传递、常见样式、声明、分文件编写以及函数提高的内容,包括函数默认参数、占位参数、重载等高级用法。
22 3
|
2月前
|
算法 机器人 C语言
ROS仿真支持C++和C语言
ROS仿真支持C++和C语言
56 1
|
29天前
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
16 0
|
22天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
30 3
|
13天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
30 10
|
6天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。