哈希的应用

1. 位图

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

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

1.1 位图的模拟实现

#pragma once 
#include<iostream>
#include<vector>
using namespace std;
namespace wzy
{
	//使用了一个非类型的模板参数,这个N表示常数,表示有多少位
	template<size_t N>
	class bitset
	{
	public:

		bitset()
		{
			//_bits.resize(N / 8 + 1,0);
			_bits.resize((N >> 3)+ 1, 0); //因为+的优先级更高,所以需要加()
		}
		bitset(size_t n)
		{
			_bits.resize((n >> 3) + 1, 0);
		}
		//将第x个位  置为1
		void set(size_t x)
		{
			size_t index = x >> 3; //vector当中的第几个char类型
			size_t num = x % 8;//char类型当中的第几个bit位
			_bits[index] |= (1 << num); //因为按位或只影响这一个bit位
		}
		//将第x个位  置为0
		void reset(size_t x)
		{
			size_t index = x >> 3;
			size_t num = x % 8;
			_bits[index] &= (~(1 << num));
		}

		//如果是1返回真,如果是0返回假
		bool test(size_t x)
		{
			size_t index = x >> 3;
			size_t num = x % 8;
			return _bits[index] & (1 << num); //全零返回假  有一个位非零就返回真
		}
	private:
		std::vector<char> _bits; //对于vector里面每一个存放的都是一个char类型,相当于8个位,可以映射8个数
	};

	//对于多个同名的命名空间编译器会自己把他们合并在一起
	void testbitset()
	{
		bitset<100> bs;
		bs.set(10);
		bs.set(17);
		bs.set(80);

		bs.reset(80);
		bs.set(81);
		
		cout << bs.test(10) << endl;
		cout << bs.test(17) << endl;
		cout << bs.test(80) << endl;
		cout << bs.test(81) << endl;

	}
}

在这里插入图片描述

1.2 位图的应用

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

2 布隆过滤器

2.1 布隆过滤器概念

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

2.2 布隆过滤器的插入

布隆过滤器中插入:“baidu”
在这里插入图片描述
在这里插入图片描述

注意布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。(使用布隆过滤器的情况就是允许误差的出现。)

2.3 布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中

bool Test(const K& key)
	{
		HashFunc1 hf1;
		size_t i1 = hf1(key) % _n;
		//如果这个位置等于1则表示不一定存在,但是如果只要映射的多个位置有一个位置为0就表示一定不在
		if (!_bs.test(i1))
		{
			return false;
		}

		HashFunc2 hf2;
		size_t i2 = hf2(key) % _n;
		if (!_bs.test(i2))
		{
			return false;
		}

		HashFunc3 hf3;
		size_t i3 = hf3(key) % _n;
		if (!_bs.test(i3))
		{
			return false;
		}
		return true;
	}

注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判

  • 比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

2.4 布隆过滤器的删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素

  • 比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。(引用计数的方式,但是这就大打折扣的消减了位图节省空间的优势

缺陷

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

2.5 布隆过滤器的优缺点

优点

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

缺点

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题
  • 位图存储标识整形,效率高,节省空间
  • 布隆过滤器存储标识任意类型(字符串最常见),效率高,节省空间,缺点就是可能存在误判 ,不一定完全准确

2.6 布隆过滤器的模拟实现

#include"BitSet.h"
#include<string>
using namespace std;
//布隆过滤器可以存储标识任意类型


//这里最主要的就是字符串
//但是这里是有问题的,因为模板的缺省参数也必须从右往左给,所以这里是错的
//template<class K = string,
//class HashFunc1,
//class HashFunc2, 
//class HashFunc3>   //映射的位越多,那么也就越消耗空间,节省空间这一项就大打折扣了

//这里要是用不同的字符串哈希算法来算不同的映射位置
//第一种使用KBDR字符串哈希算法
struct StrHash1
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash = hash * 131 + ch;
		}
		return hash;
	}
};

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

struct StrHash3
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		size_t magic = 63689;
		for (auto ch :s)
		{
			hash = hash * magic + ch;
			magic *= 378551;
		}
		return hash;
	}
};


