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

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

1. 位图

腾讯面试题:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,

如何快速判断一个数是否在这40亿个数中。

根据我们现有的知识,该如何处理上面的问题呢?

1. 遍历,时间复杂度O(N)

2. 排序(O(NlogN)),利用二分查找: logN

3. 红黑树 / 哈希表。


还有很多其他的方式,但是这些方式都行不通,


先来口算一下40亿的无符号整数占用多大的内存空间:


1024字节 = 1KB


1024KB = 1MB


1024MB = 1GB


10 0000 0000(九个零 -> 10亿)


10亿个字节 ≈ 1GB。

40亿个字节 ≈ 4GB。

40亿个无符号整数 ≈ 16GB。

而一般的内存根本放不下这么多的数据,无论是上面的哪种方法,都需要存放数据本身,即使是用数组来存放都需要16GB,如果用红黑树(有三叉链,颜色)需要大的内存,哈希表虽然少一点,但是仍然有next指针,还是存放不下。


问题中只要求判断一个数是否在这40亿个数据中,所以可以不存放数据本。

可以采用下面的位图的方式来处理这个问题。

1.1 位图的概念

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

通常是用来判断某个数据存不存在的。1字节 = 8 bit。

       位图就是哈希结构,这里我们用直接定址法,1表示在,0表示不在,就能很好处理这个面试题。对于40亿个数据,至少需要40亿个比特位才能标识它们的状态。

       对于这种情况一般选择2^32个比特位:2^32 = 42亿9千多万,40亿个数据完全可以表示的下,此时相当于一个数组,有2^32个元素,每个元素是一个比特位。

使用位图方式占用的内存就小多了:

1个字节等于8个比特位,8 = 2^3,1024 = 2^10。

  • 2^32个比特位 = 2^29个字节 = 2^19KB = 2^9MB = 512MB = 0.5GB
  • 从最开始需要16GB内存空间直接下降到了需要0.5GB的空间。

但是在语言层面上并没有比特位的数组。

  • 2^32个比特位可以用2^27个int类型的数组来表示。
  • 也可以用2^29个char类型的数组来表示。

随便例举一些数字,如上图所示,这里采用char类型为数组的基本单位。


数据范围是1到22,所以需要3个char类型的变量。

下标为1的比特位表示数字1的存在情况,下标为18的比特位表示数字18是否存在。

上图中,存在3个char类的变量,一共24个比特位,整体标号的话是0~23。

0~7使用第一个char类型的变量。

8~15使用第二个char类型变量。

16~23使用第三个char类型变量。

这3个char类型的变量是用一个数组实现的,即char [3]。


这3个char类型变量的地址从左到右依次升高。


每个char类型中比特位却是:低的比特位在右,高的比特位在左。

这是由我们的使用习惯决定的,比如3用二进制表示就是11,6用二进制表示就是100,


低比特位在右,高比特位在左。

不使用int类型数组的原因:(用int也可以)

我们知道,数据在内存中的存储是有大小端的,如果使用int类型的数组,上图就变成:

       一个int就是4个字节,8个比特位只需要一个int类型的数据就够了,并且还多出8个比特位。假设上图中是小端存储方式,并且是处理完的位图,此时将这份代码换到了大端存储方式的机器上:


       此时位图结构就变成了下图中所示,原本表示数字0~7的8个比特位放在了高地址处,变成了表示24 ~31的8个比特位。

e1d70fd31df346149eaac4d0ce4eccfe.png

        原本在小端机上的程序在大端机上极有可能出现BUG。而采用char类型数组就不用考虑大小端的问题,因为一个char类型就是一个字节,每个char都是从低地址到高地址排列。


       上面是在内存中存储的真实样子,我们在使用的时候无需知道位图在内存中样子。这种方式就是一种哈希思想,将数据直接映射到位图上。


如何确定一个数据映射在位图的哪个比特位呢?以整数18为例说明:


       8映射在位图的下标为2的八个比特位中的某一个,也就是第三个char类型变量。具体映射在下标为2的char类型变量中下标为2的比特位上,也就是在这个char类型中第三个比特位上。


确定映射到char类型变量的下标:18 / 8 = 2。

确定映射到比特位的下标:18 % 8 = 2。

       可以根据上面的图确定一下,发现和我们算出来的结果是一样的。求其他数据的映射位置时,只需要将18换成对应数据即可。

1.2 位图的实现

BitSet.h:

#pragma once
 
#include <iostream>
#include <vector>
using namespace std;
 
namespace rtx
{
  template<size_t N>
  class bitset //构造函数开空间,只需开N / 8 + 1
  {
  public:
    bitset()
    {
      //_bits.resize(N / 8 + 1, 0); 
      _bits.resize((N >> 3) + 1, 0); // 即上面注释的,效率快一点点
    }
 
  protected:
    vector<char> _bits;
  };
}

使用非类型模板参数,该参数用来指定位图比特位的个数。

底层使用的是vector,vector中存char类型变量。

在构造函数中需要指定vector的大小,否则vector的大小是0,一个比特位也没有。


非类型模板参数N指定是比特位的个数,而构造函数开辟的是char类型变量的个数,所以需要N / 8。

