哈希扩展(位图与布隆过滤器)

news2024/9/25 19:22:43

文章目录

  • 位图
  • 布隆过滤器
    • 实现原理:
      • HashMap的问题
      • 布隆过滤器结果
  • 海量数据处理
    • 位图应用
    • 布隆过滤器应用

位图

问题:从40亿个没有排序的无符号整数查找一个数是否存在

  1. 方法一:
    依次遍历:O(N) 太慢了

  2. 排序 + 二分

1G = 1024MB = 1024 * 1024KB = 1024 1024*1024byte ~= 10亿byte。*

10亿byte是一个G,那么40亿个整形为160亿字节 ,也就是16GB的内存,那我们内存肯定放不下,但是内存却放不下。

  1. 位图

2 32 2^{32} 232 = 42亿多,那么其实我们并不需要把每一个数都存储下来,我们可以使用一个bit位来表示它在不在即可,那么一个字节是8个比特位,所以我们所需要的空间为,$2^{32} /$8 ~= 5亿 ~= 500MB。那么500MB是可以接受的。

在这里插入图片描述

如上图,每一个蓝色方块代表的是一个整形,每个整型都有32位bit位,以第一个整型为例,我们可以表示 0~31这个范围的数字在不在,**如果在对应的bit位就变为1,如果不在对应的bit位就是0。**那我们如何直到插入的数字x是那个位置的呢? i = x / 32 i = x / 32 i=x/32 j = x j = x % 32 j=x,我们这里的 i i i表示我们对应的哪个整型,而 j j j表示对应的整型的哪个bit位。

位图代码实现:

namespace bit
{
	template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			_bs.resize(N/32 + 1);
		}
		void set(size_t x)
		{
			int i = x / 32;
			int j = x % 32;
			//把对应的第j个bit位变为1,其他位置不变
			//j = 4,  00000000 |= 00010000 = 00010000
			_bs[i] |= (1 << j);
		}

		void reset(size_t x)
		{
			int i = x / 32;
			int j = x % 32;
			//把对应的第j个bit位变为0,其他位置不变
			//j = 4,  00000000 &= (~00010000) -> 11101111 = 00000000
			_bs[i] &= (~(1 << j));
		}

		//验证x是否存在
		bool test(size_t x)
		{
			int i = x / 32;
			int j = x % 32;
			return (_bs[i] & (1 << j));
		}
	private:
		vector<int> _bs;
	};
}

在这里插入图片描述

上图的蓝色方块其实就是一个个的整型,我们创建一个vector来存储每一个整型,在创建vector的时候,我们要根据数据的范围来初始化vector大小,由于一个整型最大为42亿多,所以我们最多需要开辟134217728个整型。而我们vector是在堆区上开辟的所以也就不会存在内存不够的问题。这样查询42亿中一个数是否存在我们就可以在内存当中操控了。134217728*4 = 536,870,912 ~= 500MB。

总结一下位图:

  • 优点:效率高,方便插入与删除
  • 缺点:只适合整型。

在这里插入图片描述

如上图,库里面其实也实现了bitset,只不过它实现的方式和我们的不同,库里面直接使用一个整型来存储对应位置是否存在,所以他这里如果直接存储42亿的话还会报错,空间太大了。

布隆过滤器

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

布隆过滤器是哈希表和位图的结合使用,我们使用hash函数来把字符串变为整型,让对应位图中的位置变为1。相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

实现原理:

HashMap的问题

回想一下,如果让你判断一个某个元素是否存在的时候,相信大部分人都会选择HashMap,O(1)时间就可以得出结果了,但是HashMap的缺点就是空间消耗太大了,而且由于负载因子的原因,我们数组并不能被放满,那么当数据量很大例如上亿的时候,所需内存空间也很大。

布隆过滤器结果

布隆过滤器跟位图的一样的,如下图:

在这里插入图片描述

