从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

目录
相关文章
|
7月前
|
存储 负载均衡 算法
基于 C++ 语言的迪杰斯特拉算法在局域网计算机管理中的应用剖析
在局域网计算机管理中,迪杰斯特拉算法用于优化网络路径、分配资源和定位故障节点,确保高效稳定的网络环境。该算法通过计算最短路径,提升数据传输速率与稳定性,实现负载均衡并快速排除故障。C++代码示例展示了其在网络模拟中的应用,为企业信息化建设提供有力支持。
181 15
|
4月前
|
机器学习/深度学习 存储 算法
基于 C++ 布隆过滤器算法的局域网上网行为控制:URL 访问过滤的高效实现研究
本文探讨了一种基于布隆过滤器的局域网上网行为控制方法,旨在解决传统黑白名单机制在处理海量URL数据时存储与查询效率低的问题。通过C++实现URL访问过滤功能,实验表明该方法可将内存占用降至传统方案的八分之一,查询速度提升约40%,假阳性率可控。研究为优化企业网络管理提供了新思路,并提出结合机器学习、改进哈希函数及分布式协同等未来优化方向。
91 0
|
7月前
|
存储 算法 安全
企业员工数据泄露防范策略:基于 C++ 语言的布隆过滤器算法剖析[如何防止员工泄密]
企业运营过程中,防范员工泄密是信息安全领域的核心议题。员工泄密可能致使企业核心数据、商业机密等关键资产的流失,进而给企业造成严重损失。为应对这一挑战,借助恰当的数据结构与算法成为强化信息防护的有效路径。本文专注于 C++ 语言中的布隆过滤器算法,深入探究其在防范员工泄密场景中的应用。
117 8
|
8月前
|
算法 Serverless 数据处理
从集思录可转债数据探秘:Python与C++实现的移动平均算法应用
本文探讨了如何利用移动平均算法分析集思录提供的可转债数据,帮助投资者把握价格趋势。通过Python和C++两种编程语言实现简单移动平均(SMA),展示了数据处理的具体方法。Python代码借助`pandas`库轻松计算5日SMA,而C++代码则通过高效的数据处理展示了SMA的计算过程。集思录平台提供了详尽且及时的可转债数据,助力投资者结合算法与社区讨论,做出更明智的投资决策。掌握这些工具和技术,有助于在复杂多变的金融市场中挖掘更多价值。
255 12
|
9月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
183 5
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
94 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
170 0
|
6月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
183 12
|
7月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
131 16