数据结构专题 -- 哈希思想详解

news2024/12/25 1:06:50

代码会存放在:

https://github.com/sjmshsh/Data-Struct-HandWriting

通过阅读本篇文章,你可以学到:

  • 哈希思想及其本质
  • 使用C++实现简易的哈希表
  • 哈希思想的应用
    • 位图
    • 布隆过滤器
    • 哈希切分
    • 极致升华,海量数据处理面试题
  • 拓展 – 一致性哈希算法
  • 用Golang实现简易的一致性哈希算法

哈希的本质我认为是以空间换取时间。牺牲空间以换取时间复杂度为O(1),哈希的用途很广泛,例如Redis,C++的STL,Java的集合,Go的map等等都用到了哈希,他们的原理虽然略有区别,但是基本上相同,所以我们只要理解了这个思想,就可以以分钟为单位的去学习其他地方的哈希。

哈希概念

其实增删查改(CRUD)一直是一个很大的话题,为何要增删查改,增删查改本质上是一种管理方式,就好比操作系统中,操作系统是一个管理员,管理员需要先描述再组织,描述就是把相关的数据抽象成一个结构体,或者说类对象。组织就是对这些结构体或者类对象使用某种方式管理起来。数据的管理方式有很多,常见的数据结构有:链表,栈,堆,红黑树,AVL树,B/B+树等等,当然,还有我们的哈希。

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决

于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数hashFunc使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

所以哈希的本质其实是简历映射关系的过程,映射的建立是需要花费空间的,但是它带来的O(1)的时间复杂度。

插入元素

根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。

搜索元素

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。

现在可能还是很迷,但是举一个例子就好了。

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

在这里插入图片描述

好,那么我们现在再次插入44这个元素,可以发现,我的位置已经被占了,这就是所谓的哈希冲突问题。

哈希冲突

对不同的关键字可能得到同一散列地址,即[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2mcgJXBA-1673943600773)(null)],而{[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ChcppzIi-1673943601177)(null)],这种现象称为冲突(英语:Collision)。

– 来自《维基百科》

  1. 哈希冲突是否可以避免?

    答:哈希冲突是不可能避免的,只能尽量的减小哈希冲突的概率,因为在算哈希值的时候,我们使用某种算法进行计算,难免会遇到计算的哈希值是相同的情况。

  2. 哈希冲突会带来什么影响?

    答:当冲突到达一定的程度的时候,哈希表的效率会显著的降低,具体原因后续再讲。

  3. 如何尽可能的规避哈希冲突?

    答:引起哈希冲突的一个很重要的原因就是:哈希函数的设计不合理。一个优秀的哈希函数应该满足以下几个条件:

    • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
    • 哈希函数计算出来的地址能均匀分布在整个空间中
    • 哈希函数应该比较简单,如果很复杂的话,那么计算成本也是很高的

因此我们这个时候来介绍一下哈希函数。

哈希函数

这里跳过,可以去网上查一下,数学问题。我们到时候模拟实现采用的是除留余数法。

除留余数法–(常用)

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址哈希函数设计的越精妙,产生哈希冲突的可能性就比较低,但是无法避免,那么我们如果真的遇到了哈希冲突,应该怎么解决呢?

哈希冲突解决

解决哈希冲突的两种常见方法是:闭散列开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那

么可以把key存放到冲突位置中的下一个空位置中去。那如何寻找下一个空位置呢?

线性探测

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位为止。

  • 插入

    • 通过哈希函数获取待插入元素在哈希表中的位置
    • 如何该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置插入新元素。

在这里插入图片描述

  • 删除

    采用闭散列处理哈希冲突时,不能随便物理删除哈希表中的已有元素,若直接删除元素会影响其他元素的搜索。例如删除了元素4,那么我的44其实就找不到了。因为我们应该使用伪删除法来删除一个元素。

    	enum Status
    	{
    		EXIST, // 位置已经有元素
    		EMPTY, // 位置为空
    		DELETE // 删除
    	};
    

线性探测的实现

