哈希介绍以及综合运用

1.unordered 系列容器的介绍

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到log_2 N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好 的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个 unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是 其底层结构不同,本文中只对unordered_map和unordered_set进行介绍, unordered_multimap和unordered_multiset可自行查看文档介绍。

1.1 unordered_map介绍

(因为某种程度上来说,这里unordered_map 和unordered_set的差别不是很大,可以具体参考map和set的差别,因此这里就只着重介绍unordered_map了)

unordered一词直接说明力它与传统map的最大区别(它的数据存储是无序的),其他实际用法其实与map没有特别大的区别。

其次unordered_map也实现了"[ ]"这一运算符的重载,其用法和map一样。

1.2 其和普通map的区别

1**.unordered_map 里insert使用中没有make_pair这个函数**

2.unodered_map 的查询和桶操作。

函数声明功能介绍
iterator find(const K&key)返回key在哈希桶中的位置
size_t bucket_count() const返回哈希桶中桶的总个数
size_t bucket_size() const返回n号桶中有效元素的总个数
size_t bucket(const K& key)返回元素key所在的桶号
1.3unordered_map 底层

首先unordered_map 效率比 map 快多了是因为其顶层封装的是hash结构,那么下面便让我们来介绍一下这种结构吧。

2.hash基础原理

2.1基础原理

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

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

在这里插入图片描述

2.2哈希函数

如上图所示,哈希函数的设计其实就是一种元素的对应方式,而这样的对应方式又往往会发生冲突(比如我两个数模10之后得到的结果是一样的,那么这两个数将会发生冲突,通常处理方式是把后面加入进来的那个数往后放一位。

所以面对不同的问题的时候我们哈希函数的设计就显得尤为重要。

常见哈希函数的设计方法:

1.直接定址法–(常用)

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况

2.除留余数法–(常用)

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,

按照哈希函数**:Hash(key) = key% p(p<=m)**,将关键码转换成哈希地址。

3.平方取中法–(了解)

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;

再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址

平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4.折叠法–(了解)

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这 几部分叠加求和,并按散列表表长,取后几位作为散列地址。

折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5.随机数法–(了解)

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。

通常应用于关键字长度不等时采用此法

6.数学分析法–(了解)

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定 相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只 有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散 列地址。

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

3.哈希冲突解决

解决哈希冲突的两种方式:闭散列开散列

3.1闭散列
也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

那么因该如何去查找这下一个位置呢?

3.1.1线性探测

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

tips:(采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。因此线性探测采用标记的伪删除法来删除一个元素。)

3.1.2二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位 置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:

若当前key与原来key产生相同的哈希地址,则当前key存在该地址后偏移量为(1,2,3…)的二次方地址处
key1:hash(key)+0
key2:hash(key)+1^2
key3:hash(key)+2^2

当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出 必须考虑增容。

3.1.3哈希表的扩容问题

散列表负载因子的定义:a = 填入表中的元素个数 / 散列表的长度

其实控制是否扩容,本质就在控制这个负载因子,一般负载因子控制在0.7~0.8之下。

3.2开散列

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

在这里插入图片描述

3.3两者比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

4.Hash表的实现(线性探测版本)

4.1 3种状态
enum Status
{
    EXIST,
    EMPTY,
    DELETE
};
4.2 哈希的基础数据
template<class K,class V>
struct HashData
{
    pair<K,V> _kv;
    Status _status = EMPTY;
};
4.3为了接收数据时可以接收string类,做了一个特化处理
template<class T>
struct Hash
{
    size_t operator() (const K& key)
    {
        return K;
    }
};
template<>
struct Hash<string>
{
    size_t operator()(const string& s)
		{
			// BKDR这是一种常用的算法,为了防止不同单词但总字符ascii码值刚好一样
			size_t value = 0;
			for (auto ch : s)
			{
				value *= 31;
				value += ch;
			}
			return value;
		}
};
4.4哈希表的基本结构
template<class K, class V, class HashFunc = Hash<K>>//注意这里等号后面这么用很巧妙,其实目的还是为了支持string类
class HashTable
{
    private:
    vector<HashData<K, V>> _tables;
    size_t _n = 0;// 有效数据个数
};
4.5查找
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;  
    //线性探测/二次探测
    while (_tables[index]._status != EMPTY)
		{
		  if (_tables[index]._kv.first == key && _tables[index]._status == EXIST) //强调了一个状态和是否一样

			  {
				return &_tables[index];
			  }

			++i;
			//index = start + i*i;
			index = start + i; //这里可以看出 i 存在的目的就是为了往前加一位,或者使用二次探测

			index %= _tables.size(); //防止你下标的越界访问(重点其实就是二次探测万一过界)
		}
}
4.6删除
bool Erase(conset K& key)
{
    HashData<K, V>* ret = Find(key);
	if (ret == nullptr)
	{
		return false;
	}
    else
    {
        --_n;
		ret->_status = DELETE;//实际我的值还是存在那里只是改了个状态
		return true;
    }
}
4.7插入
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;
        
        HashTable<K, V, HashFunc> newHT; //新开一个表
		newHT._tables.resize(newSize); //其实还得去找那个vector去新开一个表出来
        //将数据拷贝下来
	  for (size_t i = 0; i < _tables.size(); ++i)
		{
			
          if (_tables[i]._status == EXIST)
				{
					newHT.Insert(_tables[i]._kv);
				}
		}

			_tables.swap(newHT._tables); //直接交换地址
        }
      //仿照上面find的操作
       HashFunc hf;
       size_t start = hf(kv.first) % _tables.size();
	   size_t i = 0;
	   size_t index = start;
    
       while (_tables[index]._status == EXIST || _tables[index]._status == DELETE)
			{
				++i;
				//index = start + i*i;
				index = start + i;

				index %= _tables.size();
			}
      _table[index]._kv = kv;
      _tables[index]._status = EXIST;
      ++_n;
    
     return true;
}

