【C++】位图/布隆过滤器+海量数据处理

news2024/12/24 9:12:49

作者阿润菜菜
📖专栏C++


文章目录

  • 前言
  • 一 位图
    • 1.位图法介绍
    • 2.位图实现的细节
  • 二、布隆过滤器
    • 1.布隆过滤器概念
    • 2.布隆过滤器实现
  • 三、海量数据处理
    • 1. 位图应用
    • 2. 哈希切割
    • 3. 布隆过滤器


前言

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

大多数人上来会想到这两种方法:1. 遍历,时间复杂度O(N)2. 排序(O(NlogN)),利用二分查找: logN
但是第一种效率太低了,需要一个一个遍历比对,第二种排序内存无法装得下40亿个整数数据啊!
可以发现问题是需要判断此无符号整数在不在集合中,我们可以用一个数对应一个比特位来标识,42亿个数据换算比特位进行存储,也就是需要0.5GB,512MB内存申请是没有压力的。

下面来学习一下位图法以及相关应用面试题和布隆过滤器知识。

一 位图

1.位图法介绍

位图法是一种利用每一位来存放某种状态的数据结构,适用于大规模数据的快速查找、判重、排序等。在位图法中,一个int类型的数据占用4个字节,即32个bit位,可以表示0-31的数是否存在。

位图申请内存

在内存中我们肯定是不能按照bit位来申请内存的,这不符合内存管理的机制,最小申请的内存也是1byte(字节),即8个bit位。所以在位图里面我们就开出来一个个的char,用每个char的比特位来直接对应数字。

意思是,在位图中,我们不能单独申请一个bit位来存放一个数字的状态,而是要申请一个char类型的数据,即8个bit位,然后用这8个bit位来表示8个数字是否存在。比如,如果我们要表示数字0-7是否存在,我们就可以申请一个char类型的数据a,然后用a的每一位来对应一个数字。如果a的第0位为1,表示数字0存在;如果a的第1位为1,表示数字1存在;以此类推

2.位图实现的细节

我们这里讲解位图的三个主要功能函数:

  • set():置位函数,将指定的位设置为1
  • reset():复位函数,将指定位设置为0
  • test(): 访问函数,获取指定的位的值
  1. 对于set,想要让某一比特位变为1其他位不变,则可以用1按位或对应的比特位,那就只需让1向高位移动j位,然后用位图中对应的char进行按位或等即可。

这句话的意思是,如果我们想要把一个char类型的数据a的第j位设置为1,我们可以先把1左移j位,得到一个只有第j位为1其他位为0的数b,然后把a和b进行按位或运算,得到一个新的数c,这个数c就是把a的第j位设置为1后的结果。因为按位或运算的规则是,只要有一个为1就为1,所以a的其他位不会被改变,只有第j位会被设置为1

例如,如果我们想要把a=0010 1000的第3位设置为1,我们可以先把1左移2位,得到b=0000 0100,然后把a和b进行按位或运算,得到c=0010 1000 | 0000 0100 = 0010 1100,这个数c就是把a的第2位设置为1后的结果。

  1. 对于reset,想要让某一比特位变为0其他位不变,则可以用0按位与对应的比特位,那就只需让1向高位移动j位,然后按位取反,最后用位图中对应的char进行按位与等即可。

  2. 对于test,我们可以让对应比特位按位与1,其他比特位按位与0,这样其他比特位都是0,如果测试的比特位是1,则结果是非0,那就是true,如果测试的比特位是0,则结果是0,那就是false。