先给出几个需要注意的点:

  • 注意我的取模不能模容量,要模大小

    	size_t i = kv.first % _tables.size();
    	// 不能取模capacity
    	// size_t i = kv.first % _tables.capacity();
    

    原因是如果你模容量的话可能会出现内存访问越界的问题。

  • 思考哈希表什么情况下进行扩容?如何扩容?

    首先回答一个问题,为什么要进行扩容?原因很简答

    1. 数据量太多了,哈希表放不下了
    2. 你可以想象一下,在同一片空间下,数据越来越多,是不是发生哈希冲突的概率会越来越大,那么再向一下线性探测的过程,如果哈希冲突很严重的话,就相当于遍历了,哈希表效率严重下降。

    那么什么时候进行扩容呢?

    1. 散列表的载荷因子定义为:α = 填入表中的元素个数 / 散列表的长。度[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传α是散列表装满程度的标志因子。由于表长是定值,[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传α的函数,只是不同处理冲突的方法有不同的函数。

      对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将resize散列表。

#pragma once
#include <vector>
#include <string>

using namespace std;


template<class K>
struct Hash
{
	size_t operator() (const K& key)
	{
		return key;
	}
};

// 如果K是stribg类型会走这个特化版本
template<>
struct Hash<string>
{
	size_t operator() (const string& s)
	{
		// BKDR
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 31;
			value += ch;
		}
		return value;
	}
};


namespace CloseHash
{
	enum Status
	{
		EXIST,
		EMPTY,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		Status _status = EMPTY;
	};

	template<class K, class V, class HashFunc = Hash<K>>
	class HashTable
	{
	public:
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret == nullptr)
			{
				// 没有找到说明没有这个数据,当然是不能删除的
				return false;
			}
			else
			{
				--_n;
				ret->_status = DELETE;
				return true;
			}
		}

		HashData<K, V>* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			HashFunc hf;
			size_t start = hf(key) % _tables.size();
			size_t i = 0;
			size_t index = start;
			// 线性探测 or 二次探测
			while (_tables[index]._status != EMPTY)
			{
				if (_tables[index]._kv.first == key && _tables[index]._status == EXIST)
				{
					return &_tables[index];
				}
				i++;
				index = start + i;
				index %= _tables.size();
			}
			return nullptr;
		}

		bool Insert(const pair<K, V>& kv)
		{
			HashData<K, V>* ret = Find(kv.first);
			if (ret)
			{
				return false;
			}
			// 负载因子到0.7
			// 负载因子越小,冲突概率就越小,效率越高,空间浪费越多
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				// 扩容
				size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				//vector<HashData<K, V>> newTables;
				//newTables.resize(newSize);
				 遍历原表,把原表中的数据重新按newSize映射到新表
				//for (size_t i = 0; i < _tables.size(); i++)
				//{
				//	// 
				//}
				 交换内存并把以前的内存销毁
				//_tables.swap(newTables);
				HashTable<K, V> newHT;
				newHT._tables.resize(newSize);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._status == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newHT._tables);
			}

			HashFunc hf;
			size_t start = hf(kv.first) % _tables.size();
			size_t i = 0;
			size_t index = start;
			// 不能取模capacity
			// size_t i = kv.first % _tables.capacity();

			// 线性探测
			while (_tables[index]._status == EXIST)
			{
				i++;
				index = start + i * i;
				index %= _tables.size();
			}
			_tables[index]._kv = kv;
			_tables[index]._status = EXIST;
			++_n;
			return true;
		}
	private:
		vector<HashData<K, V>> _tables;
		size_t _n; // 有效数据的个数
	};


	struct Date
	{
	public:
		int a = 1;
	};

	struct HashDate
	{
		size_t operator() (const Date* d)
		{
			// ...
		}
	};

	void TestHashTable1()
	{
		HashTable<string, int> ht;
		ht.Insert(make_pair("lxy", 12));
		cout << ht.Find("lxy") << endl;
		ht.Erase("lxy");
		cout << ht.Find("lxy") << endl;

		// 当key是一个定义类型时,需要配置这个仿函数,将key转换成整形
		HashTable<Date, string, HashDate> htds;
	}
}

