哈希应用 位图与布隆过滤器 及海量数据处理问题

位图

先介绍一个题:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
解决方案:

  1. 遍历,时间复杂度O(N)
  2. 排序(O(NlogN)),利用二分查找: logN
  3. 把数据放入map或者unordered_map中,树形结构查找
  4. 位图解决

面对这个问题我们首先想到的就是前三种。但是前三种做法的前提就是需要把数据移到内存上才可以运行,但是40亿个整型,差不多16个G(十亿字节差不多为一个G,一个整型占四个字节),而我们的电脑内存一般都是4~16G,根本无法完成。
而方法4,根据在不在的问题利用一个比特位只能为1或者为0,可以将整形元素利用直接定址法映射到一个整型数组中,然后利用一个字节八比特,即一个整型便可记录4*8=32个元素在不在的状态,且总共才消耗16G/32=500M的空间,且利用哈希的直接定址法映射,查找判断效率为O(1).完美的解决了此问题。

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

#pragma once
#include<vector>
using namespace std;

namespace MyBitSet
{
	class bitset
	{
	public:
		bitset(int N)
		{
			_bits.resize(N / 32 + 1, 0);  //每个比特映射一个元素,而这个整型数组里每一个整形元素占用32位比特,所以只需要开 N/32的空间便可以映射到每一个值,但是可能存在 33/32=1 ,一个整型空间是无法映射33个比特的,所以要加1,向上取整
			_bitcount = 0;
		}
		void setbit(size_t x) //将此整型x元素映射到位图中,即该映射位置置为1,
		{
			size_t index = x / 32; //算出具体映射在具体那个整型的32bit里
			size_t pos = x % 32;  //算出具体在那个bit位
			_bits[index] |= (1 << pos); //将此bit位置为1        //本来位图里都是初始化为0的,按位或操作,即有一为一,将pos位的bit置为1
			_bitcount++;
		}
		void resetbit(size_t x)//移除位图中x对应的bit位,删除
		{
			size_t index = x / 32;
			size_t pos = x % 32;
			_bits[index] &= ~(1 << pos); //将此bit位置位0   //不能影响到其他的位置,先找到pos位置,再取反,其他位就为1了,此位为0,再按位&操作,将pos位bit置为0,其他位不影响
			_bitcount--;
		}
		bool testbit(size_t x)// 判断x在不在(也就是说x映射的位是否为1)
		{
			size_t index = x / 32;
			size_t pos = x % 32;
			return _bits[index] & (1 << pos); //只是判断,并不设置,按位&操作
		}

	private:
		vector <int> _bits;   //位图
		size_t _bitcount; //映射存储的多少个有效数据
	};

	void test_bitset()
	{
		bitset bs(100);
		bs.setbit(99);
		bs.setbit(98);
		bs.setbit(97);
		bs.setbit(5);
		bs.resetbit(98);

		for (size_t i = 0; i < 100; ++i)
		{
			printf("[%d]:%d\n", i, bs.testbit(i));
		}
		
		//或者直接设置为整型最大值
		//bitset bs(-1);
		//bitset bs(0xffffffff);

	}
}

位图的应用

  1. 快速查找某个数据是否在一个集合中
  2. 排序 (之前计数排序的思想)
  3. 求两个集合的交集、并集等(比较两个集合中元素的对应映射的状态)
  4. 操作系统中磁盘块标记

布隆过滤器

布隆过滤器提出
我们在刷b站时,在推荐栏里它会给我们不停地推荐新的内容,其实它每次推荐时要去重,去掉那些已经看过的内容。问题来了,推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新的内容时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:位图只能存整数元素的状态,而要是存字符串类型的元素,再精妙的哈希函数也会存在哈希冲突,所以位图不能处理哈希冲突的问题
  3. 将哈希与位图结合,即布隆过滤器

