C++ | 位图与布隆过滤器

目录

前言

一、位图

1、位图的引入

2、位图的实现

(1)基本结构

(2)构造函数

(3)插入数据

(4)删除数据

(5)是否存在

3、位图的优缺点

4、位图的应用

二、布隆过滤器

1、布隆过滤器的引入

2、布隆过滤器的实现

(1)布隆过滤器的框架

(2)布隆过滤器的插入

(3)布隆过滤器的查找

(4)关于布隆过滤器的删除

3、布隆过滤器的优缺点

三、哈希切割


前言

        本文主要讲解位图及位图应用,布隆过滤器及其引用,以及我们的对于海量数据的处理,如何正确理解位图与布隆过滤器的使用场景;

一、位图

1、位图的引入

        首先,我们来看看一道来自腾讯的面试题;

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

你如果遇到这题,你会想着如何处理这题呢?

思路1:将数据放入set/unordered_set中;

        首先我们来分析一下,40亿个整型,每个整型4字节,因此总共160亿字节;160亿字节大概多少内存呢?我们都知道1KB = 1024Byte,1MB = 1024KB,1GB = 1024MB,因此我们可以推出 1GB = 1024*1024*1024Byte,大约10亿字节左右,因此160亿字节约等于16GB内存;这还仅仅只是数据占16GB内存,我们的容器里存放相应的指针也需要占用内存,明显是不合理的;故这种方案不合理

思路2:前面我们存放一个数据占用了4个字节,我们可不可以少用一些内存来存储这个数据呢?我们最低使用多大内存可以储存下这一个数据呢?想一想,我们是否可以使用一个比特位存储下这个数据,此时我们仅仅只需要0.5GB左右就可以存下这40亿的数据(以前需要一个整型的大小32比特位,现在只需要1比特位,16GB / 32 = 0.5GB),我们使用哈希映射的思想,我们给每一个数据映射到一个比特位位置,1表示这个数据存在,0表示这个数据不存在,如下图所示;我们存入一个7,我们将7对应的比特位位置置为1即可;这就是我们接下来要实现的位图的结构;

2、位图的实现

(1)基本结构

首先我们写出基本框架,具体如下图所示;

	template<size_t N>
	class bitset
	{
	public:

	private:
		std::vector<char> _bits;
	};
(2)构造函数

位图的构造函数主要是完成vector容器的初始化,我们想一想,传入N个数据的位图,我们需要开多大的空间合适;如果N为14,我们需要开2个字节空间并不合适,我们需要开三个字节的空间,即N / 8 + 1;

		// 构造
		bitset()
		{
			size_t len = N / 8 + 1;
			_bits.resize(len, 0);
		}
(3)插入数据

我们在位图中插入一个数据就是将对应的比特位置为1,我们可以通过将数据x / 8得到该数据在哪一个字节,将数据 x % 8 可以得到该数据在字节的哪一个比特位,最后通过按位或运算移位运算符将对应得比特位置为1;

		// 插入
		void set(size_t val)
		{
			// 第几个字节中
			size_t x = val / 8;
			// 字节的第几个比特位中
			size_t y = val % 8;
			// 置1
			_bits[x] |= (1 << y);
		}
(4)删除数据

位图数据得删除即将对应得比特位置为0,我们想要将对应得比特位置为零也需要得到对应比特位得位置,与插入相同的方式得到比特位的字节与字节中的位置;然后通过按位与移位运算按位取反运算将对应的比特位置为0;

		// 删除
		void reset(size_t val)
		{
			// 第几个字节中
			size_t x = val / 8;
			// 字节的第几个比特位中
			size_t y = val % 8;
			// 置0
			_bits[x] &= ~(1 << y);
		}
(5)是否存在

我们除了要实现插入与删除,我们还要提供一个测试该数据是否存在的接口;我们同样通过按位与运算查看该比特位的数据是否存在;

		// 在不在
		bool test(size_t val)
		{
			// 第几个字节中
			size_t x = val / 8;
			// 字节的第几个比特位中
			size_t y = val % 8;
            // 0是不存在,非0代表存在
			return _bits[x] & (1 << y);
		}

