【ONE·C++ || 哈希(二)】

news2024/11/20 11:27:17

总言

  主要介绍哈希运用于unordered系列的上层封装框架与相关运用:位图、布隆过滤器、哈希切割。

文章目录

  • 总言
  • 0、思维导图
  • 3、封装
    • 3.1、基础封装
      • 3.1.1、框架结构
      • 3.1.2、Inset 1.0
    • 3.2、引入迭代器
      • 3.2.1、在迭代器中
      • 3.2.2、在哈希表中
      • 3.2.3、在unordered上层
      • 3.2.4、Insert 2.0 、operator[]
      • 3.2.5、验证
    • 3.3、整体
      • 3.3.1、HashTable
      • 3.3.2、unordered_set
      • 3.3.3、unordered_map
  • 4、应用
    • 4.1、位图
      • 4.1.1、基本概念介绍
      • 4.1.2、常用接口相关实现
        • 4.1.2.1、如何构造一个位图?
        • 4.1.2.2、set、reset、test
        • 4.1.2.3、相关验证
      • 4.1.3、扩展运用·海量数据处理
        • 4.1.3.1、题一
        • 4.1.3.2、题二
        • 4.1.3.3、题三
    • 4.2、布隆过滤器
      • 4.2.1、基本概念介绍
      • 4.2.2、相关接口实现
        • 4.2.2.1、布隆过滤器的基本框架及哈希函数
        • 4.2.2.2、set、test
        • 4.2.2.3、相关验证
      • 4.2.3、扩展运用:海量数据处理
        • 4.2.3.1、问题一:布隆过滤器删除
        • 4.2.3.2、问题二:哈希切割

  
  
  
  

0、思维导图

在这里插入图片描述

3、封装

3.1、基础封装

3.1.1、框架结构

  类似于红黑树与map、set,我们同样使用一个哈希表,设置不同传入参数,实例化出unordered_map、unordered_set。因此需要修改HashTable的第二参数,同时新增一个模板参数,用于unordered_map、unordered_set的传参。
  
  1)、在哈希表中

	template<class T>
	struct HashNode
	{
		T _data;//当前结点存储值
		HashNode<T>* _next;//指向下一个结点的指针

		HashNode(const T& data)//构造
			:_data(data)
			,_next(nullptr)
		{}
	};
	template<class K, class T, class Hash, class KeyOfT>
	class HashTable
	{
		typedef HashNode<T> Node;
	public:
		//……
		//其它函数
	private:
		vector<Node*> _table;
		size_t _size = 0;
	};

  
  
  2)、在unordered上层

namespace myunorderedset
{
	template<class K,class Hash=HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

	private:
		HashBucket::HashTable<K, K, Hash, SetKeyOfT> _ht;
	};
}
namespace myunorderedmap
{
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K,V>& kv)
			{
				return kv.first;
			}
		};
		
	//……
	//其它实现

	private:
		HashBucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
	};
}

  
  
  

3.1.2、Inset 1.0

  1)、在哈希表中
  Insert、Erase、Find中需要改动如下:实际只增加KeyOfT部分。

				Hash HsTrans;
				KeyOfT Kot;

  size_t hashi = HsTrans(Kot(data)) % _table.size();,Kot是为了根据unordered_map、unordered_set获取相应Key值,而HsTrans是为了解决取模问题。

		bool Insert(const T& data)
		{
			Hash HsTrans;
			KeyOfT Kot;

			if (Find(Kot(data)))//若此处使用Find函数,需要注意其参数类型,需要转换
				return false;

			//0、扩容检查
			CheckCapacity();

			//1、计算哈希地址
			size_t hashi = HsTrans(Kot(data)) % _table.size();

			//2、插入值,修改链接关系
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_size;
			return true;
		}

		Node* Find(const K& key)
		{
			if (_table.size() == 0)//表中无元素
				return nullptr;

			Hash HsTrans;
			KeyOfT Kot;
			size_t hashi = HsTrans(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (Kot(cur->_data) == key)
					return cur;
				cur = cur->_next;
			}
			return nullptr;//找不到的情况
		}

		bool Erase(const K& key)
		{

			Node* ret = Find(key);
			if (ret)//该目标值存在
			{
				Hash HsTrans;
				size_t hashi = HsTrans(key) % _table.size();
				Node* prev = nullptr;
				Node* cur = _table[hashi];
				while (cur != ret)
				{
					prev = cur;
					cur = cur->_next;
				}
				if (prev)//非头删
					prev->_next = cur->_next;
				else//头删
					_table[hashi] = cur->_next;

				delete cur;
				--_size;

				return true;
			}
			return false;
		}

  
  
  2)、在unordered上层

		bool insert(const K& key)
		{
			return _ht.Insert(key);
		}


		bool insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

  事实上后续还需要对其返回值做修改。
  
  
  

3.2、引入迭代器

