C++--18.哈希应用

本文探讨了哈希在大数据场景中的应用,包括位图技术在判断元素存在性、数据去重及布隆过滤器的工作原理、插入与查找操作,以及如何优化布隆过滤器以支持删除。还介绍了如何用位图和布隆过滤器解决大规模文件交集问题,以及一致性哈希在分布式存储中的应用.

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在我们上一章对哈希有一个大致的了解之后,我们在这一章来学习一下它的应用

哈希的应用

位图

在我们了解位图之前,我们先来看一道经典的题目

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

我们再看到这道题首先会有几个思路

1.直接遍历,判断,O(N)

2.排序后二分查找,O(NlogN)

3.开辟一个42亿*4(因为整数的范围就在42亿)大小的哈希表/红黑树,然后依次映射进去,判断重复

事实上这些方案在内存足够的情况下是可行的,但是在这里,数据量太大,这些方式其实是无法满足的,此时我们就需要引入我们的位图

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

位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。

事实上,位图其实就是利用比特位去代替数值,因为比特位在数据中单位最小,所以我们利用位图既节省了空间,又非常高效,很适合解决这种问题

位图的实现

class bitset
	{
	public:
		bitset(size_t N)
		{
			_bits.resize(N / 32+1, 0);//因为这里我们开辟空间只能以int为单位开辟空间,所以我们要对位计算就需要/32,但是因为/会把余数抹去,所以我们在最后+1,多开32位,避免这个问题
			_num = 0;
		}
		void set(size_t x)
		{
			size_t index = x / 32;//算出在第几个整型框
			size_t pos = x % 32;//算出在整型框中的第几个单元

			_bits[index] |= (1 << pos);//将1左移pos位,使得只有pos为1,再与整形框与,这个位置就被填成了1,代表着这一位存在
             ++_num;
			//补充:这里的左移实质上是向高位移动,因为在大小端级中左右是不同的,小端机数据二进制码从右往左存,大端级从左往右,所以这里的左移在小端机为向左移,大端机实质向右,但我们这里都规定为向高位移动
		}
		void reset(size_t x)
		{
			size_t index = x / 32;//算出在第几个整型框
			size_t pos = x % 32;//算出在整型框中的第几个单元
			_bits[index] &= ~(1 << pos);//先将pos位通过取反操作置为0,其他位置为1再与整体即可
            --_num;
		}
		bool test(size_t x)
		{
			size_t index = x / 32;
			size_t pos = x % 32;
			return _bits[index] & (1 << pos);
		}
	private:
		std::vector<int> _bits;
		size_t _num;
	};

补充:若想开辟整个无符号整型的大小:bitset(-1);因为我们的-1是以补码的形式存的,在这里是无符号整数类型,所以我们这里大小就会自动变为全1序列,也就是512M,整个无符号整型数的大小

位图的应用

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

所谓位图,就是把数据映射到二进制的bit位中,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。位图的优点就是快速并且节省空间,但是缺点是只能映射整形

布隆过滤器

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:不能处理哈希冲突
3. 将哈希与位图结合,即布隆过滤器

布隆过滤器概念

布隆过滤器是 由布隆( Burton Howard Bloom )在 1970 年提出的 一种紧凑型的、比较巧妙的 概率型数据结 ,特点是 高效地插入和查询,可以用来告诉你 某样东西一定不存在或者可能存在 ,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间

 布隆过滤器的插入

 

布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为 1 。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零, 代表该元素一定不在哈希表中,否则可能在哈希表中
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因 为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找 "alibaba" 时,假设 3 个哈希函数计算的哈希值为: 1 3 7 ,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中 "tencent" 元素,如果直接将该元素所对应的二进制比特位置 0 “baidu” 元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给 k 个计数器 (k 个哈希函数计算出的哈希地址) 加一,删除元素时,给 k 个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
1. 无法确认元素是否真正在布隆过滤器中
2. 存在计数回绕