3、位图的优缺点

位图优缺点如下:

优点:

1、查找速度快,O(1)的时间复杂度内就能查找一个数据是否存在;

2、节省内存空间,40亿数据才占用0.5G内存

缺点:

1、只适用于整型的存储

4、位图的应用

位图主要应用于以下场景:

1、快速查找某个数据是否在一个集合中

2、排序 + 去重

3、求两个集合的交集、并集等

4、操作系统中磁盘块标记

接下来我以几道面试题来举例讲解位图的应用;

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

思路:题目说了100亿数据,实际上会有大量重复,因为无符号整型的最大值也只有42亿9千万左右;我们可以设计一个类,该类有连个位图,每个位置可以分别用 00 表示 该数据一次都没出现,用 01 表示该数据只出现了一次,用 10 表示该数据出现了两次及以上;最后遍历整个位图,输出 01 比特位映射的数据不就是只出现一次的数据吗?

	template<size_t N>
	class twobits1
	{
	public:
		void set(size_t val)
		{
			// 00 -> 01
			if (_bits1.test(val) == false && _bits2.test(val) == false)
			{
				_bits2.set(val);
			}// 01 -> 10
			else if (_bits1.test(val) == false && _bits2.test(val) == true)
			{
				_bits1.set(val);
				_bits2.reset(val);
			}
            // 两次及以上不必处理
		}

		void print()
		{
			for (size_t i = 0; i < N; i++)
			{
				if (_bits1.test(i) == false && _bits2.test(i) == true)
					std::cout << i << std::endl;
			}
		}
	private:
		bitset<N> _bits1;
		bitset<N> _bits2;
	};

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

思路1:只需创建一个位图,遍历文件1,存入位图中,然后遍历文件2,每个数据与位图中对比看是否存在,存在即为交集;这里有一个问题,假如文件1的数据为 1  2  4,文件2的数据为 1  2  1   1   1,此时遍历文件2输出的结果会有大量重复的1,因此我们还需要去重,我们可以在遍历文件2的同时,第一次找到位图中的交集时,我们将这个位图中的比特位置为0,这时,在后续的查找遍历中,不会再重复输出这个数据了,便可以达到去重的效果;

思路2:创建两个位图,分别遍历这两个文件,将文件1存入位图1中,文件2存入位图2中,然后从0一直遍历到无符号整型最大值,如果对应比特位都为1时,此时便是这两个文件数据的交集;

3、1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

思路:同题目1,我们设计一个类,有两个位图,分别定义状态 00 表示该位置映射数据为0个,01表示该位置映射数据为1个,10表示该位置映射数据为两个,11表示该位置映射数据为2个及以上,遍历完文件存入位图后,我们再从0遍历到无符号整型最大值,然后输出状态为 01 或 10的位置映射的值;稍稍该该题目1的代码便可实现,具体代码如下;

	template<size_t N>
	class twobits2
	{
	public:
		void set(size_t val)
		{
			// 00 -> 01
			if (_bits1.test(val) == false && _bits2.test(val) == false)
			{
				_bits2.set(val);
			} // 01 -> 10
			else if (_bits1.test(val) == false && _bits2.test(val) == true)
			{
				_bits1.set(val);
				_bits2.reset(val);
			} // 10 -> 11
			else if(_bits1.test(val) == true && _bits2.test(val) == false)
			{
				_bits1.set(val);
				_bits2.set(val);
			}
		}

		void print()
		{
			for (size_t i = 0; i < N; i++)
			{
				if ((_bits1.test(i) == false && _bits2.test(i) == true)
					|| (_bits1.test(i) == true && _bits2.test(i) == false))
					std::cout << i << std::endl;
			}
		}
	private:
		bitset<N> _bits1;
		bitset<N> _bits2;
	};

二、布隆过滤器