// 非类型模板参数
template <size_t N>
class bitset
{
public:
	bitset()
	{
		_bits.resize(N / 8 + 1, 0);
		//可能开的比特位恰好满足数字的个数,也可能最多浪费7个比特位
		//_bits.resize(N << 3 + 1, 0);//位运算符优先级过低,这里先进行+运算,则结果和我们预想的不一致,发生错误。
	}
	void set(size_t x)
	{
		size_t i = x / 8;
		size_t j = x % 8;
		_bits[i] |= 1 << j;
	}
	void reset(size_t x)
	{
		size_t i = x / 8;
		size_t j = x % 8;
		_bits[i] &= ~(1 << j);
	}
	bool test(size_t x)
	{
		size_t i = x / 8;
		size_t j = x % 8;
		return _bits[i] & (1 << j);//这里不是&=,因为test不改变位图,只是判断一下而已
		//有些编译器bool值是四个字节,返回时会发生整型提升,高位补符号位,但这些都不重要,只要是非0就行,判断为真
		//我的编译器bool值是一个字节
	}
private:
	vector<char> _bits;
};

位图的应用

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

二、布隆过滤器

1.布隆过滤器概念

布隆过滤器是一种概率型数据结构,可以用于判断一个元素是否可能存在于一个集合中,其优点是空间效率高,查询速度快,缺点是有一定的误判率和删除困难
将哈希与位图结合,即布隆过滤器
布隆过滤器的应用场景有:

  • 解决缓存穿透问题,即避免频繁查询数据库中不存在的数据
  • 邮件过滤,即用布隆过滤器来存储黑名单邮件地址,过滤掉垃圾邮件
  • 网页爬虫,即用布隆过滤器来记录已经爬取过的网址,避免重复爬取
  • 新闻推荐,即用布隆过滤器来记录用户已经浏览过的新闻,避免重复推荐

2.布隆过滤器实现

布隆过滤器的原理是:

  • 创建一个二进制位数组(bitmap)和一组哈希函数
  • 当要添加一个元素时,用哈希函数计算出该元素在位数组中的多个位置,并将这些位置的值设为1
  • 当要查询一个元素时,用哈希函数计算出该元素在位数组中的多个位置,并检查这些位置的值是否都为1,如果都为1,则认为该元素可能存在;如果有任何一个位置为0,则认为该元素一定不存在
  • 当要删除一个元素时,无法直接将位数组中的对应位置设为0,因为这样可能会影响其他元素的判断,所以需要使用一些变形的布隆过滤器来支持删除操作

布隆过滤器的删除
如果采用计数方式来实现reset,也就是布隆过滤器的删除,会存在一些问题。比如你不小心将某一个字符串多次重复删除,此时计数会进行- -,但如果是0- -呢?有可能还会发生越界访问等问题。所以计数方式也有他的缺陷,最好不要强制增加布隆过滤器的reset操作。

struct BKDRHash
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

struct APHash
{
	size_t operator()(const string& key)
	{
		unsigned int hash = 0;
		int i = 0;

		for (auto ch : key)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));
			}

			++i;
		}

		return hash;
	}
};

struct DJBHash
{
	size_t operator()(const string& key)
	{
		unsigned int hash = 5381;

		for (auto ch : key)
		{
			hash += (hash << 5) + ch;
		}

		return hash;
	}
};

struct JSHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 1315423911;
		for (auto ch : s)
		{
			hash ^= ((hash << 5) + ch + (hash >> 2));
		}
		return hash;
	}
};
//布隆过滤器不仅可以存字符串,也可以存其他类型,只要最后能转换成整型完成取模映射就行,取模是比较常用的哈希函数
//平均存储一个值,开辟X个比特位
template <size_t N, size_t X = 8, class K = string,
class Hashfunc1 = BKDRHash, class Hashfunc2 = APHash, class Hashfunc3 = DJBHash, class Hashfunc4 = JSHash>
class BloomFilter
{
public:
	void set(const K& key)
	{
		size_t hash1 = Hashfunc1()(key) % (X * N);
		size_t hash2 = Hashfunc2()(key) % (X * N);
		size_t hash3 = Hashfunc3()(key) % (X * N);
		size_t hash4 = Hashfunc4()(key) % (X * N);

		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
		_bs.set(hash4);
	}
	bool test(const K& key)
	{
		size_t hash1 = Hashfunc1()(key) % (X * N);
		if (!_bs.test(hash1))
		{
			return false;
		}
		size_t hash2 = Hashfunc2()(key) % (X * N);
		if (!_bs.test(hash2))
		{
			return false;
		}
		size_t hash3 = Hashfunc3()(key) % (X * N);
		if (!_bs.test(hash3))
		{
			return false;
		}
		size_t hash4 = Hashfunc4()(key) % (X * N);
		if (!_bs.test(hash4))
		{
			return false;
		}

		//上面判断不在的情况一定是准确的。
		return true;//这里可能会存在误判,多个哈希位置都和别的字符串冲突了
	}

private:
	std::bitset<N * X> _bs;//如果size_t类型×X过后,size_t类型存不下,也可以选择换long long类型
};