3.2.1、在迭代器中

  1)、基本框架搭建
  说明:哈希表中,迭代器的遍历需要依次遍历每个哈希桶,又在每个哈希桶中依次遍历其结点,因此我们可以为迭代器设置两个成员,分别对应单个结点以及整个哈希表。需要注意的是,哈希表的迭代器是单向迭代器,只支持operator++、不支持operator--

	//前置声明
	template<class K, class T, class Hash, class KeyOfT>
	class HashTable;

	template<class K,class T,class Hash,class KeyOfT>
	struct __Hashiterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K, T, Hash, KeyOfT> HT;

		Node* _node;//指向哈希桶中单个结点的指针
		HT* _pht;//指向哈希表的指针

		__Hashiterator(Node* node, HT* pht)//构造
			:_node(node)
			,_pht(pht)
		{}
	};

  
  
  2)、operator*、operator->

		T& operator*()
		{
			return _node->_data;//返回对应节点的存储值
		}


		T* operator->()
		{
			return &(_node->_data);//返回对应节点的存储值的地址
		}

  
  
  
  3)、operator==、operator!=

		bool operator==(const Iterator& it) const
		{
			return it._node == _node;
		}

		bool operator!=(const Iterator& it) const
		{
			return it._node != _node;
		}

  
  
  4)、operator++

		Iterator& operator++()
		{
			
			if (_node->_next) // 当前桶中节点迭代
			{
				_node = _node->_next;
			}
			else //需要寻找哈希表中下一个桶
			{
				Hash HsTrans;
				KeyOfT Kot;
				size_t hashi = HsTrans(Kot(_node->_data)) % _pht->_table.size();
				++hashi;//到下一个哈希桶位置
				for (; hashi < _pht->_table.size(); ++hashi)
				{
					if (_pht->_table[hashi])//当前迭代后的哈希桶中存在有效值
					{
						_node = _pht->_table[hashi];
						break;
					}
					//如果当前++hashi后获取到的下一个桶不存储有效值,则再次往后迭代寻找哈希桶
				}

				if (hashi == _pht->_table.size())//若将哈希表都遍历完成,则说明已经没有存储有效数据的哈希桶了
				{
					_node = nullptr;
				}
			}
			return *this;
		}

  
  
  
  
  

3.2.2、在哈希表中

  1)、基本说明
  1、begin返回的是哈希桶中首个有效节点,因此需要遍历哈希表,找存储有效节点的哈希桶。end返回最后一个节点的下一个位置,因此我们使用一个空指针即可。
  2、关于迭代器的构造,根据其参数Node* node, HT* pht,我们要传入节点指针以及哈希表指针,这里指向哈希表的指针我们可以使用this指针。

		iterator begin()
		{
			for (size_t i = 0; i < _table.size(); ++i)
			{
				if (_talbe[i])
					return iterator(_table[i], this);
			}
			return end();
		}

		iteraotr end()
		{
			return iteraotr(nullptr, this);
		}

  
  

3.2.3、在unordered上层

  在set中:

		typedef typename HashBucket::HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

  在map中:

		typedef typename HashBucket::HashTable<K, pair<K,V>, Hash, MapKeyOfT>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

  
  
  

3.2.4、Insert 2.0 、operator[]

  1)、对Insert、Find等返回值做修改

		pair<iterator,bool> Insert(const T& data)
		{
			Hash HsTrans;
			KeyOfT Kot;

			//去重
			iterator ret = Find(Kot(data));
			if (ret != end())
				return make_pair(ret, false);

			//0、扩容检查
			CheckCapacity();

			//1、计算哈希地址
			size_t hashi = HsTrans(Kot(data)) % _table.size();

			//2、插入值,修改链接关系
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_size;
			return make_pair(iterator(newnode, this), true);
		}

		iterator Find(const K& key)
		{
			if (_table.size() == 0)//表中无元素
				return end();

			Hash HsTrans;
			KeyOfT Kot;
			size_t hashi = HsTrans(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (Kot(cur->_data) == key)
					return iterator(cur,this);
				cur = cur->_next;
			}
			return end();//找不到的情况
		}

  
  在unordered_set中:

		pair<iterator, bool> insert(const K& key)
		{
			return _ht.Insert(key);
		}

  在unordered_map中:

		pair<iterator,bool> insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

  
  
  2)、unordered_map::operator[]

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

  
  

3.2.5、验证

  1)、演示一

void test06()
{
	myunorderedset::unordered_set<int> set;
	set.insert(3);
	set.insert(7);
	set.insert(9);
	set.insert(2);
	set.insert(5);
	myunorderedset::unordered_set<int>::iterator It = set.begin();
	while (It != set.end())
	{
		cout << *It << " ";
		++It;
	}
	cout << endl;

	myunorderedmap::unordered_map<string, string> dict;
	dict.insert(make_pair("comprehend", "v.理解"));
	dict.insert(make_pair("model", "n.模型"));
	dict.insert(make_pair("poisonous", "adj.有毒的"));

	myunorderedmap::unordered_map<string, string>::iterator it = dict.begin();
	while (it != dict.end())
	{
		cout << it->first << " : " << it->second << endl;
		++it;
	}
	cout << endl;
}

在这里插入图片描述

  
  
  2)、演示二

void test07()
{

	//次数统计
	string arr[] = { "晴","多云","晴","阴","小雨","多云","多云","阴","晴","小雨","大雨","阴","多云","晴" };
	myunorderedmap::unordered_map<string, int>countMap;
	for (auto& str : arr)//直接借助范围for遍历
	{
		countMap[str]++;
	}

	//范围for
	for (const auto& kv : countMap)
	{
		cout << kv.first << ": " << kv.second << endl;
	}
}

在这里插入图片描述
  
  
  3)、小结
  一个类型K,去做set和unordered_set的模板参数,有什么要求?
  1、set:①要求K类型的对象能比较大小,或者显示提供能够比较的仿函数
  2、unordered_set:①要求K类型的对象可以转换为整形取模,或者提供转成整形的仿函数;②要求K类型的对象能支持等于==比较,或者显示提供能支持等于==比较的仿函数
  
  
  
  
  

3.3、整体

3.3.1、HashTable

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

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;//能够强制类型转换为size_t的。
	}
};