布隆过滤器整体实现

#pragma once
#include "bitset.h"
#include <string>

namespace wxy
{
	struct HashStr1//不同的哈希映射仿函数
	{
		// BKDR
		size_t operator()(const std::string& str)
		{
			size_t hash = 0;
			for (size_t i = 0; i < str.size(); ++i)
			{
				hash *= 131;//对每个字符的ASSCI码*131
				hash += str[i];//加到哈希表中
			}

			return hash;//返回得到映射的哈希表
		}
	};

	struct HashStr2
	{
		// RSHash
		size_t operator()(const std::string& str)
		{
			size_t hash = 0;
			size_t magic = 63689; // 魔数,这些数据都是经过计算得到冲突相比较而言概率更低的
			for (size_t i = 0; i < str.size(); ++i)
			{
				hash *= magic;
				hash += str[i];
				magic *= 378551;
			}

			return hash;
		}
	};

	struct HashStr3
	{
		// SDBMHash
		size_t operator()(const std::string& str)
		{
			size_t hash = 0;
			for (size_t i = 0; i < str.size(); ++i)
			{
				hash *= 65599;
				hash += str[i];
			}

			return hash;
		}
	};

	template <class K = string, //数据类型常见为string,所以我们给与缺省值,同时将仿函数也给缺省
	class Hash1 = HashStr1, 
	class Hash2 = HashStr2, 
	class Hash3 = HashStr3>
	class bloomfilter//布隆过滤器
	{
	public:
		bloomfilter(size_t num)//初始化大小,布隆过滤器中元素个数
			:_bs(5*num)//经过前人的计算,发现给与5倍大小最合适
			, _N(5*num)//开辟5倍大小于元素个数的大小
		{}

		void set(const K& key)//插入函数,因为我们不确定数据类型,所以我们传入key
		{
			size_t index1 = Hash1()(key) % _N;//第一个映射地址,%的作用是将其大小限定在哈希表所包含的范围内
			size_t index2 = Hash2()(key) % _N;//第二个映射地址
			size_t index3 = Hash3()(key) % _N;//第三个映射地址

			cout << index1 << endl;
			cout << index2 << endl;
			cout << index3 << endl << endl;
			
			_bs.set(index1);//将地址插入位图,标记3个位置
			_bs.set(index2);
			_bs.set(index3);
		}

		bool test(const K& key)//查找
		{
			size_t index1 = Hash1()(key) % _N;//计算位置
			if (_bs.test(index1) == false)//当第一个位置不在
				return false;//返回不在

			size_t index2 = Hash2()(key) % _N;
			if (_bs.test(index2) == false)
				return false;

			size_t index3 = Hash3()(key) % _N;
			if (_bs.test(index3) == false)
				return false;

			return true; //当三个位置都在时返回true 但是这里也不一定是真的在,还是可能存在误判,不过概率已经降低了很多了
			// 判断在,是不准确的,可能存在误判
			// 判断不在,是准确
		}

		void reset(const K& key)
		{
			// 不支持删除,可能会存在误删。一般布隆过滤器不支持删除,若把目标位置置0,有可能会影响到其他的值,所以不支持删除
		}

	private:
		bitset _bs; // 位图
		size_t _N;//
	};

布隆过滤器优点

1. 增加和查询元素的时间复杂度为 :O(K), (K 为哈希函数的个数,一般比较小 ) ,与数据量大小无关
2. 哈希函数相互之间没有关系,方便硬件并行运算
3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

布隆过滤器缺陷

1. 有误判率,即存在假阳性 (False Position) ,即不能准确判断元素是否在集合中 ( 补救方法:再建立一个白名单,存储可能会误判的数据)
2. 不能获取元素本身
3. 一般情况下不能从布隆过滤器中删除元素
4. 如果采用计数方式删除,可能会存在计数回绕问题

海量数据处理