线性探测其实缺点很大,一旦发生哈希冲突,所有的冲突连在一起,容易产生数据堆积。因此我们可以进行一次优化,使用二次探测

二次探测

线性探测是每次遇到重复的就找下一个位置,而二次探测就是遇到重复的话,就跳过一段距离,避免数据的堆积。

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就

是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: Hi = (H0 + i^2) % m。其中:i = 1,2,3…,是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小。如果要插入44,产生冲突,使用解决后的情况为:

在这里插入图片描述

但是二次探测也有很大的缺点,就是每次跳跃平方的话,在负载因子到达一定程度的时候,你可能跳几万次都跳不到空位,因此如果是二次探测的话要严格控制负载因子。

研究表明甚至要到达0.5。对空间的浪费是很大的。于是我们有了一个更好的解决方案,也就是开散列。

顺便放一个代码,我们只需要改一个地方就可以了,就是扩容的时候控制一下即可。

在这里插入图片描述

开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

在这里插入图片描述

这个位置我们可以称作是哈希桶,每一个哈希桶里面存放的是发生了哈希冲突的元素。

极端场景是一个哈希桶内冲突的元素太多了,链表太长,因此,当一个桶长度超过一定的值之后,由链表转换为红黑树,代码实现就链表吧,红黑树太难了。

关于开散列的负载因子,实际上是1,也就是每一次插入的时候都会发生哈希冲突的时候再扩容比较好。因为它不会像闭散列一样发生堆积。

扩容的代价是很大的,所以能少扩容,负载因子就尽量大一点,要做权衡。如果数据量大到离谱,例如一个亿,每次扩容都可能会造成一定的性能抖动

那么关于字符串放入哈希表,使用字符串哈希算法就可以了,在网上搜几个就可以。然后C++使用仿函数实现即可。

还有一个需要注意的点就是:研究表明,我们每次最好模一个素数,这样可以减小哈希冲突的概率

因此使用素数表的方法来实现,STL也是这么做的。

代码实现

namespace LinkHash
{
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next;

		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			,_next(nullptr)
		{}
	};

	template<class K, class V, class HashFunc = Hash<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		bool Erase(const K& key)
		{
			if (_tables.empty())
			{
				return false;
			}
			HashFunc hf;
			size_t index = hf(key) % _tables.size();
			Node* cur = _tables[index];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						// 头删
						_tables[index] = cur->_next;
					}
					else
					{
						// 中间的地方进行删除
						prev->_next = cur->_next;
					}
					--_n;
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
				}
			}
		}

		size_t GetNextPrime(size_t num)
		{
			static const unsigned long __stl_prime_list[28] = 
			{
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};
			for (size_t i = 0; i < 28; i++)
			{
				if (__stl_prime_list[i] > num)
				{
					return __stl_prime_list[i];
				}
			}
			return __stl_prime_list[27];
		}

		Node* Find(const K& key)
		{
			if (_tables.empty())
			{
				return nullptr;
			}
			HashFunc hf;
			size_t index = hf(key) % _tables.size();
			Node* cur = _tables[index];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				else
				{
					cur = cur->_next;
				}
			}
			return nullptr;
		}

		bool Insert(const pair<K, V>& kv)
		{
			Node* ret = Find(kv.first);
			if (ret)
			{
				return false;
			}
			// 负载因子 == 1 时扩容
			if (_n == _tables.size())
			{
				size_t newSize = GetNextPrime(_tables.size());
				vector<Node*> newTables;
				newTables.resize(newSize);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					HashFunc hf;
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t index = hf(cur->_kv.first) % newTables.size();
						// 头插
						cur->_next = newTables[index];
						cur = next;
					}
					_tables[i] = nullptr;
				}
				newTables.swap(_tables);
			}

			HashFunc hf;
			size_t index = hf(kv.first) % _tables.size();
			Node* newnode = new Node(kv);
			// 头插
			newnode->_next = _tables[index];
			_tables[index] = newnode;

			++_n;
			return true;
		}
	private:
		//struct Data
		//{
		//	forward_list<T> _list;
		//	set<T> _rbtree;
		//	size_t _len;
		//};
		// 这里就不考虑极端场景了
		vector<Node*> _tables;
		size_t _n; // 有效数据的个数
	};

	void TestHashTable()
	{
		int a[] = { 4, 24, 14, 7,37,37,57,67,34,14,54 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}
	}
}