template<>
struct HashFunc<string>//方法二
{
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
		{
			val *= 131;
			val += ch;
		}
		return val;
	}
};

namespace HashBucket
{

	template<class T>
	struct HashNode
	{
		T _data;//当前结点存储值
		HashNode<T>* _next;//指向下一个结点的指针

		HashNode(const T& data)//构造
			:_data(data)
			,_next(nullptr)
		{}
	};
	

	//前置声明
	template<class K, class T, class Hash, class KeyOfT>
	class HashTable;

	template<class K,class T,class Hash,class KeyOfT>
	struct __HashIterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K, T, Hash, KeyOfT> HT;
		typedef __HashIterator<K, T, Hash, KeyOfT> Iterator;

		Node* _node;//指向哈希桶中单个结点的指针
		HT* _pht;//指向哈希表的指针

		__HashIterator(Node* node, HT* pht)//构造
			:_node(node)
			,_pht(pht)
		{}

		T& operator*()
		{
			return _node->_data;
		}


		T* operator->()
		{
			return &(_node->_data);
		}


		bool operator==(const Iterator& it) const
		{
			return it._node == _node;
		}

		bool operator!=(const Iterator& it) const
		{
			return it._node != _node;
		}

		Iterator& operator++()
		{
			
			if (_node->_next) // 当前桶中节点迭代
			{
				_node = _node->_next;
			}
			else //需要寻找哈希表中下一个桶
			{
				Hash HsTrans;
				KeyOfT Kot;
				size_t hashi = HsTrans(Kot(_node->_data)) % _pht->_table.size();
				++hashi;//到下一个哈希桶位置
				for (; hashi < _pht->_table.size(); ++hashi)
				{
					if (_pht->_table[hashi])//当前迭代后的哈希桶中存在有效值
					{
						_node = _pht->_table[hashi];
						break;
					}
					//如果当前++hashi后获取到的下一个桶不存储有效值,则再次往后迭代寻找哈希桶
				}

				if (hashi == _pht->_table.size())//若将哈希表都遍历完成,则说明已经没有存储有效数据的哈希桶了
				{
					_node = nullptr;
				}
			}
			return *this;
		}

	};


	template<class K, class T, class Hash, class KeyOfT>
	class HashTable
	{
		typedef HashNode<T> Node;

		template<class K, class T, class Hash, class KeyOfT>
		friend struct __HashIterator;

	public:

		typedef __HashIterator<K, T, Hash, KeyOfT> iterator;

		iterator begin()
		{
			for (size_t i = 0; i < _table.size(); ++i)
			{
				if (_table[i])
					return iterator(_table[i], this);
			}
			return end();
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); ++i)
			{
				Node* cur = _table[i];
				Node* next = nullptr;
				while (cur)
				{
					next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		}

		void CheckCapacity()
		{
			if (_size == _table.capacity())
			{

				vector<Node*> newtable;//新建一个哈希表

				newtable.resize(__stl_next_prime(_table.size()), nullptr);//resize重新设置容量空间

				Hash HsTrans;
				KeyOfT Kot;
				for (size_t i = 0; i < _table.size(); ++i)//遍历原表,在新表中重新建立映射关系:此处我们直接使用原表已经存在的结点,修改链接关系即可
				{
					Node* cur = _table[i];
					while (cur)//若当前桶中存在有效结点:依次取当前桶中每个结点,让其重新链接到新表中
					{
						Node* next = cur->_next;

						size_t hashi = HsTrans(Kot(cur->_data)) % newtable.size();//获取结点在新表中的哈希地址
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;

						cur = next;//在原哈希桶迭代依次修改节点,直到当前桶为空
					}
					_table[i] = nullptr;
				}

				//交换
				_table.swap(newtable);
			}
		}

		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __stl_prime_list[__stl_num_primes] =
			{
				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
			};

			//遍历上述的素数集合,设当前_table中元素个数为n,
			//则下次resize从新规定空间时,我们只需要找首个大于n的素数即可。
			for (size_t i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}

			
		pair<iterator,bool> Insert(const T& data)
		{
			Hash HsTrans;
			KeyOfT Kot;

			//去重
			iterator ret = Find(Kot(data));
			if (ret != end())
				return make_pair(ret, false);

			//0、扩容检查
			CheckCapacity();

			//1、计算哈希地址
			size_t hashi = HsTrans(Kot(data)) % _table.size();

			//2、插入值,修改链接关系
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_size;
			return make_pair(iterator(newnode, this), true);
		}

		iterator Find(const K& key)
		{
			if (_table.size() == 0)//表中无元素
				return end();

			Hash HsTrans;
			KeyOfT Kot;
			size_t hashi = HsTrans(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (Kot(cur->_data) == key)
					return iterator(cur,this);
				cur = cur->_next;
			}
			return end();//找不到的情况
		}

		bool Erase(const K& key)
		{

			Node* ret = Find(key);
			if (ret)//该目标值存在
			{
				Hash HsTrans;
				size_t hashi = HsTrans(key) % _table.size();
				Node* prev = nullptr;
				Node* cur = _table[hashi];
				while (cur != ret)
				{
					prev = cur;
					cur = cur->_next;
				}
				if (prev)//非头删
					prev->_next = cur->_next;
				else//头删
					_table[hashi] = cur->_next;

				delete cur;
				--_size;

				return true;
			}
			return false;
		}


		//统计哈希表中数据个数
		size_t Size()
		{
			return _size;
		}

		//统计哈希表中表的长度
		size_t TableSize()
		{
			return _table.size();
		}

		//统计哈希表中桶的个数
		size_t BucketNum()
		{
			size_t count = 0;
			for (size_t i = 0; i < _table.size(); ++i)
			{
				if (_table[i])
					count++;
			}
			return count;
		}

		//寻找最长桶的长度
		size_t MaxBucketLength()
		{
			size_t length = 0;
			for (size_t i = 0; i < _table.size(); ++i)
			{
				Node* cur = _table[i];
				size_t curlen = 0;
				while (cur)
				{
					++curlen;
					cur = cur->_next;
				}

				if (curlen > 0)//用于查看有数据的桶其上挂有多少个结点
					printf("[%d]号桶长度:%d\n", i, curlen);

				if (curlen > length)
					length = curlen;
			}
			return length;
		}


	private:
		vector<Node*> _table;
		size_t _size = 0;
	};
}