template<size_t N,class K = string,
class HashFunc1 = StrHash1,
class HashFunc2 = StrHash2, 
class HashFunc3 = StrHash3> 
class BloomFilter
{
public:
	BloomFilter()
		:_bs(N * 4) //这里其实有一个推导出来的公式。映射的个数和底层位图所要开的空间大小之间存在一个4倍的关系
		,_n(N * 4)
	{}
	void Set(const K& key)
	{
		HashFunc1 hf1;
		size_t i1 = hf1(key) % _n;
		_bs.set(i1);

		HashFunc2 hf2;
		size_t i2 = hf2(key) % _n;
		_bs.set(i2);

		HashFunc3 hf3;
		size_t i3 = hf3(key) % _n;
		_bs.set(i3);
		//cout << i1 << "--" << i2 << "--" << i3 << endl; //打印一下冲突的分布
	}

	//一般不支持删除,删除有可能存在把其他值给删掉的情况()因为别人也可能会映射这个位置
	void Reset(const K& key)
	{}

	bool Test(const K& key)
	{
		HashFunc1 hf1;
		size_t i1 = hf1(key) % _n;
		//如果这个位置等于1则表示不一定存在,但是如果只要映射的多个位置有一个位置为0就表示一定不在
		if (!_bs.test(i1))
		{
			return false;
		}

		HashFunc2 hf2;
		size_t i2 = hf2(key) % _n;
		if (!_bs.test(i2))
		{
			return false;
		}

		HashFunc3 hf3;
		size_t i3 = hf3(key) % _n;
		if (!_bs.test(i3))
		{
			return false;
		}
		return true;
	}
private:
	wzy::bitset<N> _bs;
	size_t _n; //底层所开位图的空间大小
};

void TestBloomFilter()
{
	//那么一开始的布隆过滤器应该开多大的位图呢?
	//存N个值,那么底层位图所开辟的空间应该是4*N,但是仿函数所算出来的有可能会超过4*N,所以还需要一个来保存空间的大小
	BloomFilter<10> bf;
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965978"); 
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965974");
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965975");
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965957");
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965944");
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965945");
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965946");
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965947");
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965948");
	bf.Set("https://blog.csdn.net/MEANSWER/article/details/117965949");

	//应该都存在 输出为1
	cout << bf.Test("https://blog.csdn.net/MEANSWER/article/details/117965978") << endl;
	cout << bf.Test("https://blog.csdn.net/MEANSWER/article/details/117965974") << endl;
	cout << bf.Test("https://blog.csdn.net/MEANSWER/article/details/117965975") << endl;
	cout << bf.Test("https://blog.csdn.net/MEANSWER/article/details/117965957") << endl;
	cout << endl;
	应该不存在,输出为0
	cout << bf.Test("https://blog.csdn.net/MEANSWER/article/details/117965988") << endl;
	cout << bf.Test("https://blog.csdn.net/MEANSWER/article/details/117965999") << endl;
}

在这里插入图片描述

3. 海量数据处理

1 布隆过滤器

  1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
    精确算法:①估算给的数据大概占用多少空间,假设一个query平均是10byte,100亿个query大概占用100G②割分:使用哈希割分(不能是平均割分)
  • 100Gquery的文件,创建200个小文件A0、A1、…A199,每个文件的大小为500M左右,依次获取文件中query,i = Hash(query) % 200,每个query进入算出的Ai文件,然后B文件也采用同样的方式得到200个小文件B0、B1、…B199,然后把Ai文件分别使用布隆过滤器的方式进行映射,然后Bi文件对应的去查找,然后将这200个小文件的所有交集相加就是两个文件精确的集合了。采用哈希切割的好处就是能够让A和B文件中相同query一定进入编号相同的Ai和Bi小文件,需要按编号比即可,时间复杂度为O(N),这样效率高如果采用平均切分方法也是可以的,但是跟Ai小文件的交集,可能在【B0,B199】都需要比一遍,时间复杂度为O(N*N)

    近似算法:BloomFilter(但是交集中可能存在不是交集的内容)

  1. 如何扩展BloomFilter使得它支持删除元素的操作(使用引用计数的方式,但是这种方式需要多个bit位来存储当前有映射这个位置的个数,占用的空间多了,不能很好发挥布隆过滤器节省空间的优势)