哈希的应用

位图

海量数据处理面试题1

我们从一道面试题来引入位图:

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

中。【腾讯】

  1. 首先分析题目,我的脑子里面的第一想法是遍历,时间复杂度O(N),然后进行优化,先排序,时间复杂度O(NlogN),然后利用二分查找:logN。d
  2. 仔细一想,哎,40亿个数字,内存放不下哎,只能用其他的方式。

这里可以使用位图

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

#pragma once

#include <vector>

using namespace std;

namespace bit
{
	template<size_t N>
	class bit_set
	{
	public:
		bit_set()
		{
			_bits.resize(N / 8 + 1);
		}

		// 设置为1
		void set(size_t x)
		{
			// 这个i算的是它在第几个char里面
			size_t i = x / 8;
			// 这个j算的是它是第几个位
			size_t j = x % 8;
			_bits[i] |= (1 << j);
		}

		// 清理为0
		void reset(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8; 
			_bits[i] &= (~(1 << j));
		}

		// 探测这个位是否是1
		bool test(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;
			return _bits[i] & (1 << j);
		}
	private:
		vector<char> _bits;
		// vector<int> _bits;
	};

	void test_bit_set()
	{
		bit_set<0xffffffff> bs;
	}
}

海量数据处理面试题2

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

首先分析一下题目:

数字出现的频率可以分为三种情况:

  1. 出现0次
  2. 出现1次
  3. 出现2次以及以上

思路是:改造位图结构,以前是一个比特位标识一个值,现在改成两个比特位标识一个值。

定义两个比特位图:

bit_set<N> _bs1;
bit_set<N> _bs2;

这两个比特位图可以标识以下几种状态:

  • 00 数字出现了0次
  • 01 数字出现了1次
  • 10 数字出现了2次
  • 11 数字出现了3次

这样用两个比特位图就可以找到只出现一次的整数了。

#pragma once

#include <vector>

using namespace std;

namespace bit
{
	template<size_t N>
	class bit_set
	{
	public:
		bit_set()
		{
			_bits.resize(N / 8 + 1);
		}

		// 设置为1
		void set(size_t x)
		{
			// 这个i算的是它在第几个char里面
			size_t i = x / 8;
			// 这个j算的是它是第几个位
			size_t j = x % 8;
			_bits[i] |= (1 << j);
		}

		// 清理为0
		void reset(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8; 
			_bits[i] &= (~(1 << j));
		}

		// 探测这个位是否是1
		bool test(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;
			return _bits[i] & (1 << j);
		}
	private:
		vector<char> _bits;
		// vector<int> _bits;
	};

	template<size_t N>
	class TwoBitSet
	{
	public:
		void Set(size_t x)
		{
			if (!_bs1.test(x) && !_bs2.test(x)) // 00 -> 01
			{
				_bs2.set(x);
			}
			else if (!_bs1.test(x) && _bs2.test(x)) // 01 -> 10
			{
				_bs1.set(x);
				_bs2.reset(x);
			}
			// 11 表示以及出现2次或者以上,不用处理			
		}

		void PrintOnceNum()
		{
			for (size_t i = 0; i < N; i++)
			{
				if (!_bs1.test(i) && _bs2.test(i)) // 01
				{
					cout << i << endl;
				}
			}
		}
	private:
		// 然后设置的到bs1,一旦设置到bs1就已经说明完成了两次了
		bit_set<N> _bs1;
		// 第一次设置到bs2
		bit_set<N> _bs2;
	};

	void test_bit_set()
	{
		bit_set<0xffffffff> bs;
	}