1、布隆过滤器的引入

        首先,我们还是来看一道经典的面试题;

1、给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出
精确算法和近似算法

思路:此时,我们发现用我们的位图无法解决这个问题了,因为位图只针对整型家族的数据才有效,此时我们引入一个新的数据结构 ----- 布隆过滤器,其思想是将字符串通过hash函数转化成我们的整型;然后将一个文件中的query存进我们的位图中,另一个文件在布隆过滤器中查询,这就是布隆过滤器的主要思想,哈希函数+位图;也是我们这道题的近似算法;

字符串转整型哈希函数

2、布隆过滤器的实现

虽然我们使用一些哈希函数将字符串映射成我们的整型,这些哈希函数虽然可以减少冲突,但不能完全避免,为了进一步减少冲突,我们通过多个哈希函数将一个字符串映射多个位置来达到减少冲突的效果;如下面实例;

上图是使用两个哈希函数,将我们的一个字符串映射到两个位置,当然,我们在查找时,映射的两个位置必须都存在,才能表示这个字符串可能存在;

(1)布隆过滤器的框架

        关于布隆过滤器还有一个问题,就是应该有多少个哈希函数映射,这其实是数学问题了,有兴趣升入了解的可以阅读下方链接文章;

布隆过滤器哈希函数深入研究

        如上公式中,k是哈希函数个数,m是布隆过滤器长度,n是数据个数;我们通过如上公式推导出 m = (k / ln 2)* n;我们写出如下代码结构;

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

	struct APHash
	{
		size_t operator()(const std::string& s)
		{
			size_t hash = 0;
			for (long i = 0; i < s.size(); i++)
			{
				size_t ch = s[i];
				if ((i & 1) == 0)
				{
					hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
				}
				else
				{
					hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
				}
			}
			return hash;
		}
	};

	struct DJBHash
	{
		size_t operator()(const std::string& s)
		{
			if (s.size() == 0)
				return 0;
			register size_t hash = 5381;
			for(auto ch : s)
			{
				hash += (hash << 5) + ch;
			}
			return hash;
		}
	};

	template<size_t N, 
		class K = std::string, 
		class HashFunc1 = BKDRHash,
		class HashFunc2 = APHash,
		class HashFunc3 = DJBHash>
	class BloomFilter
	{
	public:

	private:
		static const int _a = 4;  // k / ln 2
		bitset<N * _a> _bits;
	};

        本文挑选了三个哈希函数,读者可不必一定选这三个哈希函数,可从之前字符串转整型的哈希函数中任选;本文还算出了k / ln 2 的值并命名成a;

(2)布隆过滤器的插入

        我们分别算出我们的hashi,然后对相应位置取余数,然后再插入;

		void set(const K& key)
		{
			int len = _a * N;
			size_t hashi1 = HashFunc1()(key) % len;
			size_t hashi2 = HashFunc2()(key) % len;
			size_t hashi3 = HashFunc3()(key) % len;

			_bits.set(hashi1);
			_bits.set(hashi2);
			_bits.set(hashi3);
		}
(3)布隆过滤器的查找

        布隆过滤器的查找主要查找key对应的hashi是否全都映射到了位图中;具体代码如下所示;

		bool test(const K& key)
		{
			int len = _a * N;
			size_t hashi1 = HashFunc1()(key) % len;
			size_t hashi2 = HashFunc2()(key) % len;
			size_t hashi3 = HashFunc3()(key) % len;

			if (_bits.test(hashi1) && _bits.test(hashi2) && _bits.test(hashi3))
			{
				return true;
			}
			else
			{
				return false;
			}
		}

        此外,我们还需要注意的一点是布隆过滤器的查找在的情况是不准确的,不在的情况是完全准确的;为什么呢?下图有做解释;

        如若生物并不存在,但是映射的位置是别的字符串映射的位置(因为哈希函数不能完全保证不冲突),此时查找结果却显示生物存在布隆过滤器中;因此在有可能是不准确的;而不在一定是准确的,但凡之前插入过,其映射位置一定会被设置成1,若指定位置没设置成1,则一定不存在; 