2 哈希切割

  1. 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?
  • i = hashstr(ip) % 100,ip转换为整形,模100,得到i是多少,这个ip就进入第i个小文件(但是这个不是平均切割,有可能某个文件大于1G,也有可能某个文件小于1G),可以采用map或者unordered_map对每个文件中的ip进行统计次数,然后在拿每个文件中所统计次数最多的进行比较,就可以得到ip出现次数最多的ip。
  • 可以使用优先级队列内部数据结构使用小堆,建立一个降序的队列,那么前K个就是要找的ip地址。
  • 平均切割有可能同一个ip地址存在在任意一个小文件中,但是使用哈希切割相同的ip地址一定会进入相同的文件,这也是解这类题的关键

3 位图应用

  1. 给定100亿个整数,设计算法找到只出现一次的整数?(因为整数只有42亿多个,所以肯定有重复的)
  • 解法一:哈希切割(万能解法),大概是40G大小,切出来50个小文件,然后在对每一个小文件进行整数的统计,找到所有里面只出现一次的,然后再将他们加起来。
  • 解法二位图及其变形:对解法一进行优化,使用两个位图,且两个位图相对应的两个bit位来共同表示一个数存在的状态,00表示0次,01表示一次,10表示2次及以上出现次数,然后找所有01状态的,就是我们要找的只出现一次的整数,空间和时间上的效率都比哈希切割更高。
  1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
  • 位图:首先想清楚,整形只有42亿多个,所以这里面的整形肯定有大量重复的,只需要大概512M就能够映射下42亿多个整形。假设两个文件分别叫做A、B,把文件A中的整形都设置到一个位图bit_set中,读取文件B的每一个整形,判断在不在A中,在就是交集。
  • 假设两个文件分别叫做A、B,把文件A和B中的整形都分别映射到一个位图bit_set中,然后让A的位图按位“与”上B的位图,A中bit位设置为1的,就是交集。
  1. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
  • 这里可以用第一题的解法二,用两个位图,但是此时应该存在4中状态,00表示0次,01表示1次,10表示2次,11表示3次及以上,然后找所有01和10状态的,就是我们要找的出现不超过2次的整数,空间和时间上的效率都比哈希切割更高。(当然哈希切割也是可以的)

扩展知识

  1. 用布隆过滤器来当做一个前置过滤层(还可以应用到过滤垃圾文件)
    对于一个学校来说,学生有很多,那么这些学生的信息都保存在数据库中,但是访问数据库就相当于去访问磁盘,效率是很低的。现在来了一个学生,我想让你判断一下是否是这个学校的学生?可以在数据库前面加一个布隆过滤器,就是拿着全校学生的唯一标识符(身份证号码、手机号码、学号等其中的一个,通过多个hash函数进行映射),来了一个学生我先进行布隆过滤器的查找,如果映射的位置的bit位都是1,说明可能是存在的(因为可能发生了哈希冲突,即使概率已经很低了),让他再去数据库中查找,是否有这个人。但是如果所映射的bit位并不是都是1,说明学校的学生中一定没有这个人,也就避免了再去数据库查找的过程。
  2. 分布式
    对于一个大型的公司来说,是要有很多个数据进行存储,如果把数据都存储在一台机器上,显然是不安全的,万一这台主机出现问题呢?所以需要把数据存储在很多台机器上,然后把多台机器很好的管理起来。此时用户来了一个数据,拿着这个客户的唯一标识符(微信号、手机号)先转换为整数,然后在通过hash函数找到对应的主机,但是万一此时坏掉一台,那么hash函数的capacity不就出现了问题,那么就全乱了,如果是在增加5000台机器,那么在查找数据的时候也就乱了,所以这里又引入了一个一致性哈希

转载:
一致性哈希详解文章

简单理解一致性哈希:上面所说的当10000台机器突然坏掉一台或者想要增加5000台的时候,查找数据就会出现很大的问题。此时一致性哈希不再使用模机器的台数,而是模一个固定的值i= hash(key) % 2^32 , 得到的结果就是就是0-2^32-1的范围,假设此时算出的结果在0-10000的就映射到0号机器,10000-20000就映射1号机器,也就把 0-2^32-1根据机器台数划分为了10000份,映射的机器也就在0-9999号之间,那么如果此时我要更加5000台机器,那么首先我找到这10000台机器中数据比较多的5000台,然后拿出比如0号机器的0-5000范围的数据映射到10000号机器上,这样就可以缓解扩容带来的问题。

哈希和MD5加密算法还有关系

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值