3.3.2、unordered_set

#pragma once

#include"HashTable_3.h"

namespace myunorderedset
{
	template<class K,class Hash=HashFunc<K>>
	class unordered_set
	{

		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

	public:

		typedef typename HashBucket::HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

		pair<iterator, bool> insert(const K& key)
		{
			return _ht.Insert(key);
		}

		

	private:
		HashBucket::HashTable<K, K, Hash, SetKeyOfT> _ht;
	};
}

3.3.3、unordered_map

#pragma once



#include"HashTable_3.h"

namespace myunorderedmap
{
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K,V>& kv)
			{
				return kv.first;
			}
		};

	public:

		typedef typename HashBucket::HashTable<K, pair<K,V>, Hash, MapKeyOfT>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

		pair<iterator,bool> insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

	private:
		HashBucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
	};

}

  
  
  
  
  

4、应用

4.1、位图

4.1.1、基本概念介绍

  1)、位图引入
  问题:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
  
  解决方案:
  1、使用搜索树或哈希表是否能解决?
  分析:首先我们先分析数据大小,若直接将40亿个无符号整型存储入内存中,需要多大空间?

1G = 1024MB = 1024 * 1024KB = 1024 * 1024 * 1024Byte; 
1G = 230Byte ≈ 1X10^9 (10亿) Byte

40亿无符号整形,设其一共40*4=160Byte,故需要的内存空间为16G。

  在内存空间有限的情况下,上述大小的数据无法存储下,更别说搜索树、哈希表的结构中除了存储基础数据,还有其它成员。因此上述方法并不适用于这类海量数据处理。
  
  2、使用外排序+二分查找是否能解决?
  分析:外排序将数据存储在磁盘上,可以解决内存中存放不下的问题,但在磁盘上使用二分查找相对来说效率慢,支持起来不方便。
  
  3、除了上述方法,是否还有其他解决办法?
  分析:实际上我们只需要判断出一个数是否存在集合中,不一定需要真实将该数据存储起来,有一个能标记其状态信息的方式即可。即我们将要介绍的位图。
  
  
  2)、位图介绍
  位图: 就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
  
  1、比特位映射标记值:上述问题中,我们需要判断某一数据是否在给定的整形数据中,结果只有在或者不在,刚好对应两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,比如,二进制比特位为1代表存在,0代表不存在。
  2、数据所需内存大小:在统计时,可以采用直接定址法,整形数据32位下范围为 0 0 0 ~ 2 32 2^{32} 232,那么我们最多只用开辟 2 32 2^{32} 232bit空间位置(原因:32位下整形数据不会超过这个范围),类似于哈希函数对给定的数据集合进行一一映射,则有:

2^32 = 4294967296 bit
4294967296÷8÷1024÷1024÷1024=0.5G

  即我们最多只用到0.5G的内存空间。
  
  PS:实际上SQL中也提供了位图:bitset
  
  
  3)、位图优缺点介绍
  优点:速度快、节约空间
  缺点:相对局限,只能映射整形数据
  
  

4.1.2、常用接口相关实现

4.1.2.1、如何构造一个位图?

	template<size_t N>
	class bitset
	{
		bitset()
		{
			_bits.resize(N / 8 + 1, 0));
		}

		//……

	private:
		vector<char> _bits;
	};

  细节说明:
  1、如何按位开辟空间?
  根据上述位图使用分析,我们是以bit位来判断数据是否存在的,最大只需要开辟 2 32 2^{32} 232bit位的空间。但实际内存中,没有以单位bit来衡量的类型大小,最小的类型单位char也要8bit。因此,对开辟的内存空间我们需要做一定处理:
在这里插入图片描述

  
  2、非类型模板参数size_t N,此处设置N是根据实际需求开辟对应大小的比特位。但根据1可知,实际vector类型为char,因此resize重新开辟空间时需要做一定转换,这里最后+ 1是因为通常情况下/属于向下取整,假设 0 < N < 8 0<N<8 0<N<8;则 N / 8 = 0 N/8=0 N/8=0。为了防止此类情况,可以牺牲一点内存资源,多开辟一个char大小的空间。

		bitset()
		{
			_bits.resize(N / 8 + 1, 0);//此处不能使用sizeof(char),因为其计算单位为字节
		}

  
  

4.1.2.2、set、reset、test

  这里我们只实现位图中相对重要的接口:

set:将映射的比特位设置为1
reset:将映射的比特位设置为0
test:检测位图中目标值是否存在

  
  
  1)、set:如何将某一比特位设置为1?
  step1:直接定址法获取给定数值对应哈希地址。 需要思考问题:由于开辟的vector每个数据空间是char类型大小,因此需要判断给定数据是在第几个char的第几个比特位上。
  方法:

/ 8 :获取给定数据是在第几个char
% 8 :获取给定数据是在该char中第几个比特位

在这里插入图片描述
  
  
  step2:将对应的bit位设置为存在状态(这里我们默认1为存在)。
  方法:需要用于位移操作符、按位或。
  
  
  step3:相关实现如下:

		void set(size_t val)
		{
			size_t i = val / 8;
			size_t j = val % 8;

			_bits[i] |=  (1 << j);//这里使用的是或等
		}

  
  
  
  2)、reset:如何将某一比特位恢复为0?
  有了上述基础,对reset的实现其思想类似。①找对应的比特位;②需要思考如何将对应比特位设置为0,且不改变其它比特位的值?

设左移3,则0000 0001<<30000 1000,按位取反得,1111 0111;
接下来按位与,则全为1才是1,即可保障其它位不变,0位必为0.

  相关实现如下:

		void reset(size_t val)
		{
			size_t i = val / 8;
			size_t j = val % 8;

			_bits[i] &= ~(1 << j);//与等
		}

  
  
  
  3)、test:如何判断某一比特位上的值?
  方法类似。

若原先该比特位为1,则按位与1后得1,表示位图中存在该数据;
若原先该比特位为0,则按位与1后得0,表示位图中不存在该数据;

  相关实现:

		bool test(size_t val)
		{
			size_t i = val / 8;
			size_t j = val % 8;

			return _bits[i] & (1 << j);
		}

  
  
  

4.1.2.3、相关验证

  1)、测试set、reset、test

void test08()
{
	mybitset::bitset<100> bs1;
	bs1.set(6);
	bs1.set(12);
	bs1.set(24);

	cout << bs1.test(6) << endl;
	cout << bs1.test(12) << endl;
	cout << bs1.test(24) << endl;

	bs1.reset(6);
	bs1.reset(12);
	bs1.reset(24);

	cout << bs1.test(6) << endl;
	cout << bs1.test(12) << endl;
	cout << bs1.test(24) << endl;
}

在这里插入图片描述

  
  
  
  
  2)、测试给定数值最大范围(2^32,32位下)空间
  假设我们需要开辟最大的bit位空间(32位下即 2 32 2^{32} 232),那么非类型模板参数size_t N 该如何传参?
  
  如下述,我们可以传递-1,也可以使用十六进制的0XFFFFFF。对于-1,由于N的类型位size_t,故会转换为无符号整形。

	bitset<-1> bs1;
	bitset<0xffffffff> bs2;

  之前我们计算得4294967296÷8÷1024÷1024÷1024=0.5G,可通过任务管理器观察是否开辟了0.5G的内存空间。
在这里插入图片描述

  
  
  
  

4.1.3、扩展运用·海量数据处理

4.1.3.1、题一

  1)、问题与分析
  问题: 给定100亿个整数,设计算法找到只出现一次的整数?
  分析:
  1、这也是海量数据处理问题,需要考虑到内存空间有限,限制了哈希、搜索树的使用。
  2、我们学习了位图,这里可以考虑到它的运用。在之前的模拟实现中实际类似于K模型,只要求判断一个值在或不在,此题中的场景类似于KV模型,要确定对应K值实际出现几次(V)。
  3、分析题目,我们可将其分类处理:出现0次、出现1次、出现2次及2次以上。由于一个比特位只能代表两种情况,因此,我们可以使用两个比特位(可表示四种情况)来表示上述分类。
在这里插入图片描述

  4、按照3的方式,我们需要重新调整和修改bitset的内部结构。且实际SQL中也有bitset,我们不可能直接对其修改,那就意味着要完完全全重新构建,可相比于直接重建,复用已有资源是更优选择。一种方式是,使用两个位图,利用位图中相同的比特位,表示上述情况分类。
在这里插入图片描述

  
  
  2)、相关实现与验证

	template<size_t N>
	class twobitset
	{
	public:
		//需求:给定的val值,设置其出现状况
		void set(size_t val)
		{
			bool ret1 = _bits1.test(val);
			bool ret2 = _bits2.test(val);

			if (ret1 == false && ret2 == false)
			{ //原先为00:此时val值进来,需要置为01,表示出现一次
				_bits2.set(val);
			}
			else if (ret1 == false && ret2 == true)
			{//原先为01:此时val值进来,需要设置为10,表示出现两次及两次以上
				_bits1.set(val);
				_bits2.reset(val);
				
			}
			
		}


		//用于检测某一值是否只出现一次,若是则将其打印出来
		void print_once_num()
		{
			for (size_t i = 0; i < N; ++i)
			{
				if (_bits1.test(i) == false && _bits2.test(i) == true)
				{
					cout << i << " ";
				}
			}
		}



	private:
		bitset<N> _bits1;
		bitset<N> _bits2;
	};

  
  代码验证:
在这里插入图片描述

void test10()
{
	vector<size_t> v;
	int n = 20;
	v.reserve(n);

	srand(time(0));
	for (int i = 0; i < n; ++i)
		v.push_back(rand() % 15 + 1);


	cout << "size:" << v.size() << endl;;
	for (const auto& e : v)
		cout << e << " ";
	cout << endl;

	mybitset::twobitset<20> bs1;
	for (const auto& e : v)
	{
		bs1.set(e);
	}
	cout << "只出现一次的数:";
	bs1.print_once_num();
	cout << endl;

}

  
  
  