和位图不一样的是,我们的位置是由哈希函数算出来的,所以我们不单单可以使用整型也可以使用字符串,通过对应的哈希函数转为整型,而由于字符串哈希函数可能会("abcba)和(“caabb”)产生冲突,我们一般一个元素会有多个哈希函数来计算出不同的位置,这样可以降低冲突几率。

在这里插入图片描述

如上图,我们"abandon"通过三个哈希函数算出来的位置为: [ 3 , 8 , 12 ] [3,8,12] [3,8,12],而 " b a i d u " "baidu" "baidu"通过三个函数算出来的位置为: [ 5 , 8 , 10 ] [5,8,10] [5,8,10]这也就代表了我们这个位置可能会被别的字符串占用,这也就导致了我们布隆过滤器可能会出现误判的情况。

例如:随着数据量的增加,我们即是内存里面并没有 " a p p l e " "apple" "apple"这个字符串,但是通过哈希函数算出来的位置都被别人占完了,所以 即是内存里面并没有这个元素,返回的结果也可能为有。

同时布隆过滤器还有几个公式:

m : 表示布隆过滤器中bit的位数

n:表示插入元素的个数

k:表示哈希函数的个数

结论:

哈希函数个数固定的时候我们有一个结论: ( 误判率 ) f ( k ) = ( 1 − 1 e n k m ) k (误判率) f(k) = (1-\frac{1}{{e^{\frac{nk}{m}}}})^k (误判率)f(k)=(1emnk1)k这就意味着我们的n越大,我们的分母越大,整个数越小,1-这个数就越大,所以误判率就越大,而我们的m越大我们的误判率越小m/n越大,误判率越小。

当我们的n和m固定的时候, k = m n l n 2 k = \frac{m}{n}ln2 k=nmln2 误判率最小。

当我们误判率p和插入个数n去确定的情况下,通过期望的误判率和插入个数可以得出来我们的bit长度为: m = − n l n p ( l n 2 ) 2 m = -\frac{nlnp}{(ln2)^2} m=(ln2)2nlnp

代码实现:

//三个哈希函数
struct BKDRHash
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 31;
			value += ch;
		}
		return value;
	}
};

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

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

namespace bit
{
	//N表示插入个数,X表示 M / N (M为bit长度)
	template<size_t N,
			size_t X = 5,
			class K = string,
			class Hash1 = BKDRHash,
			class Hash2 = DJBHash,
			class Hash3 = APHash>
	class boolmfilter
	{
	public:
		void set(const K& key)
		{
			size_t M = N * X;
			//分别求出对应的整型
			size_t hash1 = Hash1()(key) % M;
			size_t hash2 = Hash2()(key) % M;
			size_t hash3 = Hash3()(key) % M;

			//设置到位图当中
			_bs.set(hash1);
			_bs.set(hash2);
			_bs.set(hash3);
		}

		bool test(const K& key)
		{
			size_t M = N * X;
			//三个位置当中只要有一个位置不存在,那么这个元素一定不存在。
			size_t hash1 = Hash1()(key) % M; //不能超过bit数组可容纳的长度
			if (!_bs.test(hash1))
				return false;
			size_t hash2 = Hash2()(key) % M;
			if (!_bs.test(hash2))
				return false;
			size_t hash3 = Hash3()(key) % M;
			if (!_bs.test(hash3))
				return false;

			//如果三个位置都存在也不一定存在,可能是其他元素把位置占完了。
			return true;
		}
	private:
		bit::bitset<N*X> _bs;
	};
}

void Test_boolmfilter()
{
	string s1("apple");
	string s2("banana");
	string s3("pear");

	bit::boolmfilter<10> bs1;
	bs1.set(s1);
	bs1.set(s2);
	bs1.set(s3);

	//前三个在,后三个不在
	cout << bs1.test(s1) << endl;
	cout << bs1.test(s2) << endl;
	cout << bs1.test(s3) << endl;
	cout << bs1.test("aaaa") << endl;
	cout << bs1.test("bbbb") << endl;
	cout << bs1.test("cccc") << endl;
}