(4)关于布隆过滤器的删除

        布隆过滤器一般不会提供删除接口,如下图所示;

        若布隆过滤器提供删除接口,如若删除语文,将语文对应的比特位设置成0,此时我们查找英语和数学时,我们会发现也查不到的情况;

        若是必须提供删除接口,你有什么办法呢?

思路:我们每个映射的位置用多个比特位来储存,而不是只用一个,假设用四个比特位代表一个位置,然后我们用引用计数的思想,每插入一个哈希映射,我们对其++。比如四个比特位可以表示15,即为 1111 ;而删除一个哈希映射位置时,我们必须对其-1操作;这样就可以实现布隆过滤器的删除接口,且删除一个数据不会对别的数据产生影响;但是这种方法是有弊端的,其一是占用空间增多了,其二是引用计数的数据范围有限;

3、布隆过滤器的优缺点

布隆过滤器的优缺点如下:

优点:

1、增加与查找的时间复杂度为O(K)(K为哈希函数个数,一般很少);

2、布隆过滤器不需要存储元素本身,对于某些场合有数据安全性;

3、可以存储数据规模较大的数据群体;

4、允许小规模误判的情况下,布隆过滤器其存储数据有很大的空间优势

缺点:

1、有误判率(在有可能不准确)。

2、不能获取元素本身

3、一般情况下,不能删除布隆过滤器的元素

4、即使提供引用计数来实现删除,也可能会产生计数回绕的问题(unsigned int性质)

        下面我们还提供了一个测试布隆过滤器的准确性的测试接口;我们可以通过调节_a来控制误判率;

void test_bloomfilter()
{
	srand((unsigned int)time(0));
	const int N = 1000000;
	MySpace::BloomFilter<N> bf;
	string str1 = "https://mp.csdn.net/mp_blog/creation/editor/132078512?spm=1001.2014.3001.5352";
	// 源字符串
	vector<string> v1;
	for (int i = 0; i < N; i++)
	{
		v1.push_back(str1 + to_string(i));
	}
	for (auto& e : v1)
	{
		bf.set(e);
	}

	// 近似字符串
	vector<string> v2;
	for (int i = 0; i < N; i++)
	{
		v2.push_back(str1 + to_string(999999 + rand()));
	}
	size_t count1 = 0;
	for (auto& e : v2)
	{
		if (bf.test(e))
		{
			count1++;
		}
	}
	// 不近似字符串
	vector<string> v3;
	string str2 = "www.baidu.com";
	for (int i = 0; i < N; i++)
	{
		v3.push_back(str2 + to_string(rand() + i));
	}
	size_t count2 = 0;
	for (auto& e : v3)
	{
		if (bf.test(e))
		{
			count2++;
		}
	}
	cout << count1 << endl;
	cout << count2 << endl;
	cout << "近似字符串的重复率:" << (double)count1 / (double)N << endl;
	cout << "不近似字符串的重复率:" << (double)count2 / (double)N << endl;

}

三、哈希切割

        哈希切割是本章的一个补充话题,关于哈希切割首先看如下面试题;

1、给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

思路:首先,我们看题目是让我们找到出现次数最多的IP地址,我们首先想到这就是一个经典的TopK问题;我们可以通过堆来找,但是堆有无法存放这么多的数据,如何处理呢?我们可以将数据通过哈希函数划分成500份文件,然后将每个文件中通过map/unordered_map得到出现次数最多的IP并记录下来,可是,我们是通过哈希函数切分,并不能保证每个文件的大小一致,仍可能出现有的文件过大;

对于过大的小文件,有如下两种情况:

1、小文件有很多重复IP;

2、小文件没有很多重复IP,仅仅是哈希切割恰好切到了一起;