布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
下图便是使用三个不同的哈希函数对每一个元素进行映射:
在这里插入图片描述
但是布隆过滤器可能存在误判的情况,有时候某个元素恰好对应的三个映射位置为1,会认为是存在的,但是事实是这个元素并不存在,只是恰好其三个映射位置与其他元素的映射位置重合而导致的误判;但是找不存在时,例如上图的w元素,只要其有一个映射位置为0,那就说明没出现过;
总结:
布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。

布隆过滤器模拟实现

#pragma once
#include "bitset.h"
#include<string>
#include <iostream>
using namespace std;
//布隆过滤器可以映射字符串类型,即通过哈希函数转整型,再映射  定义三个不同的哈希仿函数类,每一个元素映射三个不同的bit位,
//即哈希和位图的结合,但是也必然存在哈希冲突的问题
namespace MyBloomFilter
{

	struct HashStr1
	{
		//BKDR算法
		size_t operator()(const string& s)
		{
			size_t count = 0;
			for (size_t i = 0; i < s.size(); i++)
			{
				count *= 131;
				count += s[i];
			}
			return count;
		}
	};
	struct HashStr2
	{
		// RSHash算法
		size_t operator()(const string& s)
		{
			size_t count = 0;
			size_t magic = 63689; //魔数
			for (size_t i = 0; i < s.size(); i++)
			{
				count *= magic;
				count += s[i];
				magic *= 378551;
			}
			return count;
		}
	};
	struct HashStr3
	{
		// SDBMHash算法
		size_t operator()(const string& s)
		{
			size_t count = 0;
			for (size_t i = 0; i < s.size(); i++)
			{
				count *= 65599;
				count += s[i];
			}
			return count;
		}

	};
	template <class K=string, class Hash1 = HashStr1,class Hash2 = HashStr2,class Hash3 = HashStr3>
	class bloomfilter
	{
	public:
		bloomfilter(size_t N)
			:_bs(3*N)
			, _bitnum(3*N)
		{}
		
		void setbit(const K& key)
		{
			size_t index1 = Hash1()(key) % _bitnum;
			size_t index2 = Hash2()(key) % _bitnum;
			size_t index3 = Hash3()(key) % _bitnum;
			_bs.setbit(index1);
			_bs.setbit(index2);
			_bs.setbit(index3);
		}

		bool testbit(const K& key)
		{
			size_t index1 = Hash1()(key) % _bitnum;
			size_t index2 = Hash2()(key) % _bitnum;
			size_t index3 = Hash3()(key) % _bitnum;
			//只要有一个映射位置不存在,则绝对不存在
			if (_bs.testbit(index1) == false)
				return false;
			if (_bs.testbit(index2) == false)
				return false;
			if (_bs.testbit(index3) == false)
				return false;

			return true; // 但是这里也不一定是真的在,还是可能存在误判   可能存在一个元素映射的三个位置可能是其他的元素映射的不同位置
			// 判断在,是不准确的,可能存在误判
			// 判断不在,是准确
		}
		//void resetbit(const K& key)
		//{
			// 将映射的位置给置0就可以,但是是三个哈希函数的映射位置,所以不能随便删除
			// 不支持删除,可能会存在误删。一般布隆过滤器不支持删除	
		//}

	private:
		MyBitSet::bitset _bs;//底层也还是位图,只不过还利用了哈希函数处理字符串的思想,且有几个哈希函数便要多开几倍的空间
		size_t _bitnum;
	};

	void test_bloomfilter()
	{
		bloomfilter<std::string> bf(100);
		bf.setbit("abcd");
		bf.setbit("aadd");
		bf.setbit("bcad");

		cout << bf.testbit("abcd") << endl;
		cout << bf.testbit("aadd") << endl;
		cout << bf.testbit("bcad") << endl;
		cout << bf.testbit("cbad") << endl;
	}
}

布隆过滤器优点

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

布隆过滤器缺陷
7. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
8. 不能获取元素本身
9. 一般情况下不能从布隆过滤器中删除元素

海量数据面试题思路分析

