从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

目录
打赏
0
1
1
0
47
分享
相关文章
基于 C++ 语言的迪杰斯特拉算法在局域网计算机管理中的应用剖析
在局域网计算机管理中,迪杰斯特拉算法用于优化网络路径、分配资源和定位故障节点,确保高效稳定的网络环境。该算法通过计算最短路径,提升数据传输速率与稳定性,实现负载均衡并快速排除故障。C++代码示例展示了其在网络模拟中的应用,为企业信息化建设提供有力支持。
70 15
从集思录可转债数据探秘:Python与C++实现的移动平均算法应用
本文探讨了如何利用移动平均算法分析集思录提供的可转债数据,帮助投资者把握价格趋势。通过Python和C++两种编程语言实现简单移动平均(SMA),展示了数据处理的具体方法。Python代码借助`pandas`库轻松计算5日SMA,而C++代码则通过高效的数据处理展示了SMA的计算过程。集思录平台提供了详尽且及时的可转债数据,助力投资者结合算法与社区讨论,做出更明智的投资决策。掌握这些工具和技术,有助于在复杂多变的金融市场中挖掘更多价值。
77 12
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
81 5
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
208 0
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
186 23
一文彻底搞清楚C语言的函数
本文介绍C语言函数:函数是程序模块化的工具,由函数头和函数体组成,涵盖定义、调用、参数传递及声明等内容。值传递确保实参不受影响,函数声明增强代码可读性。君志所向,一往无前!
32 1
一文彻底搞清楚C语言的函数
|
3月前
|
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
167 15
|
3月前
|
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
84 24
|
3月前
|
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
173 16
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
96 3
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等