我们如何区分这两种情况呢?我们可以将这个小文件插入map/unordered_map中,若是因为小文件有很多重复IP,我们可以统计出IP文件个数,因为有很多会插入失败;若是情况2,我们插入过程中,肯定会抛出bad_alloc的异常;这时我们对这个小文件换哈希函数继续切分即可;最终每个小文件肯定能统计出一个出现次数最多的IP,我们将这些IP以pair<IP, 次数>的形式放进大堆中(按次数比较),最后便可得到出现最多的IP地址;

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

pytorch入门

详细安装教程和环境配置可以看&#xff1a;Python深度学习&#xff1a;安装Anaconda、PyTorch&#xff08;GPU版&#xff09;库与PyCharm_哔哩哔哩_bilibili 跟学课程&#xff1a;B站我是土堆 pytorch中两个实用函数&#xff1a; dir()&#xff1a;打开 help():说明书…

Java POI 百万规模数据的导入和导出

目录 1、百万数据导入1.1 需求分析1.2 思路分析1.3 代码实现1.3.1 步骤分析1.3.2 自定义处理器1.3.3 自定义解析1.3.4 测试 2、百万数据导出2.1、概述2.2、解决方案分析2.3、原理分析2.4、百万数据的导出2.4.1、模拟数据2.4.2、思路分析2.4.3、代码实现2.4.4、测试结果 1、百万…

python-网络爬虫.Request

Request python中requests库使用方法详解&#xff1a; 一简介&#xff1a; Requests 是Python语言编写&#xff0c;基于urllib&#xff0c; 采用Apache2 Licensed开源协议的 HTTP 库。 与urllib相比&#xff0c;Requests更加方便&#xff0c;处理URL资源特别流畅。 可以节约我…

如何消除浮动

第一种方法: 1、创建一个general.css文件&#xff1a; charset "utf-8"; .clearfix:after {content: "";display: block;clear: both;} /* flex */ .flex,.flexA,.flexB,.flexC {display: flex;flex-wrap: wrap;} .flexA {justify-content: space-aroun…

iPhone 6透明屏是什么?原理、特点、优势

iPhone 6透明屏是一种特殊的屏幕技术&#xff0c;它能够使手机屏幕变得透明&#xff0c;让用户能够透过屏幕看到手机背后的物体。 这种技术在科幻电影中经常出现&#xff0c;给人一种未来科技的感觉。下面将介绍iPhone 6透明屏的原理、特点以及可能的应用。 iPhone 6透明屏的原…

if语句实现成绩等级判断

if语句实现成绩等级判断 案例分析代码实现小结Time 案例分析 使用键盘输入一个成绩&#xff0c;然后通过if判断语句实现成绩等级的判断 代码实现 import java.util.Scanner;public class DetermineDemo {public static void main(String[] args) {Scanner scanner new Scanne…

服务器硬件、部署LNMP动态网站、部署wordpress、配置web与数据库服务分离、配置额外的web服务器

day01 day01项目实战目标单机安装基于LNMP结构的WordPress网站基本环境准备配置nginx配置数据库服务部署wordpressweb与数据库服务分离准备数据库服务器迁移数据库配置额外的web服务器 项目实战目标 主机名IP地址client01192.168.88.10/24web1192.168.88.11/24web2192.168.88…

ElasticSearch可视化管理工具之ElasticHD

推荐的五种客户端 1.Elasticsearch-Head &#xff0c; Elasticsearch-Head 插件在5.x版本之后已不再维护&#xff0c;界面比较老旧。 2.cerebro 据传该插件不支持ES中5.x以上版本。 3.kinaba 功能强大&#xff0c;但操作复杂&#xff0c;以后可以考虑。 4.Dejavu 也是一个 Elas…

vue 新学习 04 css样式绑定,渲染,key的重要意义

之前的html文件如何去绑定css样式&#xff1f; 01.首先在html文件中&#xff0c;在<head>标签中&#xff0c;用<style>中去写样式&#xff0c;通过html标签(每一个标签都有这样子的属性)中的class或者是id属性来完成<style>中的描绘的样式的用。 例子&#x…

语义分割文献整理

