哈希的应用(位图,布隆过滤器)

本文深入探讨了位图和布隆过滤器的概念、实现与应用,详细讲解了位图在快速查找、排序和集合操作中的作用,以及布隆过滤器在处理大数据集时的高效查询与存储优势。

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

1.位图

1.1位图概念

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

1.2位图的实现

BitSet.hpp

#pragma once
#include<vector>
namespace bite
{
	class bitset
	{
	public:
		bitset(size_t bitCount)
			:_set(bitCount/8+1)
			,_bitCount(bitCount)
		{}
		//置1
		void set(size_t which)
		{
			if (which >= _bitCount)
				return;
			//计算对应的字节
			size_t index = (which>>3);
			size_t pos = which % 8;

			_set[index] |= (1 << pos);
		}
		//置0
		void reset(size_t which)
		{
			if (which >= _bitCount)
				return;
			//计算对应的字节
			size_t index = (which >> 3);
			size_t pos = which % 8;

			_set[index] &= ~(1 << pos);
		}
		//检测which比特位是否为1
		bool test(size_t which)
		{
			if (which >= _bitCount)
				return false;
			//计算对应的字节
			size_t index = (which >> 3);
			size_t pos = which % 8;

			return 0!=(_set[index] & (1 << pos));
		}
		//返回比特位总得个数
		size_t size()
		{
			return _bitCount;
		}
		//返回为1的比特位的总数
		size_t count()
		{
			int bitCnttable[256] = {
				0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2,
				3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3,
				3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3,
				4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4,
				3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5,
				6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4,
				4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5,
				6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5,
				3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3,
				4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6,
				6, 7, 6, 7, 7, 8 };
			size_t szcount = 0;
			for (size_t i = 0; i < _set.size(); i++)
			{
				szcount+=bitCnttable[_set[i]];
			}
			return szcount;
		}
	private:
		vector<unsigned char> _set;
		size_t _bitCount;
	};
}
void TestBitSet()//测试
{
	bite::bitset bt(100);
	bt.set(10);
	bt.set(20);
	bt.set(30);
	bt.set(40);
	bt.set(41);
	cout << bt.size() << endl;
	cout << bt.count() << endl;
	if (bt.test(40))
		cout << "40 bite is 1" << endl;
	else
		cout << "40 bite is not 1" << endl;
	bt.reset(40);
	cout << bt.count() << endl;
	if (bt.test(40))
		cout << "40 bite is 1" << endl;
	else
		cout << "40 bite is not 1" << endl;
}

1.3 位图的应用

  1. 快速查找某个数据是否在一个集合中
  2. 排序(数据不能有重复)
    在这里插入图片描述
  3. 求两个集合的交集、并集等
    交集:映射两个位图,查看相同为1的就是交集
    并集:映射两个位图,有1的就是并集
  4. 操作系统中磁盘块标记

2.布隆过滤器

2.1布隆过滤器提出

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

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:不能处理哈希冲突
  3. 将哈希与位图结合,即布隆过滤器

2.2布隆过滤器概念

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

2.3布隆过滤器的插入

以下图片来自知乎:Young Chen
来源自
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如以下针对值:
1.首先是向布隆过滤器插入"baidu” ,假如"baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,如下图:
在这里插入图片描述
2.接着是向布隆过滤器插入"tencent” ,如果"tencent” 的三个不同的哈希函数分别生成了哈希值 3、4、8,如下图:
在这里插入图片描述
3.由上图可以知道,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “huawei” 这个值是否存在,假如这个值的哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,这说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “huawei” 这个值一定不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

可能存在的原因:

随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样像某个值 “jingdong” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “jingdong” 这个值存在。
Common.hpp<----代码来源链接

#pragma once
#include <string>
class StrINT1
{
public:
	size_t operator()(const std::string& s)
	{
		return SDBMHash(s.c_str());
	}
	unsigned int SDBMHash(const char *str)
	{
		unsigned int hash = 0;
		while (*str)
		{
			// equivalent to: hash = 65599*hash + (*str++);
			hash = (*str++) + (hash << 6) + (hash << 16) - hash;
		}
		return (hash & 0x7FFFFFFF);
	}

};