由于N / 8的结果不是整数时会取整而抛弃小数部分,所以需要在N /8 后再加1,也就是再增加 8 个比特位来确保位图够用。

       CPU在计算除法的时候,其实是很复杂的,而进行移位运算就很简单。N / 8相当于N右移3位。所以我们使用移位运算来代替除法来提高效率,需要注意的是,加法的优先级比移位运算高,所以必须给(N>>3)加括号,否则就是成了 N>>4了。


下面来写bitset的接口函数:

set();  该接口的作用是将x映射在位图中的比特位置1,表示该数据存在。

  • 首先将x映射在位图中的位置计算出来。
  • 然后将映射的比特位置1。

怎么将对应的比特位置1?这就要我们以前C语言学的知识:

如上图所示,要将一个char类型中的8个比特位的某一个位置一而不影响其他位,就需要或等一个只有那个位是1其他位都是0的char类型,这样一个char类型可以通过1左移固定位数得到。

    void set(size_t x)
    {
      size_t i = x >> 3; // 将x映射在位图中的位置计算出来。
      size_t j = x % 8; // //映射到char中第几个比特位
 
      _bits[i] |= (1 << j); //将映射的比特位置1。
    }

现在来实现reset();该接口的作用是将x映射在位图中的比特位置0,表示该数据不存在。

和set的思路一样同样先计算处x所在位图中的位置。 然后再进行置0。

怎么将对应比特位置0?上面是或等,这里就要与等一个数。


       这里与等一个只有那个位是0其他位都是1的char类型变量这样一个char类型可以通过1左移固定位数(就是set或等的那个数),然后按位取反得到。

    void reset(size_t x)
    {
      size_t i = x >> 3; // 将x映射在位图中的位置计算出来。
      size_t j = x % 8; // //映射到char中第几个比特位
 
      _bits[i] &= ~(1 << j); //将映射的比特位置0,这里~是按位取反,不要用到!逻辑取反
    }


现在来实现test(); 该接口的作用是在位图中查找数据x是否存在。

  • 首先计算出x映射在位图中的位置。
  • 然后看该比特位是0还是1。


       如上图所示,判断某个比特位是1还是0,需要与一个只有这个位是1其他位都是0的char类型变量,如果这个bit是0,那么与以后的结果就是0,对应的bool值flase,如果这个bit是1,那么与以后的结果就不是0,对应的bool值是true。


bool值本质上是4个字节的整形,所以这里涉及到了整形提升,但是并没有影响。

如果与以后的结果是0,整形提升后的结果仍然是0,bool值就是false。

如果与以后的结果非0,即使符号位是1,整形提升和的结果仍然非0,bool的值就是true。

    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值
    }

位图主要的接口就是这三个,下面来测试一下:

#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;
}
 
int main()
{
  test_bitset();
 
  return 0;
}


STL中的位图:

在STL库中,是存在位图的,但是用的比较少。

上面实现的这3个操作也是有的,当然它还提供了其他的接口,这里就不介绍了。

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

目录
相关文章
|
2月前
|
存储 算法 C语言
通义灵码在考研C语言和数据结构中的应用实践 1-5
通义灵码在考研C语言和数据结构中的应用实践,体验通义灵码的强大思路。《趣学C语言和数据结构100例》精选了五个经典问题及其解决方案,包括求最大公约数和最小公倍数、统计字符类型、求特殊数列和、计算阶乘和双阶乘、以及求斐波那契数列的前20项和。通过这些实例,帮助读者掌握C语言的基本语法和常用算法,提升编程能力。
70 4
|
2月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
ly~
|
2月前
|
网络协议 算法 关系型数据库
C语言的应用
C 语言因其高效性和对硬件的直接访问能力,在多个领域有广泛应用。在系统软件领域,它被用于开发操作系统(如 Unix 和 Linux 的内核)和嵌入式系统(如汽车电子控制系统)。在游戏开发中,C 语言常用于构建游戏引擎的底层部分(如 Unity 和 Unreal Engine 的核心模块)及性能要求高的独立游戏。此外,C 语言也用于数据库管理系统(如 MySQL 和 PostgreSQL 的核心功能)和网络编程(如 TCP/IP 协议栈和网络服务器的核心模块)。
ly~
37 3
|
2月前
|
Java Unix Linux
1.3 C语言的应用范围
C语言自20世纪80年代以来一直是主流编程语言,适用于小型计算机、个人电脑及大型机。因其高效紧凑且易于修改和移植,广泛用于软件开发。尽管后来C++和JAVA流行起来,但C语言仍然是软件行业核心,并在嵌入式系统、科学编程和操作系统开发如Linux中扮演重要角色。即使到现在,掌握C语言仍是一项重要技能。不是必须得是计算机专家才能使用C语言,学习C语言同时也能学到很多C++的知识。
50 8
|
2月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
58 2
|
3月前
|
编译器 C++
【C++核心】函数的应用和提高详解
这篇文章详细讲解了C++函数的定义、调用、值传递、常见样式、声明、分文件编写以及函数提高的内容,包括函数默认参数、占位参数、重载等高级用法。
25 3
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
36 3
|
1天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
22 6
|
18天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
31 6
|
2月前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
43 10