5.Hash表的实现(桶版本)//这边就略讲了,只挑一些不一样的地方重点分析

5.1基础结点
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)
		{}
	};
5.2哈希表的基础结构
template<class K, class V, class HashFunc = Hash<K>>
class HashTable
	{
		typedef HashNode<K, V> Node;
    
    private:
	
		vector<Node*> _tables;

		size_t _n = 0;  // 有效数据的个数
	};
5.3查找
	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;
		}
5.4删除
bool Erase(const K& key)
		{
			if (_tables.empty())
			{
				return false;
			}

			HashFunc hf;
			size_t index = hf(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[index];
			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;
					cur = cur->_next;
				}
			}

			return false;
		}
5.5查找
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;
		}
5.6插入
bool Insert(const pair<K, V>& kv)
		{
			Node* ret = Find(kv.first);
			if (ret)
				return false;

			HashFunc hf;
			// 负载因子 == 1时扩容
			if (_n == _tables.size())
			{
				size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*> newTables;
				newTables.resize(newSize);
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t index = hf(cur->_kv.first) % newTables.size();
						// 头插
						cur->_next = newTables[index];
						newTables[index] = cur;

						cur = next;
					}

					_tables[i] = nullptr;
				}

				_tables.swap(newTables);
			}

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

			++_n;
			return true;
		}

6.封装

6.1unordered_set 的封装
template<class K, class hash = Hash<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename LinkHash::HashTable<K, K, SetKeyOfT, hash>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}

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

		bool insert(const K& key)
		{
			return _ht.Insert(key);
		}
	private:
		HashTable<K, K, SetKeyOfT, hash> _ht;
	};
6.2unordered_map的封装
template<class K, class V, class hash = Hash<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename LinkHash::HashTable<K, pair<K, V>, MapKeyOfT, hash>::iterator iterator;
		iterator begin()
		{
			return _ht.begin();
		}

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


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

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

7.位图

7.1基础概念

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

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

比如你有一个数组,那么此时创造一个位图,位图的每一位都用0或者1来表示这个数组对应下标的元素是否存在,这样位图就能节省非常多的空间。

7.2具体实现
template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			_bits.resize(N / 8 + 1, 0);
		}

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

			_bits[i] |= (1 << j);
		}

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

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

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

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

	private:
		std::vector<char> _bits;
		//std::vector<int> _bits;
	};

8. 布隆过滤器

8.1概念

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

8.2具体机制

当我们需要检索一个元素是否存在于一个集合中时,通常的做法是使用哈希表或者二叉搜索树等数据结构。但是,在一些情况下,这些数据结构的存储和查询成本可能很高,例如,当数据量很大时,或者当内存非常有限时。在这些情况下,布隆过滤器是一种更加高效的数据结构。

布隆过滤器是一种用于检索一个元素是否属于一个集合的数据结构。它的基本原理是利用多个不同的哈希函数,将元素映射到一个固定长度的二进制位数组(位图)中。当要查询一个元素是否属于该集合时,我们可以用同样的哈希函数将该元素映射到位图上,如果对应位置的值为1,则说明该元素可能在集合中,但如果对应位置的值为0,则该元素肯定不在集合中。

由于布隆过滤器使用的是哈希函数,因此它具有很快的查询速度和空间效率。然而,布隆过滤器的缺点是,当位图中有多个元素映射到同一位置时,会出现冲突,这会导致误判。另外,一旦元素被加入到布隆过滤器中,就无法删除。

8.3代码实现
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,
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;
		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;
};

9.哈希切割

9.1概念

哈希切割是一种常见的数据分割方法,主要用于将大数据集划分成多个小数据块,以便更快地进行数据查询和处理。

哈希切割的原理是将数据块按照哈希函数的值进行划分,即将每个数据块映射到一个唯一的哈希值,并将具有相同哈希值的数据块放置在同一数据块中。这样,数据查询时可以通过哈希函数快速定位到数据块,从而加快查询速度。

举个例子,假设有一个拥有100万个用户的社交网络平台,为了提高查询速度,我们可以将用户数据按照用户ID的哈希值进行划分,将具有相同哈希值的用户放置在同一数据块中。这样,当用户登录时,系统只需要查询包含其用户ID的数据块,而不需要遍历整个数据集,从而加快查询速度。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值