struct StrINT2
{
public:
	size_t operator()(const std::string& s)
	{
		return RSHash(s.c_str());
	}
	// RS Hash Function
	unsigned int RSHash(const char *str)
	{
		unsigned int b = 378551;
		unsigned int a = 63689;
		unsigned int hash = 0;

		while (*str)
		{
			hash = hash * a + (*str++);
			a *= b;
		}

		return (hash & 0x7FFFFFFF);
	}
}; 

struct StrINT3
{
public:
	size_t operator()(const std::string& s)
	{
		return JSHash(s.c_str());
	}
	// JS Hash Function
	unsigned int JSHash(const char *str)
	{
		unsigned int hash = 1315423911;

		while (*str)
		{
			hash ^= ((hash << 5) + (*str++) + (hash >> 2));
		}

		return (hash & 0x7FFFFFFF);
	}
};

struct StrINT4
{
public:
	size_t operator()(const std::string& s)
	{
		return PJWHash(s.c_str());
	}
	// P. J. Weinberger Hash Function
	unsigned int PJWHash(const char *str)
	{
		unsigned int BitsInUnignedInt = (unsigned int)(sizeof(unsigned int) * 8);
		unsigned int ThreeQuarters = (unsigned int)((BitsInUnignedInt * 3) / 4);
		unsigned int OneEighth = (unsigned int)(BitsInUnignedInt / 8);
		unsigned int HighBits = (unsigned int)(0xFFFFFFFF) << (BitsInUnignedInt - OneEighth);
		unsigned int hash = 0;
		unsigned int test = 0;

		while (*str)
		{
			hash = (hash << OneEighth) + (*str++);
			if ((test = hash & HighBits) != 0)
			{
				hash = ((hash ^ (test >> ThreeQuarters)) & (~HighBits));
			}
		}

		return (hash & 0x7FFFFFFF);
	}
};


struct StrINT5
{
public:
	size_t operator()(const std::string& s)
	{
		return ELFHash(s.c_str());
	}
	// ELF Hash Function
	unsigned int ELFHash(const char *str)
	{
		unsigned int hash = 0;
		unsigned int x = 0;

		while (*str)
		{
			hash = (hash << 4) + (*str++);
			if ((x = hash & 0xF0000000L) != 0)
			{
				hash ^= (x >> 24);
				hash &= ~x;
			}
		}

		return (hash & 0x7FFFFFFF);
	}

};

struct StrINT6
{
public:
	size_t operator()(const std::string& s)
	{
		return BKDRHash(s.c_str());
	}

	// BKDR Hash Function
	unsigned int BKDRHash(const char *str)
	{
		unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
		unsigned int hash = 0;

		while (*str)
		{
			hash = hash * seed + (*str++);
		}

		return (hash & 0x7FFFFFFF);
	}

};
struct StrINT7
{
public:
	size_t operator()(const std::string& s)
	{
		return DJBHash(s.c_str());
	}
	// DJB Hash Function
	unsigned int DJBHash(const char *str)
	{
		unsigned int hash = 5381;

		while (*str)
		{
			hash += (hash << 5) + (*str++);
		}

		return (hash & 0x7FFFFFFF);
	}
};

struct StrINT8
{
public:
	size_t operator()(const std::string& s)
	{
		return APHash(s.c_str());
	}
	// AP Hash Function
	unsigned int APHash(const char *str)
	{
		unsigned int hash = 0;
		int i;

		for (i = 0; *str; i++)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));
			}
		}
		return (hash & 0x7FFFFFFF);
	}
};

BloomFilter.hpp

#pragma once
#include"BitSet.hpp"
#include "Common.hpp"
//BloomFilter:位图+多个哈希
template<class T,class HF1=StrINT1,class HF2= StrINT2
	,class HF3= StrINT3, class HF4= StrINT4
	, class HF5= StrINT5>