2014年文献 1.论文题目《Semantic Image Segmentation with Deep Convolutional Nets and Fully Connected CRFs》 1.1.网络别名《DeepLabV1》 1.2.论文引用 Chen L C, Papandreou G, Kokkinos I, et al. Semantic image segmentation with deep convolutional nets and ful…

通过 CCIP 构建跨链应用(5 个案例)

Chainlink 的跨链互操作性协议&#xff08;CCIP&#xff09;是一种新的通用跨链通信协议&#xff0c;为智能合约开发人员提供了以最小化信任的方式在区块链网络之间传输数据和通证的能力。 目前&#xff0c;部署在多个区块链上的应用程序面临着资产、流动性和用户的碎片化问题…

【电源专题】电压查表法显示电量的原理与缺点

在文章:【电源专题】电量计估计电池荷电状态方法(开路电压法及库仑计法)的差别中我们讲到电量计估计荷电状态的方法。其中开路电压法实现方法较容易,可借着开路电压对应荷电状态查表而得到。 那么为什么能够使用电压查表法去预估电池容量呢?如下所示如果我们往一个有刻度…

LLM大模型——langchain相关知识总结

目录 一、简介LangChain的主要价值支柱简单安装 二、 LangChain的主要模块1.Model I/Oprompt模版定义调用语言模型 2. 数据连接3. chains4. Agents5. MemoryCallbacks 三、其他记录多进程调用 主要参考以下开源文档 文档地址&#xff1a;https://python.langchain.com/en/lates…

小白到运维工程师自学之路 第六十二集 (docker持久化与数据卷容器)

一、概述 Docker持久化是指将容器中的数据持久保存在主机上&#xff0c;以便在容器重新启动或迁移时不丢失数据。由于Docker容器是临时和可变的&#xff0c;它们的文件系统默认是易失的&#xff0c;这意味着容器中的任何更改或创建的文件都只存在于此容器的生命周期内。但是&a…

LVDS端口ESD静电放电保护电路图(经典)

Low Voltage Differential Signaling&#xff08;LVDS&#xff09;是一种低压差分信号技术接口&#xff0c;是美国NS公司为克服以TTL电平方式传输宽带高码率数据时功耗大、EMI电磁干扰大等缺点而研制的一种数字视频信号传输方式。LVDS端口电路包括两部分&#xff1a;驱动板侧的…

3DEXPERIENCE用户角色 | Structural Mechanics Engineer 结构力学工程师

真实条件下实施复杂的线性和非线性分析 直观验证设计并更快地做出产品决策 Structural Mechanics Engineer 在基于云的 3DEXPERIENCE 平台上构建&#xff0c;您可对产品行为执行结构线性和非线性静态、低速和高速动态和热仿真。具备材料校准功能&#xff0c;有助于确保材料行为…

十分钟python入门 日期时间

1.Python 日期 Python 中的日期不是其自身的数据类型&#xff0c;但是我们可以导入名为 datetime 的模块&#xff0c;把日期视作日期对象进行处理。 1.1 导入 datetime 模块并显示当前日期&#xff1a; import datetime#导入 datetime 模块并显示当前日期&#xff1a; x da…

微信小程序接入腾讯云天御验证码

腾讯云新一代行为验证码&#xff08;Captcha&#xff09;&#xff0c;基于十道安全防护策略&#xff0c;为网页、APP、小程序开发者打造立体、全面的人机验证。在保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时&#xff0c;提供更精细化的用户体验。 …

在使用Python爬虫时遇到503 Service Unavailable错误解决办法汇总

在进行Python爬虫的过程中&#xff0c;有时会遇到503 Service Unavailable错误&#xff0c;这意味着所请求的服务不可用&#xff0c;无法获取所需的数据。为了解决这个常见的问题&#xff0c;本文将提供一些解决办法&#xff0c;希望能提供实战价值&#xff0c;让爬虫任务顺利完…

【问题随记】

ubuntu 14.04源更新(sources.list) deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ trusty-update…
最新文章