当然了想要测出来本来不在的却输出在我们还需要更多的数据,这里数据太少了测不出来。

//测试相似误判和不相似误判
void Test_BoolFilter()
{
	srand(time(0));
	const int N = 1000000;
	vector<string> v;
	
    //原始数据
	bit::boolmfilter<N> bs;
	string url = "https://blog.csdn.net/anjibgdexuexi?type=blog";
	for (int i = 0; i < N; i++)
	{
		v.push_back(url + std::to_string(i));
	}
	for (auto& e : v) bs.set(e);

	v.clear();
	//相似数据,相同前缀,不同后缀
	for (int i = 0; i < N; i++)
	{
		string nurl = url;
		nurl += std::to_string(9999999 + i);
		v.push_back(nurl);
	}
	int n = 0;
	for (auto& e : v)
	{
		if (bs.test(e))
			n++;
	}
	cout << "相似字符串误判率:" << (double)n / (double)N << endl;

	v.clear();

	//不相似,前后缀都不一样
	for (int i = 0; i < N; i++)
	{
		string str = "孙悟空";
		str += std::to_string(rand() + i);
		v.push_back(str);
	}

	int n1 = 0;
	for (auto& e : v)
	{
		if (bs.test(e))
			n1++;
	}
	cout << "不相似字符串误判率:" << (double)n1 / (double)N << endl;
}

上段测试代码,我们就可以看到布隆过滤器的误判率了,同时当我们m/n越大我们误判率就越小。

海量数据处理

位图应用

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

因为有一百亿个整数,但是由于整数最大的值就是42亿多,所以我们设置bit位长度的时候只需要设置 2 32 2^{32} 232即可。剩下的大量的重复值。当我们需要记录某个元素的出现次数的时候我们一般想到的都是哈希表,这里数据量太大内存肯定是不够的,所以我们可以使用位图。上述我们位图每个元素用一个bit位来表示在不在。

那么这里的话我们只需要使用两个bit位来表示:

  1. 如果两个bit位都是0,说明不在
  2. 如果第一个为0,第二个为1,表示出现了一次
  3. 如果第一个为1,第二个为0,表示出现了2次
  4. 如果两个都为1,表示出现了三次及以上

代码实现:以下代码对于超过两次的统一设置为3次。

template<size_t N>
	class twobitset
	{
	public:
		twobitset()
		{
			_bs.resize(N / 32 + 1);
		}
		void set(size_t x)
		{
			int i = x / 16;
			int j = x % 16;
			//第j位和j+16位来表示
			int one = (_bs[i] & (1 << (j+16))) > 0; //获取第j+16位
			int two = (_bs[i] & (1 << j)) > 0;       //获取第j位
			if (one == 0 && two == 0) // 00 ->01
			{
				_bs[i] |= (1 << j);
			}
			else if (one == 0 && two == 1) // 01 -> 10
			{
				_bs[i] &= (~(1 << j));
				_bs[i] |= (1 << (j + 16));
			}
			else if (one == 1 && two == 0) // 10 -> 11
			{
				_bs[i] |= (1 << j);
			}
		}
		//验证x是否存在
		int test(size_t x)
		{
			int i = x / 16;
			int j = x % 16;

			//第j位和j+16位来表示
			int one = (_bs[i] & (1 << (j + 16))) > 0; //获取第j+16位
			int two = (_bs[i] & (1 << j)) > 0;       //获取第j位

			if (one == 0 && two == 0) // 00 ->01
			{
				return 0;
			}
			else if (one == 0 && two == 1) // 01 -> 10
			{
				return 1;
			}
			else if (one == 1 && two == 0) // 10 -> 11
			{
				return 2;
			}
			else
				return 3;
		}
	private:
		vector<int> _bs;
	};

如上段代码,我们用每个整型的前16个bit位和后16个bit位表示出现的次数。但其实还有另一种实现方式,我们直接使用两个位图,那么次数还是一样的变化过程,而且比我们上面这种方式要简单很多,直接复用位图的成员函数。