三、海量数据处理

1. 位图应用

经典面试题及解决方案:
1. 给定一个文件,包含40亿个不重复的无符号整数,给一个无符号整数,如何快速判断这个数是否在文件中?
解决方案:使用一个40亿位的位图,将文件中的每个整数映射到位图中,然后根据给定的整数在位图中查找即可。
2. 给定两个文件,分别包含100亿个整数,只有1G内存,如何找到两个文件的交集?
解决方案:使用哈希切分的方法,将两个文件分别按照哈希函数分成1000个小文件,然后对每一对小文件求交集即可。
3. 给定一个文件,包含100亿个整数,只有1G内存,设计算法找到出现次数不超过2次的所有整数?
解决方案:使用两个位图,分别记录每个整数出现的次数,如果出现0次,则两个位图都为0;如果出现1次,则第一个位图为1,第二个位图为0;如果出现2次或以上,则第一个位图为0,第二个位图为1。最后遍历两个位图,找出第一个位图为1且第二个位图为0的位置即可。

2. 哈希切割

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

因为问题是100G大小的文件,肯定是无法加载到内存解决的,传统的KV模型如map来是不能解决的,我们这里采用哈希切割的思想来解决此问题。

哈希切割是一种将一个大文件利用哈希的原理,将其分割为若干个小文件的方法,相同数据分到同一个文件

一种解决方案是:
上来先遍历子文件内容,将每个内容构造成键值对插入到map里面,如果map存不下,则在插入的过程中会出现内存不够的情况,insert会报错,那其实就是new结点失败,new失败是会抛异常的,我们只要捕获这个异常即可,此时说明这个子文件中大多是不同的IP,那么只需要递归哈希切分这个子文件即可。
如果map能够存的下,则正常统计出 出现次数最多的IP即可,无须进行其他任何操作。

在这里插入图片描述

3. 布隆过滤器

  1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
  • 精确算法:可以使用哈希切割的方法,将两个文件按照query的哈希值分割成若干个小文件,使得每个小文件的大小不超过内存限制。然后对每一对小文件,用散列表或者排序的方法找出其中的交集。最后将所有小文件的交集合并起来,就得到了两个大文件的交集。
    -在这里插入图片描述

  • 近似算法:可以使用布隆过滤器的方法,先将一个文件中的所有query插入到一个布隆过滤器中,然后遍历另一个文件中的query,用布隆过滤器检查是否可能存在于第一个文件中。如果可能存在,则加入到候选集合中。最后再对候选集合进行一次精确匹配,就得到了两个大文件的近似交集。

  1. 如何扩展BloomFilter使得它支持删除元素的操作?
  • 一种方法是使用计数型布隆过滤器,即在每个位数组位置上不再存储一个比特位,而是存储一个计数器。当插入一个元素时,将其映射到的位数组位置上的计数器加一;当删除一个元素时,将其映射到的位数组位置上的计数器减一。这样就可以实现删除操作,但是会增加空间开销和计算复杂度 。
  • 另一种方法是使用双重布隆过滤器,即维护两个布隆过滤器,一个用于存储插入的元素(A),一个用于存储删除的元素(B)。当插入一个元素时,将其加入到A中;当删除一个元素时,将其加入到B中。当查询一个元素时,先检查它是否在A中,如果不在,则认为不存在;如果在,则再检查它是否在B中,如果在,则认为已经删除;如果不在,则认为存在。这样也可以实现删除操作,但是会增加误判率和维护成本 。