4.1.3.2、题二

  1)、问题与分析
  问题: 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
  
  方法如下:
  我们仍旧可以使用位图来解决,但需要注意在查找交集前需要都文件内部数据去重。判断结果时,可以分别查找两个文件中是否存在该值true&&true,或者对两个位图进行按位与 1&1=1

在这里插入图片描述

  
  
  

4.1.3.3、题三

  1)、问题与分析
  问题: 位图应用变形。1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
  
  分析:此题大体方法和之前题一一致,只是需要的状态情况不同。00表示出现1次,01表示出现 1次,10表示出现2次,11表示3次及三次以上。

		//需求:给定的val值,设置其出现状况
		void set(size_t val)
		{
			bool ret1 = _bits1.test(val);
			bool ret2 = _bits2.test(val);

			if (ret1 == false && ret2 == false)
			{ //原先为00:此时val值进来,需要置为01,表示出现一次
				_bits2.set(val);
			}
			else if (ret1 == false && ret2 == true)
			{//原先为01:此时val值进来,需要设置为10,表示出现两次
				_bits1.set(val);
				_bits2.reset(val);
			}
			else if (ret1 == true && ret2 = false)
			{//原先为10:此时val值进来,需要设置为11,表示出现三次及三次以上
				_bits1.set(val);
				_bits2.set(val);
			}
			
		}

  
  
  
  
  
  

4.2、布隆过滤器

4.2.1、基本概念介绍

  布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
  
  
  场景分析: 对于字符串等类型判断其在集合中是否存在?
  说明: 使用一个哈希函数将其映射到一个位置并进行标记,如下,存在误判行为。
在这里插入图片描述

当前获取结果为存在:该判断不准确,可能会因为哈希冲突导致误判
当前获取结果为不存在:该判断准确

  
  上述情况如何改进?
  如下,我们可以使用多个哈希函数进行映射,以降低误判率。但这种误判率的降低不是可无限扩大的,哈希函数使用的越多,映射位置就越多,那么空间消耗也越多。
在这里插入图片描述

  因此,如何选择哈希函数的个数和布隆过滤器的长度就是其中一个需要探讨的问题。相关链接
在这里插入图片描述
  
  
  

4.2.2、相关接口实现

4.2.2.1、布隆过滤器的基本框架及哈希函数

  1)、关于哈希函数的选择问题说明
  根据上述公式描述,假设我们选取3个哈希函数,即 K = 3 K=3 K=3,则插入 N N N个元素,所需开辟的布隆过滤器长度为 M = K × N l n 2 ≈ 3 × N 0.7 ≈ 4.29 N M=\frac{K×N}{ln2}≈\frac{3×N}{0.7}≈ 4.29N M=ln2K×N0.73×N4.29N ,我们取 M = 5 N M=5N M=5N
  
  关于字符串哈希函数的选择如下:

//三个哈希函数
struct HashBKDR
{
	// BKDR
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
		{
			val *= 131;
			val += ch;
		}

		return val;
	}
};

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

struct HashDJB
{
	// BKDR
	size_t operator()(const string& key)
	{
		size_t hash = 5381;
		for (auto ch : key)
		{
			hash += (hash << 5) + ch;
		}

		return hash;
	}
};

  
  2)、基础结构框架

template<size_t N, class K=string, 
	class Hash1= HashBKDR, class Hash2= HashAP, class Hash3= HashDJB>
class BloomFilter
{
public:

private:
	const static size_t _ratio = 5;//const修饰的静态成员变量可赋缺省参数
	bitset<_ratio*N> _bits;
};

  上述实现中,我们直接在BloomFilter类中开辟了bitset<_ratio*N> _bits。若数据量过大时可能存在内存空间不够的问题,因此可以使用下述方法:直接在堆上申请空间。

template<size_t N, class K=string, 
	class Hash1= HashBKDR, class Hash2= HashAP, class Hash3= HashDJB>
class BloomFilter
{
public:

private:
	const static size_t _ratio = 5;//const修饰的静态成员变量可赋缺省参数
	bitset<_ratio* N>* _bits = new bitset<_ratio* N>;
	//bitset<_ratio*N> _bits;
};

  
  
  

4.2.2.2、set、test

  1)、对set的实现
  set的任务主要是在布隆过滤器中对给定的数值进行设置。因为我们上述规定使用三个哈希函数,因此需要分别用这三个哈希函数获取映射地址,并将该位置进行标记。

	void set(const K& val)
	{
		size_t hash1 = Hash1()(val) % (_ratio * N);
		_bits->set(hash1);

		size_t hash2 = Hash2()(val) % (_ratio * N);
		_bits->set(hash2);

		size_t hash3 = Hash3()(val) % (_ratio * N);
		_bits->set(hash3);
	}

  
  
  2)、对test的实现
  test的任务则是判断给定值是否存在于布隆过滤器中。根据之前内容,当我们分别在三个哈希函数映射的地址查询元素时,若得到的结果为true,存在误判行为,若结果为false,则表示确切结果。

	bool test(const K& val)
	{
		size_t hash1 = Hash1()(val) % (_ratio * N);
		if (!_bits->test(hash1))//非真:准确结果
			return false;

		size_t hash2 = Hash2()(val) % (_ratio * N);
		if (!_bits->test(hash2))
			return false;

		size_t hash3 = Hash3()(val) % (_ratio * N);
		if (!_bits->test(hash3))
			return false;

		return true;//所有哈希函数验证结果都为真,这里存在误判行为
	}

  
  
  

4.2.2.3、相关验证

  1)、验证一