代码实现:

	template<size_t N>
	class twobitset
	{
	public:
		void set(size_t x)
		{
			if (_bs1.test(x) == 0 && _bs2.test(x) == 0) // 00 ->01
			{
				_bs2.set(x);
			}
			else if (_bs1.test(x) == 0 && _bs2.test(x) == 1) // 01 -> 10
			{
				_bs2.reset(x);
				_bs1.set(x);
			}
			else if (_bs1.test(x) == 1 && _bs2.test(x) == 0) // 10 -> 11
			{
				_bs2.set(x);
			}
			//超过两次的这里不处理
		}
		//验证x是否存在
		int test(size_t x)
		{

			if (_bs1.test(x) == 0 && _bs2.test(x) == 0) // 00 
			{
				return 0;
			}
			else if (_bs1.test(x) == 0 && _bs2.test(x) == 1) // 01 
			{
				return 1;

			}
			else if (_bs1.test(x) == 1 && _bs2.test(x) == 0) // 10 
			{
				return 2;

			}
			else
				return 3;
		}
	private:
        //直接复用位图
		bit::bitset<N> _bs1; 
		bit::bitset<N> _bs2;
	};
}
  • 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

使用位图遍历其中一个文件,统计每个元素是否存在,然后再依次遍历另一个文件,如果在位图中,则 加入到交集里面。

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

如上段代码,都已次数小于等于2的即可。

布隆过滤器应用

  • 给两个文件,分别有100亿个query(查询字符串),我们只有1G内存,如何找到两个文件交集? 假设每一个query都有50个字节,那么一共有5000亿字节 ~= 500G内存,那如何做呢?
  1. 平均分成1000个小文件。

在这里插入图片描述

如上图,我们把大文件A和大文件B均分为1000个小文件,然后再进行查找交集,但是这里有均分会有一个问题,我们A0需要去和 B 0 , B 1... B 999 B0,B1...B999 B0,B1...B999每一个都要去找交集,A1也是一样那么这里就是 O ( N 2 ) O(N^2) O(N2)的时间复杂度。

  1. 哈希切分

在这里插入图片描述

我们通过哈希函数求出每个 q u e r y query query然后对1000取模,就可以求出这1000个小文件对应的坐标,那么我们 A i Ai Ai小文件就不需要每个都要和 B i Bi Bi小文件去查交集了

因为我们相同字符串通过哈希函数%1000一定求出来的是同一个坐标,不可能求出来的坐标不同,所以我们 A i Ai Ai只需和对应的 B i Bi Bi去找交集即可,这样时间复杂度有 O ( N 2 ) − > O ( N ) O(N^2)->O(N) O(N2)>O(N)不过这里有个缺陷,由于布隆过滤器会出现误判的情况,对与交集我们肯定不会错,但是不是交集的可能也被误判进去了O.o?。

但是哈希切分还有一个问题,那就是有可能其中一个文件特别大,几G或者几十G都有可能(重复值过多或求出来的哈希值一样),本质上都是哈希冲突,那怎么办呢?

针对这个问题可以进行二次划分,再使用一个新的哈希函数进行求坐标。就是当一个文件的内容太大的时候,在对这个 A i Ai Ai划分为多个 A A i AAi AAi,于此同时我们对应的 B i Bi Bi也需要划分为多个 B B i BBi BBi但其实我们可以不管这个问题,无论文件多么的大,我们都直接使用set插入即可。

我们set插入失败只有两种可能:1. query已存在。2. 内存不够,那么如果是重复值过多的情况下,由于set的特性就可以解决,如果真的就是哈希函数不行,我们不同的值也很多, 那么单独对这个文件再换一个哈希函数进行重新划分即可。

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

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

相关文章

Python3.12兼容性问题-ImpImporter替换的解决办法