1.哈希切割类型问题()
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?
解决方案:
分析:首先这里要做的是统计次数,统计次数我们一般用kv模型的map/unordered_map解决,但是这里的问题是有100G数据,放不到内存中。但是我们可以利用分治的思想,将其分为1000个100M的小文件,而利用相同哈希函数进入到同一个文件的,则ip相同的也一定在一个文件中。
在这里插入图片描述
使用map<string,int> countMap,读取Ai中的人IP统计出次数,一个文件读完了clear,再加载读另一个文件。且使用一个pair<string,int> max记录保存出现次数最多的IP就可以求出答案。
如果要找topK,那么就是用一个堆来搞定就可以(主要是分治的思想,分为多个小文件,再利用其他的数据结构处理)。

2.位图应用类型问题(整型/涉及到出现次数)

  1. 给定100亿个整数,设计算法找到只出现一次的整数?
    判断存不存在可以用一个比特位0/1标识存不存在;
    而判断出现一次的次数问题,可以使用两个比特位可以表示0~3次,(即使用两个位图,每个位图提供一个比特位判断);
    00对应出现0次、01表示出现1次,10则表示出现2次及2次以上(应为题目中只要求出现一次的,所以包含即可)
class sultion
{
	//标记出现次数
	void bitset(size_t x)
	{
		if (_bs1.testbit(x) == false && _bs2.testbit(x) == false) // 00 出现0次的,则一个位图设置为1 -----》01,设置为出现一次
		{
			_bs2.setbit(x);
		}
		else if (_bs1.testbit(x) == false && _bs2.testbit(x) == true) //01 ,则出现一次的 ------》10.设置为出现两次
		{
			_bs1.setbit(x);
			_bs2.resetbit(x);
		}
		//其他的情况就是两次即两次以上的了,不用关心
	}
	bool findone(size_t x) //判断是否出现一次,即01状态的
	{
		if (_bs1.testbit(x) == false && _bs2.testbit(x) == true)
			return true;
	}
	/遍历插入位图中,再遍历每一个数,看是否只出现一次即可得出答案
private:
	MyBitSet::bitset _bs1; 
	MyBitSet::bitset _bs2;
};
  1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
    方案1:将文件1的整数映射到一个位图中,读取另外一个文件2中的整数,判断在不在位图,在就是交集。消耗500M内存
    方案2:将文件1的整数映射到位图1中,将文件2的整数映射到位图2中,然后将两个位图中的数按位与。与操作之后为1的位就是交集。 消耗内存1G。

  2. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
    本题跟上面的第1题思路是一样的,而本题找的不超过2次的,也就是要找出现1次和2次的整数;
    还是用两个位表示一个数,分为出现0次00表示,出现1次的01表示,出现2次的 10表示,出现3次及3次以上的用11表示

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

分析: query一般 是sql查询语句或者网络请求的ur1等,一般是一个字符串类型;
假设平均一个query 30~60比特,100亿个query大约占用300G ~600G。所以也是个海量数据问题。

近似算法(利用布隆过滤器):将文件1中的query映射到一个布隆过滤器,读取文件2中的query,判断在不在布隆过滤器中,在就是交集。但是是由缺陷的:交集中有些数不准确,不存在的可以会被误判为存在的。

精确算法(利用分治思想和哈希切分和set去重查找)
分析思路:这两个文件都非常大,大概在300~ 600G之间,也没有合适的数据结构能直接精确的找出交集。文件很大不能都放到内存中,那么我们可以把文件切分多个小文件,小文件数据加载到内存中。一般切出来一个小文件的大小能放进内存就可以。那么这里一个文件300- 600G,切1000份,一个文件300~600M,这里有1G内存,所以可以搞定。

切分文件时利用哈希切分: i = hashstr (query) % 1000, i计算出来多少,此query就进入第Ai/Bi的小文件中,文件A/ 文件B都别这样处理,由于是一样的哈希函数,所以相同的query必然在对应的i文件里,再判断交集即可。
在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值