void test11()
{
	BloomFilter<10> bf;
	string arr1[] = { "防风", "苏叶", "钩吻", "泽兰", "连翘", "桔梗", "当归", "防风", "钩吻", "防风", "连翘" };

	for (auto& str : arr1)
	{
		bf.set(str);
	}

	for (auto& str : arr1)
	{
		cout << str << ":" << bf.test(str) << " ";
	}
	cout << endl << endl;

	string arr2[] = { "防风01", "苏叶", "钩吻03", "泽兰", "连翘05", "桔梗", "当归07", "防风", "钩吻09", "防风", "连翘11" };

	for (auto& str : arr2)
	{
		cout << str << ":" << bf.test(str) << endl;
	}
}

在这里插入图片描述

  
  
  
  
  2)、验证二
  相关代码:

void test12()
{
	srand(time(0));
	const size_t N = 100000;
	BloomFilter<N> bf;//布隆过滤器
	cout << sizeof(bf) << endl;

	//——————————————————————————————————————
	//对照组:给一段字符串,其只是末尾有区别
	std::vector<std::string> v1;
	std::string url = "https://www.csdn.net/?spm=1001.2014.3001.4476";

	for (size_t i = 0; i < N; ++i)
	{
		v1.push_back(url + std::to_string(1234 + i));
	}

	for (auto& str : v1)
	{
		bf.set(str);
	}
	//——————————————————————————————————————


	//——————————————————————————————————————
	//实验组一:与对照组相似的字符串。目的:测试相似字符串在布隆过滤器中的误判率
	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "https://www.csdn.net/?spm=1001.2014.3001.4476";
		url += std::to_string(rand() + 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";
		url += std::to_string(rand() + i);
		v3.push_back(url);
	}

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

}

  
  结果一:可以看到相似字符串误判率相对高一些。
在这里插入图片描述

  
  结果二:调节布隆过滤器长度和插入元素数量的比值,误判率也会随之改变。
在这里插入图片描述

  
  
  
  

4.2.3、扩展运用:海量数据处理

4.2.3.1、问题一:布隆过滤器删除

  1)、问题:布隆过滤器支持删除吗?
  回答: 通常情况下,布隆过滤器不支持删除。因为当存在哈希冲突时,删除某一比特位的数据,可能会导致其它数据查找失效,造成误判。
  
  问题: 如果要强行支持删除元素的操作呢?
  回答: 一种支持删除的方法是,将布隆过滤器中的每个比特位扩展成一个小的计数器(用于统计k个哈希函数计算出的哈希地址处存储的数据量)。比如:插入元素时,给k个计数器加一,删除元素时,给k个计数器减一。但这样一来,布隆过滤器为了支持删除操作,多占用几倍存储空间,其优势也被削弱。
  
  

4.2.3.2、问题二:哈希切割

  问题描述:给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
  
  近似:上述布隆过滤器即可解决。
  
  精确:哈希切分。
在这里插入图片描述

  
  
  
  
  
  

  
  
  
  
  
  

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

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

相关文章

springBoot搭建

这里写目录标题 一、在学习spring Boot之前我们来回顾一下Spring二、Spring Boot介绍 三、Spring Boot开发环境搭配四、Spring Boot核心五、Spring Boot添加模块六、Spring Boot新注解介绍 一、在学习spring Boot之前我们来回顾一下Spring 首先说一下spring的优点: spring是轻…

Wireshark 解密https 数据

默认情况下 wireshark 抓到的https 数据包都是加密后的&#xff0c;无法展示明文内容 方式一 -SSLKEYLOGFILE 变量方式 【推荐&#xff0c;适用各种情况】 配置环境变量 浏览器在访问https 站点的时候会检测这个SSLKEYLOGFILE 变量&#xff0c;如果存在&#xff0c;则将https…

SpringCloud Nacos实战应用

目录 1 Nacos安装1.1 Nacos概要1.2 Nacos架构1.3 Nacos安装1.3.1 Nacos Derby安装1.3.2 Nacos MySQL版安装1.3.3 Docker 安装Nacos 2 Nacos功能应用2.1 Nacos服务注册与发现2.2 负载均衡2.3 配置中心2.4 灰度发布 3 Nacos集群3.1 集群架构3.2 Nacos集群部署3.3 客户端接入Nacos…

c# 依赖注入

依赖注入 文章目录 依赖注入一、.net core主要提供了三种依赖注入的方式二、权重三、如果我们需要注入的对象很多怎么办 一、.net core主要提供了三种依赖注入的方式 AddTransient瞬时模式&#xff1a; 每次请求&#xff0c;都获取一个新的实例。即使同一个请求获取多次也会是…

2022年度互联网平均薪资出炉!高到离谱!

近期&#xff0c;国家统计局发布2022年平均工资数据&#xff0c;互联网行业薪资再次成为大家关注的焦点。 在2022年分行业门类分岗位就业人员年平均工资中&#xff0c;信息传输、软件和信息技术服务业的薪资遥遥领先其他行业&#xff0c;为全国平均薪资水平的 1.78 倍&#xf…

devfreq

devfreq 是指频率电源可以动态调节的设备&#xff0c;可以添加不同设备及不同govvernor ; devfreq 框架和opp(operating performance point) 频率电源对 devices driver 通过 devfreq profile 交互 devfreq_dev_profile include/linux/devfreq.h devfreq_governor 与 dev…

如何用二极管实现不同电压的输出?