	void TestTwoBitSet()
	{
		int a[] = { 99,0,4,50,33,44,2,5,99,0,50,99,50,2 };
		TwoBitSet<100> bs;
		for (auto e : a)
		{
			bs.Set(e);
		}
		bs.PrintOnceNum();
	}
}

海量数据处理面试题3

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

思路1:把一个文件中的整数set到一个比特位图,读取第二个文件中的整数判断在不在位图。在就是交际,不在就不是交集。

这个思路看起来可以,但是实际上有一个很大的缺点:

int a1[] = {5, 30, 1, 99, 10};
int a2[] = {8, 10, 11, 9, 30, 10, 30};

8不在。10在。11不在。9不在。30在。10在。30在。最终结果。

10 30 10 30

还要加入到set进行去重。可行,但是还是不完美。

思路2:把文件set到一个位图,然后同时再set到另外一个位图,然后把两个位图相与,与完是1的就是交集。

海量数据处理面试题4

1个文件有100亿个int,1G内存,设计算法找到出现次数不能超过2次的所有整数。

两个位图即可,跟第二题差不多。

总结一下

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

布隆过滤器

布隆过滤器的提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看

过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记

录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查

找呢?(Tips:推荐系统是可以有容错性的,所以这是一种解决方案)

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

特点

一句话解决:

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”

布隆过滤器是可以过滤字符串的,首先我们使用字符串哈希算法把字符串转换成一个整数。

然后把这个整数映射到比特位图里面,其实这就是布隆过滤器。

如果比特位是0的话,那么说明这个位置对应的字符串一定不存在,如果比特位是1的话,代码可能存在,因为我们前面已经了解到了,哈希冲突是无法避免的。

既然无法避免,我们就需要去尽可能规避。

  1. 首先选择优秀的字符串哈希函数
  2. 其次,使用多个字符串哈希函数,同时进行映射,减少冲突的概率。
  3. 开的空间尽可能提升,负载因子尽可能小

在这里插入图片描述

例如这里是3个哈希函数的例子。

插入

算3个哈希插入。

查找

如果我找到了有一个比特位为0,那么我可以百分百说没有这个字符串。如果我找到了3个比特位都是1。那么这个值因为哈希冲突的原因可能不存在,就是这么一个道理。

删除

img

img

不能支持删除工作,因为在删除一个元素的时候,可能会影响其他元素。

比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,

因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈

希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删

除操作。这相当于是引用计数吧。但是存在计数回绕问题。所以这个计数器取多少,是一个玄学问题。

代码实现

#pragma once


#include <bitset>
#include <string>
#include <time.h>

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

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;
	}
};

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

template<size_t N,
	size_t X = 8,// 这个X越大,空间分配越多,但是于此同时误判率是越低的
	class K = string,
	class HashFunc1 = BKDRHash,
	class HashFunc2 = APHash,
	class HashFunc3 = DJBHash>
	class BloomFilter
{
public:
	void Set(const K& key)
	{
		size_t len = X * N;
		size_t index1 = HashFunc1()(key) % len;
		size_t index2 = HashFunc2()(key) % len;
		size_t index3 = HashFunc3()(key) % len;
		/*	cout << index1 << endl;
			cout << index2 << endl;
			cout << index3 << endl<<endl;*/


		_bs.set(index1);
		_bs.set(index2);
		_bs.set(index3);
	}

	bool Test(const K& key)
	{
		size_t len = X * N;
		cout << len << endl;
		size_t index1 = HashFunc1()(key) % len;
		if (_bs.test(index1) == false)
			return false;

		size_t index2 = HashFunc2()(key) % len;
		if (_bs.test(index2) == false)
			return false;

		size_t index3 = HashFunc3()(key) % len;

		if (_bs.test(index3) == false)
			return false;

		return true;  // 存在误判的
	}

	// 不支持删除,删除可能会影响其他值。
	void Reset(const K& key);
private:
	bitset<X*N> _bs;
};