class BloomFilter
{
public:
	BloomFilter(size_t size=10)
		:_bt(10*size)
		,_size(0)
	{}
	bool Insert(const T& data)
	{
		size_t index1 = HF1()(data) % _bt.size();
		size_t index2 = HF2()(data) % _bt.size();
		size_t index3 = HF3()(data) % _bt.size();
		size_t index4 = HF4()(data) % _bt.size();
		size_t index5 = HF5()(data) % _bt.size();
		_bt.set(index1);
		_bt.set(index2);
		_bt.set(index3);
		_bt.set(index4);
		_bt.set(index5);
		return true;
	}
	bool IsIn(const T& data)//布隆过滤器的查找
	{
		size_t index = HF1()(data) % _bt.size();
		if (!_bt.test(index))
			return false;
		index = HF2()(data) % _bt.size();
		if (!_bt.test(index))
			return false;
		index = HF3()(data) % _bt.size();
		if (!_bt.test(index))
			return false;
		index = HF4()(data) % _bt.size();
		if (!_bt.test(index))
			return false;
		index = HF5()(data) % _bt.size();
		if (!_bt.test(index))
			return false;
		_size++;
		//元素可能在
		return true;
	}
	size_t Size()const
	{
		return _size;
	}
private:
	bite::bitset _bt;
	size_t _size;
};
void TestBloomFilter()//测试
{
	//坏人名单
	BloomFilter<string> bf;
	bf.Insert("本拉登");
	bf.Insert("希特勒");
	bf.Insert("裕仁天皇");
	cout << bf.Size() << endl;
	if (bf.IsIn("蒋介石"))
		cout << "坏人" << endl;
	else
		cout << "好人" << endl;
	if (bf.IsIn("裕仁天皇"))
		cout << "坏人" << endl;
	else
		cout << "好人" << endl;

}

2.4布隆过滤器的查找

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

2.5布隆过滤器删除

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

缺陷:

  1. 无法确认元素是否真正在布隆过滤器中
  2. 存在计数回绕

2.6布隆过滤器优缺点

优点:
  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
  2. 哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点:
  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白
    名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题

3.海量数据面试题

3.1哈希切割

1.1给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
文件100G大文件—>分割—>1000份---->100M
分割大文件:如果能将相同的IP地址放到同一个文件中
如果有内存限制(比如1G)—>文件总大小/内存限制
哈希分割:
从源文件中获取一个IP地址—>IP(可逆转化为整型数据)%文件份数
统计每个IP地址出现的次数<IP,次数>即:<整型数据,次数>

利用unordered_map
m[ip]++;

然后for()—>找到出现次数最多的ip地址


1.2与上题条件相同,如何找到top K的IP?
堆—>最多前k个ip地址—><次数,ip地址>


1.3如何直接用Linux系统命令实现?

3.2位图应用

2.1 给定100亿个整数,设计算法找到只出现一次的整数?
2个比特位表示数据(一个字节只能表示4个数据):00、01、10、11(需要空间:2^32/4=1G)

00:没有出现
01:出现一次
10:出现多次
11:不用

取一个数据:对应哪个字节,哪两个比特位

if(00)
	01
else if(01)
	10

2.2 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
映射两个文件需要空间:512MB+512MB=1G
查看相同比特位是否为1:1是交集
2.3 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

类似第一题

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

位图解决

3.3布隆过滤器

3.1 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
3.2 如何扩展BloomFilter使得它支持删除元素的操作

3.4倒排索引

4.1给上千个文件,每个文件大小为1K—100M。给n个词,设计算法对每个词找到所有包含它的文件,你只有100K内存

4.常见的面试题

4.1 什么是哈希冲突?
4.2 处理哈希冲突的方法有哪些?优缺点分别是什么?
4.3 哈希表的增删查改的时间复杂度是多少?开放定制法哈希冲突很严重怎么办?哈希桶哈希冲突很严重怎么办?
4.4 海量数据处理的面试题?
4.5 unordered_map和map的区别是什么?哪一个更好一些?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值