利用二极管的单向导电性可以设计出好玩、实用的电路。本文分析限幅电路和钳位电路&#xff0c;是如何用二极管来实现的。 限幅电路 如下图所示&#xff0c;当在正半周期&#xff0c;并且VIN大于等于0.7V&#xff0c;二极管正向导通。此时&#xff0c;VOUT会被钳位在0.7V上。 …

C++11:右值引用 -- 移动构造和移动赋值

目录 一. 左值引用和右值引用的概念和性质 1.1 什么是左值引用和右值引用 1.2 左值引用和右值引用的性质 二. 移动构造和移动赋值 2.1 左值引用的缺陷 2.2 临时对象返回减少拷贝的问题&#xff08;移动构造和移动赋值&#xff09; 2.3 C11 STL容器接口的一些变化 三. 完…

【C++进阶之路】手把手教你使用string类的接口

文章目录 前言基本认识基本使用 一.构造函数默认构造函数拷贝构造函数其它构造函数①string(const char* s)②string(size_t n, char c)③string (const string& str, size_t pos, size_t len npos) 二.容量接口①size与length②max_size③capacity④empty⑤clear⑥revers…

Elastic Learned Sparse Encoder 简介:Elastic 用于语义搜索的 AI 模型

作者&#xff1a;Aris Papadopoulos, Gilad Gal 寻找意义&#xff0c;而不仅仅是文字 我们很高兴地与大家分享&#xff0c;在 8.8 中&#xff0c;Elastic 提供开箱即用的语义搜索。语义搜索旨在根据文本的意图或含义进行搜索&#xff0c;而不是词汇匹配或关键字查询。与传统的…

华为云服务器租用费用及CPU性能(1核2G/2核4G/4核8G)

华为云HECS云服务器即云耀云服务器&#xff0c;类似于阿里云和腾讯云的轻量应用服务器&#xff0c;HECS云服务器1核2G配置39.02元一年、2核4G配置99元一年、4核8G配置69.94元3个月&#xff0c;华为云百科分享华为云HECS云服务器租用费用及CPU性能详解&#xff1a; 目录 华为云…

图解LeetCode链表题

&#x1f490;文章导读 本篇文章主要详细的用图解的方式为大家讲解了简单程度的链表题&#xff0c;如果题中有错误的地方&#xff0c;还麻烦您在评论区指出&#xff0c;你的意见就是我最大的进步&#xff01;&#xff01;&#xff01; &#x1f490;专栏导读 &#x1f934;作者…

什么是数字化?企业为什么要数字化转型

一、什么是数字化&#xff1f; 什么是数字化&#xff1f;在我理解&#xff0c;数字化是一个基于时代科技发展所产生的概念&#xff0c;首先它是一个工具&#xff0c;在企业的经营发展中将信息技术融入到传统的企业模式中&#xff0c;起到了转型的作用。 其次数字化转型是企业…

国产易灵思FPGA的FIFO应用详解

一、软件设置界面 FIFO&#xff08;First In First Out&#xff0c;即先入先出&#xff09;&#xff0c;是一种数据缓冲器&#xff0c;用来实现数据先入先出的读写。与 ROM 或 RAM 的按地址读写方式不同&#xff0c; FIFO 的读写遵循“先进先出”的原则&#xff0c;即数据按顺…

关于this->moveToThread(this)——QtWidgets

前言 官方关于QThread的用法有两种&#xff1a;一是子类QThread&#xff0c;并重新实现run&#xff1b;二是使用QObject::MoveToThread&#xff0c;通过信号槽在不同的线程内通信。 最近看到了一种写法&#xff0c;就是将两者融合就是子类QThread&#xff0c;然后this->mo…

【小程序】封装时间选择组件:用单元格van-cell和插槽slot,包括起始时间和终止时间

效果 可以选择起始时间和终止时间&#xff0c;并显示。 时间选择器放在van-cell的value插槽中。 用的库&#xff1a; https://vant-contrib.gitee.io/vant-weapp/#/home https://dayjs.fenxianglu.cn/category/ 用的组件&#xff1a;Cell单元格、DatetimePicker时间选择、Pop…

【Unity编辑器扩展】(二)PSD转UGUI Prefab, 图层解析和碎图导出

书接上回&#xff1a;【Unity编辑器扩展】(一)PSD转UGUI Prefab, Aspose.PSD和Harmony库的使用_TopGames的博客-CSDN博客 工具使用预览&#xff1a; 工具目标&#xff1a; 1. 实现将psd解析生成为UI预制体&#xff0c;并导出UI图片。需支持UGUI和TextMeshProGUI, 如Button、To…

文心一言 VS 讯飞星火 VS chatgpt (22)-- 算法导论4.2 2题

二、为Strassen算法编写伪代码。 文心一言&#xff1a; Strassen算法是一种用于矩阵乘法的算法&#xff0c;它的核心思想是通过不断将原始矩阵分割成较小的子矩阵来实现高效的乘法运算。以下是Strassen算法的伪代码&#xff1a; function Strassen(A, B): if A[0][0] 0 or…

优化|数学软件是如何求解线性方程Ax=b ?

编者按 对于大家来说&#xff0c;我们从学会多项式开始就得和求解矩阵方程打交道。大学之前靠手算&#xff0c;到了大学阶段我们学会了使用科学计算软件&#xff0c;然后只需要输入简单的一行指令 x A \ b x A \backslash b xA\b&#xff0c;就可以轻轻松松求解方程组 A x …

html实现酷炫科技风大屏风格模板(附源码)

文章目录 1.设计来源1.1 大屏主界面1.2 弹框界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/130870963 html实现酷炫科技风大屏风格模板源码 &#xff0c;html大屏源码…