结束
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/568353.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java

FileOutputStream写数据的3种方式 void write(int b) //一次写一个字节的数据 void write(byte[] b) //一次写一个字节数组数据 void write(byte[] b, int off,int len) //一次写一个字节数组的部分数据 参数一:数组;参数二:起始索引 0;参数三:个数换行: windows:“\r\n” lin…

springboot+java小区社区宽带安装管理系统

本次程序软件的开发的目的就是让使用者可以通过使用该软件提高信息数据的管理效率&#xff0c;同时该程序软件也需要针对不同的操作用户设置对应的功能&#xff0c;因此&#xff0c;此程序的操作流程应该尽量与用户日常操作软件的行为习惯相贴合&#xff0c;另外&#xff0c;程…

分享最强国内免费ChatGPT的镜像网站,记得收藏(2023年更新中)

众所周知的原因&#xff0c;要想在国内使用ChatGPT&#xff0c;肯定是要“折腾一番”的。但是对于绝大多数普通小白&#xff0c;有没有比较容易的方法就用上官方的ChatGPT呢&#xff1f;答案是肯定的&#xff0c;下面就给大家分享几个2023年我正在使用的ChatGPT镜像网址&#x…

Python入门学习

一、执行Python&#xff08;Hello World&#xff09;程序 对于大多数程序语言&#xff0c;第一个入门编程代码便是 “Hello World&#xff01;”&#xff0c;以下代码为使用 Python 输出 “Hello World&#xff01;” 1.1 创建hello.py文件 1.2 编写程序 #!/usr/bin/python…

听我一句劝,别去外包,干了五年,废了....

先说一下自己的情况&#xff0c;大专生&#xff0c;18年通过校招进入杭州某软件公司&#xff0c;干了接近5年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落! 而我已经在一个企业干了5年的功能测试…

Gateway网关参数进行验签POST 包含requestbody 请求体封装

Gateway网关自定义拦截器的不可重复读取数据 特别注意一点, 因为在网关层 拿出 request 流之后,必须重写getbody()方法把所有的参数放进去,否则后面转发的请求无法接收到任何数据, 坑,巨坑,因为版本问题网上很多都不能兼容, 我的springboot环境 依赖包 <parent><gr…

Linux--ServerProgramming--TCP\IP协议族

1.TCP/IP 协议族 1.1 TCP/IP协议族及主要协议 TCP/IP 协议族是一个四层协议系统。自上而下为&#xff08;如下图所示&#xff09;&#xff1a;应用层传输层网络层数据链路层 应用层负责处理应用程序逻辑&#xff0c;在用户空间实现。&#xff08;少数服务器程序在内核中实现。…

如何下载外文期刊文献,怎么下载又快又省力!

文章开头我们先了解一下下面这些查找外文期刊文献的数据库: 1、Web of Science&#xff1a;是获取全球学术信息的重要数据库。它收录了全球13000多种权威的、高影响力的学术期刊&#xff0c;内容涵盖自然科学、工程技术、生物医学、社会科学、艺术与人文等领域。其中以SCIE、S…

OpenCV入门简单的人脸识别项目

在学会图像处理和打开摄像头获取视频流后&#xff0c;就可以开展简单的人脸识别项目。 文章目录 检测人脸区域并绘制矩形多个人脸进行识别绘制五官位置视频检测人脸人脸识别 人脸识别首先需要检测到人脸。 检测人脸区域并绘制矩形 # 加载图片img face_recognition.load_image…

接口全生命周期的生产利器 ApiKit

一、ApiKit 整体介绍&#xff1a; 1、接口管理的需求与现状&#xff1a; 在软件项目研发的过程中&#xff0c;必然存在以下几个需求&#xff1a; API 接口文档的管理&#xff0c;常用的解决方案有 Swagger API 接口的调试&#xff0c;常用的解决方案有 Postman API 接口的自…