void TestBloomFilter1()
{
	BloomFilter<100> bits;
	bits.Set("我的名字是李鑫阳");
}

void TestBloomFilter2()
{
	/*BloomFilter<100> bf;
	bf.Set("张三");
	bf.Set("李四");
	bf.Set("牛魔王");
	bf.Set("红孩儿");
	bf.Set("eat");


	cout << bf.Test("张三") << endl;
	cout << bf.Test("李四") << endl;
	cout << bf.Test("牛魔王") << endl;
	cout << bf.Test("红孩儿") << endl;
	cout << bf.Test("孙悟空") << endl;
	cout << bf.Test("二郎神") << endl;
	cout << bf.Test("猪八戒") << endl;
	cout << bf.Test("ate") << endl;*/

	BloomFilter<100> bf;

	srand(time(0));
	size_t N = 100;
	std::vector<std::string> v1;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
		url += std::to_string(1234 + i);
		v1.push_back(url);
	}

	for (auto& str : v1)
	{
		bf.Set(str);
	}

	for (auto& str : v1)
	{
		cout << bf.Test(str) << endl;
	}
	cout << endl << endl;

	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
		url += std::to_string(6789 + i);
		v2.push_back(url);
	}

	size_t n2 = 0;
	for (auto& str : v2)
	{
		if (bf.Test(str))
		{
			++n2;
		}
	}
	cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;

	std::vector<std::string> v3;
	for (size_t i = 0; i < N; ++i)
	{
		// string url = "zhihu.com";
		//std::string url = "https://www.baidu.com/s?wd=ln2&rsv_spt=1&rsv_iqid=0xc1c7784f000040b1&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_dl=tb&rsv_enter=1&rsv_sug3=8&rsv_sug1=7&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=ln2&rsp=5&inputT=4576&rsv_sug4=5211";
		//std::string url = "https://zhidao.baidu.com/question/1945717405689377028.html?fr=iks&word=ln2&ie=gbk&dyTabStr=MCw0LDMsMiw2LDEsNSw3LDgsOQ==";
		std::string url = "https://www.cnblogs.com/-clq/archive/2012/01/31/2333247.html";
		url += std::to_string(rand());
		v3.push_back(url);
	}

	size_t n3 = 0;
	for (auto& str : v3)
	{
		if (bf.Test(str))
		{
			++n3;
		}
	}
	cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;

}

应用场景

  • 规避缓存击穿
  • 垃圾邮件判断
  • 。。。数据量大,节约空间,允许误判等场景

海量数据面试题5

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

近似算法使用布隆过滤器即可。但是存在误判。

如果要给出精确算法的话,那么我们就得使用哈希切分思想了。

哈希切分

首先分析一下内存,1个G内存,1个query大概10字节。100亿个query大概100G左右。那么我们切分成200个小文件即可。

我们把两个文件命名为A号文件和B号文件。

首先在A号文件中读取query,i = BKRDHash(query) % 200。这个quert就进入Ai号小文件。B同理。

我们有结论:**A和B中相同的query一定进入编号相同的小文件。**这是由哈希的性质决定的,因为哈希一样的query计算出来的i是相同的。

所以我们查找的时候只需要把Ai号和Bi号文件加载进入内存,然后查找就可以了,相同的query必定是相同的字符串里面。

海量数据面试题6

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

答:使用哈希切分,这样相同IP的一定进入相同文件,然后使用小根堆来以O(logN)来统计出现次数最多的IP地址。

步骤如下:

  • Hash分桶法:
  • 将100G文件分成1000份,将每个IP地址映射到相应文件中:file_id = hash(ip) % 1000
  • 在每个文件中分别求出最高频的IP,再合并 Hash分桶法:
  • 使用Hash分桶法把数据分发到不同文件
  • 各个文件分别统计top K
  • 最后Top K汇总
  • Linux命令,假设top 10:sort log_file | uniq -c | sort -nr k1,1 | head -10

拓展:一致性哈希算法

我写过博客,可以看一看:

https://blog.csdn.net/qq_61039408/article/details/128697332?spm=1001.2014.3001.5501

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

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