 这里我们来看几道题

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

当我们拿到这个题时,想到的一定是利用位图来解决,因为数据量太大了,如果放在内存中需要40G,但是我们常规的位图只能标记一个数据是否存在,因为未运算中仅有01两种状态,而不能标记其出现的次数,那么此时应该如何解决呢?

在我们思索过后,发现其实我们可以将位图稍微转变一下,利用两个位去标记一个数据,此时便具有了00,01,10,11四种状态,在我们这道题中只需要取三种状态,不存在的00,仅出现一次的01,出现两次以上的10,然后去计算01的位置即可,不过要注意的是,此时我们应该取x/16,x%16,1<<2*pos个单元,因为此时我们将两个比特位看成一个单元,即可解决这个问题

其实还有一个类似的方式,就是我们创建两个位图,分别记录第一位第二位的数据

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

我们通过了上一道题的经验,依旧对这道题使用位图来进行处理,我们可以给到两个方案

1.将其中一个文件1的整数映射到一个位图中,读取另一个文件2中的整数,判断在不在位图,在就是交集,消耗500M内存

2.将文件1的整数映射到位图1中,文件2的整数映射到位图2中,然后将两个位图的数按位与,与之后为1的位就是交集,消耗内存1G.

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

这道题基本与第一题是一样的,只是多了一个条件而已,个数3或者大于3的可以设置为11,其他的都与第一题一样,最后找除过11的数即可

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

解释:query意为查询,可以理解为一个语句,一个查询字符串

我们可以先思考一下,100亿个query占用多少空间呢?假设平均一个query30-60byte,100亿个query大约占用300-600G的大小,这个数量是极其庞大的

我们针对这道题给出方案

1.将文件1中的query映射到一个布隆过滤器,读取文件中的query,判断在不在布隆过滤器中,在就是交集。

但是我们想一下,对于布隆过滤器而言,单纯通过位去判断并不是准确的,因为一个位可能对应多个字符串,一个字符串也对应多个位,所以如果我们将布隆过滤器中所有被标记的位所对应的字符串全都取出,得到的这些字符串虽然包含了所有的交集数据,但是可能并不准确,会将其他非交集数据但是又映射到当前位上的数据包含,所以在这里,布隆过滤器是并不准确的,仅是近似算法

那么接下来我们来看准确做法,稍有麻烦

2.因为文件足够大,所以我们可以对其进行切割,300-600G的大小,我们可以将其切割为1000份,此时一个文件占用300-600M的大小,我们1G的内存,可以使用,之后我们再进行分析,如果平均分配的话,那么A0可以放到内存中存储到了一个set中,B0--B999小文件中的数据都得跟A0进行比较,以此类推,A1放到内存中,也需要跟B0--B999的小文件分别比较,到了这里,我们可以看到,1.优势的是部分数据可以放到内存中2.Ax的小文件数据可以放到set中,比较效率可以高很多

 但是,这种方式毕竟是需要依次比较的,效率很低,那么我们就需要思考有没有别的提高效率的方法呢?

我们可以对切割方式进行改变,不再进行平均切割,进行哈希切割:i=hash(query)%1000,i是多少,query就进入第Ai/Bi的小文件中,文件A/文件B都这样处理

经过这样的处理之后,A与B相同的query一定进入的是相同编号Ai与Bi的小文件,此时只需要在编号相同的文件中去寻找交集就可以了

