unordered_map/set

1.unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,但是在节点较多的情况下,查询效率也不理想,于是,在C++11中STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,查询效率更快。

2.unordered_map

(1). unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与
其对应的value。
(2). 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
(3). 在内部,unordered_map没有对按照任何特定的顺序排序, 为了能在常数范围内
找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
(4). unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭
代方面效率较低。
(5). unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问
value。
(6). 它的迭代器至少是前向迭代器。
#pragma once
#include "HashTable.h"

namespace L
{

	// 一个类型要做unordered_map/unordered_set的key,要满足支持转换成取模的仿函数和 == 比较
	// 一个类型要做set/map的key,要满足支持 < 比较
	template >
	class unordered_map
	{
	public:
		struct KeyOfT_map
		{
			const K& operator()(const pair& kv)
			{
				return kv.first;
			}
		};

		typedef typename HashBucket::HashBucket, KeyOfT_map,Hash>::iterator iterator;
		typedef typename HashBucket::HashBucket, KeyOfT_map, Hash>::const_iterator const_iterator;

		iterator begin()
		{
			return _bucket.begin();
		}

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

		const_iterator begin() const
		{
			return _bucket.begin();
		}

		const_iterator end() const
		{
			return _bucket.end();
		}

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

		pair Insert(const pair& kv)
		{
			return _bucket.Insert(kv);
		}
	private:
		HashBucket::HashBucket, KeyOfT_map,Hash> _bucket;
	};

3.unordered_set

(1)不再以键值对的形式存储数据,而是直接存储数据的值。

(2)容器内部存储的各个元素的值都互不相等,且不能被修改。

(3)不会对内部存储的数据进行排序(这和该容器底层采用哈希表结构存储数据有关。

#pragma once
#include "HashTable.h"

namespace L
{
	// 一个类型要做unordered_map/unordered_set的key,要满足支持转换成取模的仿函数和 == 比较
	// 一个类型要做set/map的key,要满足支持 < 比较

	template >
	class unordered_set
	{
	public:

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

		typedef typename HashBucket::HashBucket::const_iterator iterator;
		typedef typename HashBucket::HashBucket::const_iterator const_iterator;

		iterator begin()
		{
			return _bucket.begin();
		}

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

		const_iterator begin() const
		{
			return _bucket.begin();
		}

		const_iterator end() const
		{
			return _bucket.end();
		}

		pair Insert(const K& key)
		{
			return _bucket.Insert(key);
		}
	private:
		HashBucket::HashBucket _bucket;
	};

 

 4.与map/set的区别

C++11新增的unordere系列的map和set与C++98的map和set不同点有:

(1)底层结构不同,C++98底层是红黑树结构,C++11底层是哈希表/哈希桶结构。

(2)查询效率不同,C++11的map和set查询效率更高。

(3)功能略微不同,C++98的map和set支持按key排序,而unordered_map/set不支持排序。

5.底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
为哈希表(Hash Table)(或者称散列表)。

5.1 哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。为了尽可能的不产生那么多哈希冲突,引入哈希函数。

5.2 哈希函数

常见哈希函数
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种不同的符号在各位上出现的频率不一定
相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
列地址。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

5.3 哈希冲突解决 

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

5.3.1 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
又分为线性探测和二次线性探测。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
unordered_map/set_第1张图片

一般当负载因子大于等于0.7,哈希表则开始扩容。 

	size_t hashi = kv.first % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state == EXIST)
			{
				index = hashi + i;
				index %= _tables.size();
				++i;
			}

			_tables[index]._kv = kv;
			_tables[index]._state = EXIST;
			++_n;

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

闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。 

5.3.2 开散列(链地址法)

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中
unordered_map/set_第2张图片
5.3.2.1 增容问题
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
表进行增容,每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

如果key为字符串类型 ,可以提供一个仿函数,把字符串转成size_t类型

size_t operator()(const string& str)
	{
		size_t hash = 0;
		for (auto ch : str)
		{
			hash += ch;
			hash *= 31;
		}

		return hash;
	}

 // 一个类型要做unordered_map/unordered_set的key,要满足支持转换成取模的仿函数和 == 比较。
 // 一个类型要做set/map的key,要满足支持 < 比较,因为其底层是红黑树,而红黑树本质是二叉搜索树。

 //如果哈希桶里的一个桶链接的节点过多,可以把这个桶改为链接红黑树。

5.3.3 再哈希法 

同时构造多个不同的哈希函数,等发生哈希冲突时就使用第二个、第三个……等其他的哈希函数计算地址,直到不发生冲突为止。虽然不易发生聚集,但是增加了计算时间。 

 

5.4 通过key获取value

struct KeyOfT_map
		{
			const K& operator()(const pair& kv)
			{
				return kv.first;
			}
		};

 

你可能感兴趣的:(C++学习,数据结构学习,哈希算法,算法)