前言 目前现有的很多Python代码都是基于Python3.8、或者Python3.9的甚至是更早的版本。 当我们用最新的Python3.12来跑这些程序的时候&#xff0c;就会出现很多兼容性的问题。 本文就对“ImpImporter”和“zipimporter”的替换问题给出了一个解决方案。 1、错误描述 Attribu…

JVM系列(七) -对象的内存分配流程

一、摘要 在之前的文章中,我们介绍了类加载的过程、JVM 内存布局和对象的创建过程相关的知识。 本篇综合之前的知识,重点介绍一下对象的内存分配流程。 二、对象的内存分配原则 在之前的 JVM 内存结构布局的文章中,我们介绍到了 Java 堆的内存布局,由 年轻代 (Young Ge…

2024数学建模国赛D题39页成品论文+每小问解题代码+可视化结果

2024 年高教社杯全国大学生数学建模竞赛完整分析文章 点击链接加入群聊【2024数学建模国赛资料汇总】&#xff1a;http://qm.qq.com/cgi-bin/qm/qr?_wv1027&klZncBILk30DuPRI1Bd8X-3Djv7ZVZyAv&authKeykKqNSSEbbZN%2FVKn%2BICOqJGahEHfhJEe7BSxK5IMua%2BYQqwaHkGaHvK…

ArrayList是线程安全的吗?怎么解决?

ArrayList是一个线程不安全的容器&#xff0c;如果在多线程中使用ArrayList&#xff0c;可能会导致程序出错。那可能是哪些问题呢&#xff1f;请看一个例子&#xff1a; package test;import java.util.ArrayList;/*** ArrayList并发问题演示*/ public class ArrayListMultiTh…

渗透测试中目标主机不出网的解决办法

当目标主机无法直接访问互联网时&#xff08;即“目标主机不出网”&#xff09;&#xff0c;渗透测试的难度会增加&#xff0c;因为攻击者无法通过通常的外部资源&#xff08;如命令与控制服务器、远程主机&#xff09;与目标通信。然而&#xff0c;有多种技术可以帮助渗透测试…

Bagging: 数量,而不是质量。

由 AI 生成&#xff1a;过度简化的树、引导聚合、集成方法、弱学习器、减少方差 集成方法 — 数量&#xff0c;而不是质量 一、说明 机器学习中的集成方法是指组合多个模型以提高预测性能的技术。集成方法背后的基本思想是聚合多个基础模型&#xff08;通常称为弱学习器&#…

在JavaScript当中,为什么NaN===NaN和NaN==NaN返回false?

作者&#xff1a;CSDN-PleaSure乐事 欢迎大家阅读我的博客 希望大家喜欢 使用环境&#xff1a;vscode chrome 今天这个话题的缘由是早上和一位同专业的学长聊天&#xff0c;他在公司完成了一个项目&#xff0c;项目当中有一个40个字段的返回值&#xff0c;于是牵扯到了某道他遇…

算法练习题14——leetcode84柱形图中最大的矩形(单调栈)

题目描述&#xff1a; 解题思路&#xff1a; 要解决这个问题&#xff0c;我们需要找到每个柱子可以扩展的最大左右边界&#xff0c;然后计算以每个柱子为高度的最大矩形面积。 具体步骤如下&#xff1a; 计算每个柱子左侧最近的比当前柱子矮的位置&#xff1a; 使用一个单调…

MATLAB实现PID参数自动整定

目录 1、项目说明 2、文件说明 1、项目说明 本项目旨在通过 MATLAB 语言实现 PID 参数的自动整定&#xff0c;并设计了一个直观易用的 GUI 界面。该系统特别适用于实验室环境下的 PID 参数自整定任务。整定的核心原则在于优化系统性能&#xff0c;使系统的衰减比尽可能接近理…

R-Adapter:零样本模型微调新突破,提升鲁棒性与泛化能力 | ECCV 2024

大规模图像-文本预训练模型实现了零样本分类&#xff0c;并在不同数据分布下提供了一致的准确性。然而&#xff0c;这些模型在下游任务中通常需要微调优化&#xff0c;这会降低对于超出分布范围的数据的泛化能力&#xff0c;并需要大量的计算资源。论文提出新颖的Robust Adapte…

linux————根据端口查找运行目录的三种方法

先查询端口找到进程 netstat -anlpt | grep 16443 | grep -v grep tcp 0 0 0.0.0.0:16443 0.0.0.0:* LISTEN 3710563/nginx: mast tcp 0 0 192.168.110.253:16443 192.168.110.22:64430 ESTABLISHED 3710580/n…

When can I expect multiple “choices“ in an OpenAI response?

题意&#xff1a;"什么时候我可以在 OpenAI 的回复中预期到多个“选择”&#xff1f;" 问题背景&#xff1a; For a simple OpenAI api call in python 对于一个在 python 中简单的 OpenAI API 调用 response openai.ChatCompletion.create(model"gpt-4&quo…

vue2+ueditor集成秀米编辑器

一、百度富文本编辑器 1.首先下载 百度富文本编辑器 下载地址:GitHub - fex-team/ueditor: rich text 富文本编辑器 2.把下载好的文件整理好 放在图片目录下 3. 安装插件vue-ueditor-wrap npm install vue-ueditor-wrap 4.在你所需要展示的页面 引入vue-ueditor-wrap 此时…

苹果电脑需要安装杀毒软件吗?探索Mac的安全世界!

在聊到电脑安全时&#xff0c;许多Mac用户都骄傲地声称&#xff1a;“我的Mac是不会中病毒的&#xff01;”确实&#xff0c;与Windows PC相比&#xff0c;Mac因其UNIX-based的操作系统构架&#xff0c;天生就更加安全。但这是否意味着Mac完全不需要杀毒软件呢&#xff1f;让我…

细胞因子系列

细胞因子是一类分泌性蛋白质&#xff0c;可以调控细胞之间的相互作用以及免疫反应等生物学过程。细胞因子在细胞信号传导、细胞生长、细胞分化和细胞凋亡等过程中起着重要作用。它们可以通过在细胞表面结合受体、介导细胞信号转导&#xff0c;从而调节免疫应答、细胞增殖和炎症…

Oracle RAC环境NBU异机恢复

一、故障原因 9月3日中午&#xff0c;主机运维人员误操作&#xff0c;对sydb数据库的ASM磁盘做了格式化&#xff0c;将三块共享磁盘做了分区&#xff0c;然后使用vgextend命令加入到本地文件系&#xff0c;造成sydb的两个实例异常&#xff0c;没有操作CRS磁盘&#xff0c;所以…

如何在IDEA的一个工程中创建多个项目?

在IDEA中&#xff0c;可以通过Module来创建新的工程。

仕考网:公务员考试资格审核时间是多久?

在国家公务员考试的报名过程中&#xff0c;考生完成信息填报并选定职位之后&#xff0c;资格审核结果将在提交后48小时内在报名信息页面公布。一般是从报名日次日上午8点开始&#xff0c;直至报名截止日次日18点结束。在此期间&#xff0c;考生可以通过国家公务员局的官方网站查…

8.分布式服务部署

文章目录 1.分布式服务部署1.1服务器个数1.2 ubuntu 的 MySQL 安装1.3对其他服务器授权1.4Java服务部署1.5常见问题 大家好&#xff0c;我是晓星航。今天为大家带来的是 分布式服务部署 相关的讲解&#xff01;&#x1f600; 1.分布式服务部署 1.1服务器个数 机器个数 1 - N…

Unity实战之滑动列表居中的放大其他的缩小功能

前言 最近项目需要做一个关卡选择列表,滑动到屏幕中间,代表该关卡被选中,展示选中状态。 预期效果 思路 使用3个父节点(左中右)用于挂载关卡item,通过改变关卡item的父节点来改变item的层级。 创建ui 创建父节点 首先创建一个空节点,命名为levelGroup,代表该节点…