 这便是这道题的解决方案

如何拓展BloomFilter使得它支持删除元素的操作

我们常规的布隆过滤器是不支持删除的,因为存在不同数据映射同一位的情况,删除一位会对其他数据有影响,那么我们如何来进行删除呢?

事实上,我们可以依据问题来解决,它存在多个数据映射,那么我们可以每个位设置一个计数器,当我们删除一个数据时,将其对应的所有位的计数器数-1,代表这个位没有了这个数据的映射,减到0时归0,那么这时问题就又出现了,到底应该用几个位来表示计数器呢,给的位如果少了,如果多个值映射到同一位置就会导致计数器溢出,比如一个byte最多计数到256(2^8),假设260个值都映射到一个位置就出问题了,但是如果使用更多的位映射一个位置,那么空间消耗就大了,这违背了我们使用布隆过滤器的特点,其特点就是节约空间

给一个超过100G大小的log file,log中存着IP地址,设计算法找到出现次数最多的IP地址,同样我们只有1个G的内存,如何找到topK的IP

我们对这道题进行分析,首先我们这道题要做的是统计次数,内存中我们一般采用kv模型的map解决,但是这里的问题是有100G的数据,放不到内存中

我们有了上面几道题的经验,可以试着将其分为1000个小文件A0--A999,读取IP计算出i=hashstr(IP)%1000,i是多少,IP就进入对应编号的Ai小文件中,这样相同的IP一定进入了同一个小文件中,此时我们再利用map<string , int >countMap,来读取Ai中的IP统计出次数,一个都问了clear,再读入另一个,使用一个pair<string,int>max记录出现次数最多的IP就可以求出

如果要找topK,那么就使用一个堆来搞定

一致性哈希

哈希在我们信息时代的用途非常广泛,很多数据的存储也利用了很多这样的知识

我们来看这样一个例子,一般情况下我们的电脑内存为4--8G,硬盘为500-1024G,假设我们要存储每个人的微信号和他的朋友圈信息,而且要方便的快速查找<微信号,朋友圈>

此时我们需要考虑服务器存储数据的问题,微信有10亿用户,假设一个用户的信息是100M,那么大概需要10亿*100M=10WT的大小,也就是说需要大概10万台服务器来存储,不仅要多机存储,还要满足增删改查的条件

我们可以进行一波分析,小马哥发了条朋友圈,插入到那台机器,浏览和删除朋友圈应该上哪台服务器去找呢?

此时信息的存储与机器就形成了一个映射关系,xiaomage信息要存到几号机器,根据映射关系,可能是i=hashstr(xiaomage)%10W,i是多少,xiaomage就存储到哪一台机器(实际中可能需要一台额外的机器存储编号信息与IP的映射关系,这样算出是i号机器,就可以找到他的IP,就可以找到服务器了)

但是我们仔细想想,这样的方案是一种解决方案,但是并不是完美的,因为我们可以试想,当数据越来越多,我们的服务器10W台都不够存储了怎么办?我们需要将服务器数量增加到15W台,如果依照之前的想法,还需要将所有的数据重新输入映射关系,重新计算数据的迁移,多于我们如此庞大的数据而言,这显然不是好的解决方案,那么我们是如何解决这个问题的呢?

这里就采用了我们的一致性哈希,我们改变了策略,改变%的数,使其变得非常大,i=hashstr(xiaomage)%2^32(其实这里也可以是其他很大的值),这样就不会仅限于固定数量比如15 W的服务器了,但是我们如果%的结果超过15W了呢?其实我们的哈希并不一定是一一对应的关系,也可以是一段区间对应一个服务器,那么此时我们就在0-2^32-1中一段范围可以映射这10W台机器,如果需要增肌5W台机器,那么不需要所有的数据迁移,只需要迁移部分负载重的机器上的数据

 比如此时我们如果想在两个数据中间加入服务器,原来数据段中间的数据都分到了3号服务器,而此时经过计算,会将一部分数据迁移到中间新加的服务器中,进入环,我们就完成增加服务器的过程,比如:计算10000-20000范围映射到3号服务器,现在增加机器了,那么10000-15000范围映射的新机器X,前移3号服务器中再10000-15000范围的数据到新的X即可

总结:一致性哈希就是给一个特别大的除数,那么增加机器也不需要整个重新计算迁移,他是一段范围映射一台机器<x1-x2,ip>那么增加机器只需要改变映射范围即可,且迁移极小部分的数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值