Java——网络编程套接字

目录 一、网络编程基础 1.1 为什么需要网络编程&#xff1f;——丰富的网络资源 二、什么是网络编程? 三、网络编程中的基本概念 3.2 请求和响应 3.3 客户端和服务端 常见的客户端服务端模型 四、Socket套接字 五、通信模型 5.1 Java数据报套接字通信模型 5.2 Java流…

【大数据之Hive】二、Hive安装

Hive安装部署&#xff08;最小化部署&#xff09; 安装部署Hive&#xff08;最小化只用于本机测试环境中&#xff0c;不可用于生产环境&#xff09;&#xff0c;并运行。 步骤&#xff1a; &#xff08;1&#xff09;把apache-hive-3.1.3-bin.tar.gz解压到/opt/module/目录下&…

IIC总线学习

IIC总线 1.总线空闲状态。2.IIC总线的起始停止条件。3.IIC总线的数据传送4.IIC总线的应答5.IIC时序 1.总线空闲状态。 总线空闲时&#xff0c;SDA和SCL均为高电平。 2.IIC总线的起始停止条件。 起始条件&#xff1a;在SCL为高时&#xff0c;SDA总线被拉低&#xff0c;即出现…

json-server的基本使用

1、mock是什么&#xff1f; mockjs 作用&#xff1a;生成随机数据&#xff0c;拦截 Ajax 请求 目的&#xff1a;很多时候前端开发页面的过程中&#xff0c;后端的接口并没有写好&#xff0c;这个时候需要前端自己定义接口及接口的返回数据的结构体&#xff0c;这个时候就需要…

【活动】云计算的优势与发展趋势

写在前面 人生是一场消耗&#xff0c;要把美好的时光放在喜欢的人与事上。 一、前言 云计算作为一种新兴的信息技术应用解决方案&#xff0c;可以帮助企业解决IT资源利用率低、IT基础设施的建设和维护成本高、IT系统的安全和稳定性问题等痛点&#xff0c;提高企业的业务灵活性…

react表格行下载文件方法总结

一、前言 下载文件时&#xff0c;后台接口返回的响应体是文件流格式的&#xff0c;前端接收时如果不进行处理&#xff0c;就会无法正确下载文件&#xff08;有可能会直接打开文件等&#xff09;。 在此记录下react的表格行使用file-saver下载文件的方法。&#xff08;注意不同…

文件包含的本质、预处理符号、# vs ##

何为头文件&#xff1f; 在C语言中&#xff0c;文件包含是一种常见的编程技术&#xff0c;它允许程序员在一个源文件中使用另一个源文件中的函数或变量。 文件包含通常使用#include预处理指令来实现。#include指令告诉预处理器将文件的内容插入到当前文件的指定位置中。 例如&a…

今天面了个阿里拿 38K 出来的,让我见识到了测试界的天花板

一直觉得自己的技术已经很不错了&#xff0c;直到最近遇到了一个阿里来的大佬 5年测试&#xff0c;应该是能达到资深测试的水准&#xff0c;即不仅能熟练地开发业务&#xff0c;而且还能熟悉项目开发&#xff0c;测试&#xff0c;调试和发布的流程&#xff0c;而且还应该能全面…

第04章 IDEA的安装与使用

【Why IDEA ?】 【注】JetBrains官方说明&#xff1a; 尽管我们采取了多种措施确保受访者的代表性&#xff0c;但结果可能会略微偏向 JetBrains 产品的用户&#xff0c;因为这些用户更有可能参加调查。 此外&#xff0c;2022年&#xff0c;某美国软件开发商在对近千名专业的J…

chatgpt赋能python:Python中的Split函数:去空操作详解

Python中的Split函数&#xff1a;去空操作详解 在Python编程中&#xff0c;我们经常需要对字符串进行操作。而字符串的分割操作在其中是非常常见的操作。Python中的split函数便是用来实现字符串分割的函数。不过&#xff0c;在使用split函数时通常还需要经过去除空格等操作。 …