相关文章

二叉树的应用——哈夫曼树

哈夫曼树与哈夫曼编码 1.树的带权路径长 百分制成绩转五级制的算法流程图&#xff08;A/B/C/D/E的人数分别为6/18/21/36/19&#xff09;带权路径长 路经长 x 权重 树的带权路经长&#xff1a;所有叶结点的带权路径长度之和。 例如&#xff1a; &#xff08;a&#xff09;图…

[ 攻防演练演示篇 ] 利用谷歌 0day 漏洞上线靶机

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

橘子学kafka之基础命令使用

本系列主要开始处理关于kafka的一些技术知识点&#xff0c;尽量会以代码和实际命令为主要表达形式来做表现。 本文主要是关于如何在客户端使用命令做一个描述&#xff0c;其实我本来不想写的&#xff0c;但是今天在公司有同事居然不会&#xff0c;所以我觉得还是描述一下。而且…

贪心算法合集

95 分糖果问题 思路非常简单&#xff0c;和题解一模一样&#xff1a; 用数组存每个人对应的糖果数量&#xff0c;初始为1 从左到右遍历&#xff0c;如果比左边的大&#xff0c;1再从右到左遍历&#xff0c;如果比右边的大&#xff0c;1 import java.util.*;public class Solu…

录屏大师电脑版推荐(一键录制声画同步的视频)

很多小伙伴使用电脑多年&#xff0c;却不知道电脑有录屏功能。想要对电脑屏幕进行录制&#xff0c;只需在电脑上安装一个录屏大师。那有没有录屏大师电脑版推荐呢&#xff1f;在试用了多款电脑录屏大师之后&#xff0c;小编今天给大家推荐一款可以一键录制声画同步视频的录屏大…

使用Python为二年级的学生批量生成数学题

文章目录一.使用Python为二年级的学生批量生成数学题1.1 背景二.解决思路及其代码三.排版及其打印四.本文源码一.使用Python为二年级的学生批量生成数学题 1.1 背景 我妹妹今年上二年级&#xff0c;她的老师今天给他们布置了一项作业&#xff1a; 从今天起到开学&#xff0c;…

ENSP的AR40问题解决

AR40以及其他相关问题都可以参考此方法&#xff0c;都已经可以正常使用 现在有enspAR40问题的同学有救了&#xff0c;ensp的AR40问题困扰了我很长时间&#xff0c;根据官方的问题解决文档没有解决&#xff0c;反正就是之前的所有方法都没有用&#xff0c;也不是都没有用&#x…

初读《编程之美》就想秀一下,结果还翻车了

文章目录 一、前言 二、我的思路 三、Code 四、翻车现场 五、后续问题 一、前言 ———如何写一个短小的程序&#xff0c;让 Windows 的任务管理器显示CPU的占用率为50%? 这道有趣的面试题我是这两天从《编程之美》电子版中看到的&#xff0c;看意思就是邹老师在微软对一…

入门postgre sql(PG的下载和安装,包括普通用户源码构建的安装方式)

目录PG的下载安装1、Windows 上安装2、Linux上安装有root权限的安装无root权限的安装PG的下载安装 点击这里&#xff0c;了解pg 1、Windows 上安装 (1)下载安装 访问官网下载地址 https://www.enterprisedb.com/downloads/postgres-postgresql-downloads 下载最新发布的Po…

3.kafka-3.生产者,消费者

文章目录1.个性化配置&#xff0c;增加吞吐量2.发送事务消息3.消费组手动提交offset指定offset位置进行消费指定时间消费当新增消费者&#xff0c;或者消费组时&#xff0c;如何消费漏消息和重复消息如何解决消费解压问题1.个性化配置&#xff0c;增加吞吐量 private static vo…

使用 .NET 7、Blazor 和 .NET MAUI 构建你自己的 Podcast App

