【C++】哈希(位图,布隆过滤器)(一)

简介: 【C++】哈希(位图,布隆过滤器)

一、位图

1.位图概念

今天的内容从一道面试题开始引入:


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

这40亿个数中。

首先我们对40亿个无符号整数改变一下,它到底是多少G呢?

40亿个整数大概是  40亿*4个字节=160亿个字节

4G=2^32byte,大概为42亿九千万字节,所以1G大概就是10亿字节 ,所以40亿个整数大概就是16G,那这么大数据放到内存中肯定是放不下的,所以什么二分查找,什么map,set更何况还有额外的消耗,这更不可能完成了,于是我们可以利用哈希的思想来搞一个位图!

       判断数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0

代表不存在。

位图是一种直接定址法的哈希,因此效率很高,用O(1)就可以探测到对应位是0还是1,效率非常高,因此可以快速判断。



利用每一个比特位的0或1的情况,来判断数在不在,所以40亿不重复的数,开辟2^32-1个比特位,转化为G,也就512m,内存很小。

举例说明:

 

每个数我们可以先num/8,算出他在第几个char里,然后再num%8算出在哪一位

比如:23/8=2,在第二个char;23%8=7,在第七位上面。

那如何把任意一位置1,且不改变其他位?


把它和左移(向高位移动)以后的1(即其他位是0,只有要改变那一位是1)和原来的数进行或运算,就可以得到结果。保证了其他位不变,只有该位被改变为了1.


那到底怎么移动呢?

(一个char中)


那可能有人就会想,这会不会跟大小端有关系,数据在内存中的存储形式???


错错错,大错特错,首先大小端只存在于大于1字节的数据类型中,其次不管从哪边移动,本质是向高位或者低位移动。


所以说,%8以后,是哪一位,1直接左移几位(即向高位移动)。


那么在把某一位置为1以后,要重新置为0的话,应该怎么搞呢?


同理得:直接将1移位以后,再取反,将结果和原数进行与运算。


那要测试这个数在不在位图中,怎么测试呢?也就是看某一位是不是1


直接返回  1移位以后和原数相与的结果,不为0则存在,为0则不存在。


我们来看代码实现:

template<size_t N>
  class bitset
  {
  public:
    bitset()
    {
      //_bit.resize((N/8) + 1, 0);
      _bit.resize((N >> 3) + 1, 0);//左移3位就相当于/8,效率更快一些,但要注意运算符的优先级
    }
    void set(size_t x)
    {
      size_t i = x >> 3;
      size_t j = x % 8;
      _bit[i] |= (1 << j);//在知道是哪一个char之后,直接把这一个char相与。
    }
    void reset(size_t x)
    {
      size_t i = x >> 3;
      size_t j = x % 8;
      _bit[i] &= (~(1 << j));
    }
    bool test(size_t x)
    {
      size_t i = x >> 3;
      size_t j = x % 8;
      return _bit[i] & (1 << j);
    }
  private:
    vector<char> _bit;
  };

2.位图的应用

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

 

统计次数的话,那么就需要两个位图来实现,两个比特位来统计00(0次),01(1次),10(2次及以上)。

直接上代码:

template<size_t n>
  class two_bitset
  {
  public:
    void set(size_t x)
    {
      if (!_bs1.test(x) && !_bs2.test(x))//00
      {
        _bs2.set(x); //0次变1次
      }
      else if (!_bs1.test(x) && _bs2.test(x))//01
      {
        _bs1.set(x);
        _bs2.reset(x);//1次变两次
      }
    }
    void printonce()
    {
      for (size_t i = 0; i < n; ++i)
      {
        if (!_bs1.test(i) && _bs2.test(i))
        {
          cout << i << endl;
        }
      }
      cout << endl;
    }
  private:
    bitset<n> _bs1;
    bitset<n> _bs2;
  };

一个数如果在两个位图中的同一位置都是0,那么说明就是0次 ,再进来的数就要将00第二位set为01,表示出现一次,后面同理可得。


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

首先我们知道,整数的范围最大是42亿多,所以100亿个整数中,一定存在许多重复的整数。

所以将文件中的数据都放入位图中,只看 存在或者不存在两种状态,这就自动去掉了重复的数,某一位一直是1。只有512MB,可以存入内存中进行处理。

然后两个位图进行相与操作,同时为1说明是交集。

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

首先我们知道,整数的范围最大是42亿多,所以100亿个整数中,一定存在许多重复的整数。

所以将文件中的数据都放入位图中,只看 存在或者不存在两种状态,占用内存很少。

通过两个位图来表示次数,00(0次),01(1次),10(2次),11(3次及以上),然后控制条件(只找01,10)输出结果,和第一个问题其实是一样的。

二、哈希切分

还是一道面试题来引入哈希切分:


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

由前面所学,我们可能会想到位图,但是行不通,要统计次数就要开辟多个位图,

成倍的开辟位图来表示次数的话,会占用大量的内存空间,内存也存不下。

我们用的是map,但是在用map之前,要把大文件处理:

那我们就可以利用哈希的思想来把100G的文件分成100个小文件,每个1G,那么不就可以进内存了吗?

那怎么分???平均分?那当然不行,平均分对于分散的ip地址,都不在同一个小文件中,进入内存用map统计时,结果是不正确的。

直接哈希切分!!

我们可以对100G大文件中的ip进行哈希切分,利用哈希表的思想,将哈希值相同的放入同一个小文件中,然后通过一个一个的小文件进入内存读取并统计个数,搞完一个clear掉,记录再进下一个。


理想很美好,现实却有点骨感?


单个文件超过1G:


因为存在哈希冲突,在数据进入小文件时,就会产生下面两种情况:


       1.一个小文件中,差不多都是不重复的数据,且个数还挺多,且map再加额外开销,导致内存很大,直接报错。

       2.一个小文件中,都是很多重复的数据,且个数还挺多,但是map却可以存下(重复的只增加次数),可以统计。


所以我们无需判断是哪种情况,直接无脑map,第一种情况发生就抛异常,捕获以后,换另一种哈希函数,再进行递归分割,拆成更小的文件后用map统计次数。与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

利用堆来解决topK问题。

三、布隆过滤器

1.布隆过滤器的概念

开始讲布隆过滤器之前,我们要说一说位图的缺点是什么?


目录
相关文章
|
1月前
|
存储 算法 C++
【算法】哈希映射(C/C++)
【算法】哈希映射(C/C++)
|
3月前
|
存储 搜索推荐 Serverless
【C++航海王:追寻罗杰的编程之路】哈希的应用——位图 | 布隆过滤器
【C++航海王:追寻罗杰的编程之路】哈希的应用——位图 | 布隆过滤器
37 1
|
3月前
|
存储 缓存 NoSQL
【C++】哈希容器
【C++】哈希容器
|
3月前
|
存储 Serverless C++
【C++航海王:追寻罗杰的编程之路】一篇文章带你认识哈希
【C++航海王:追寻罗杰的编程之路】一篇文章带你认识哈希
19 0
|
4月前
|
存储 算法 数据挖掘
【C++】位图
【C++】位图
45 1
|
5天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
27 5
|
11天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
40 4
|
13天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
36 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
24 4