.NET Podcast App 首次在 .NET Conf 2021上推出&#xff0c;最近进行了更新以在 .NET Conf 2022 keynote 中突出显示 .NET 7 中的新功能。该 Podcast App 已准备好使用展示 .NET&#xff0c;ASP.NET Core&#xff0c;Blazor&#xff0c;.NET MAUI&#xff0c;Azure Container A…

Android 蓝牙开发——概述(一)

一、蓝牙简介 蓝牙技术是一种无线数据和语音通信开放的全球规范&#xff0c;它是基于低成本的近距离无线连接&#xff0c;为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。 其中将1.x~3.0之间的版本称之为经典蓝牙&#xff0c;4.x开始的蓝牙称之为低功耗蓝牙&…

Memcache学习总结

这里写自定义目录标题介绍一致性哈希寻找节点一致性哈希介绍内存管理slab结构寻找存储chunkChunk中存储的Item数据结构grow factor 调优回收删除一些特性介绍 基于内置内存Key-Value形式存储数据(字符串、对象)集群服务器是通过数组链表方式存储K-V数据<分布式>基于哈希…

编程语言那么多,我为什么推荐你学Java?

Java一直都是稳居排行榜第一的语言&#xff0c;在未来10年Java都会是最热门的语言之一&#xff0c;因为Java技术具有卓越的通用性、高效性、安全性和平台移植性&#xff0c;它可以跨平台的应用到不同的领域&#xff0c;工作需求足够大。 为什么选择学习Java编程语言&#xff1…

更具科技感的中塔机箱,模块设计兼容性强,鑫谷昆仑御风机箱上手

大家装机的时候应该都接触过鑫谷的机箱和散热器外设&#xff0c;作为一家有年头的外设品牌&#xff0c;这两年鑫谷推陈出新&#xff0c;像是在电源方面&#xff0c;就有不少很受欢迎的产品&#xff0c;像是昆仑系列等&#xff0c;前端鑫谷在昆仑系列中带来了一款设计新颖的机箱…

琥珀酰亚胺-双硫键-琥珀酰亚胺NHS-SS-NHS双端活性酯二硫键交联剂

名称:NHS-SS-NHS 中文名称:活性酯-双硫键-活性酯 琥珀酰亚胺-双硫键-琥珀酰亚胺 分子式 :C14H16N2O10S2 分子量 :436.41 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 用 途&#xff1a;仅供科研实验使用&#xff0c;不用于诊治 外观: 固体或粘性液体&am…

VMwareWorkstationPro16的下载与安装,以及vm账号注册的问题

VMwareWorkstationPro16的下载与安装&#xff0c;以及vm账号注册的问题查看虚拟化支持是否开启vm的安装vm账号注册的常见问题VM 16的安装步骤查看虚拟化支持是否开启 可以从任务管理器中的性能去查看CPU是否开启虚拟化支持 vm的安装 访问 vm 的官网: https://www.vmware.co…

I2C_Adapter驱动框架讲解与编写

I2C_Adapter驱动框架讲解与编写 文章目录I2C_Adapter驱动框架讲解与编写参考资料&#xff1a;一、 回顾1.1 2C驱动程序的层次1.2 I2C总线-设备-驱动模型二、 I2C_Adapter驱动框架2.1 核心的结构体1. i2c_adapter2. i2c_algorithm2.2 驱动程序框架1. 所涉及的函数2. i2c_algorit…

lq-递归

1、递归实现指数型枚举从 1∼n 这 n个整数中随机选取任意多个&#xff0c;输出所有可能的选择方案。输入格式输入一个整数 n。输出格式每行输出一种方案。同一行内的数必须升序排列&#xff0c;相邻两个数用恰好 1个空格隔开。对于没有选任何数的方案&#xff0c;输出空行。本题…

AStar(A*)算法核心思想( for unity)

AStar算法算法思想举例理解核心代码A* 算法&#xff0c;A* (A-Star)算法是一种静态路网中求解最短路径最有效的直接搜索方法&#xff0c;也是解决许多搜索问题的有效算法。算法中的距离估算值与实际值越接近&#xff0c;最终搜索速度越快。 注意:AStar的类应该作为一种单例类只…