哈希表的模拟实现

一.unordered_set

1.unordered_set的介绍

1>unordered_set底层关键字类型为Key

2>unordered_set默认要求Key⽀持转换为整形,并支持比较大小

3>unordered_set底层存储数据的内存是从空间配置器申请的

4>unordered_set底层⽤哈希桶实现增删查平均效率是O(1),注意此时迭代器遍历不再有序

2.unordered_set和set的异同

同:两者增删查的使用方法相同

异:

1>set要求Key⽀持⼩于⽐较,⽽unordered_set要求Key⽀持转成整形且⽀持等于⽐较

2>set的iterator是双向迭代器,unordered_set是单向迭代器,其次set底层是红⿊树,红⿊树是⼆叉搜索树,⾛中序遍历是有序的,所以set迭代器遍历是有序+去重。⽽unordered_set底层是哈希表,迭代器遍历是⽆序+去重

3>unordered_set的增删查改更快⼀些,因为红⿊树增删查改效率是O(logN),⽽哈希表增删查平均效率是O(1)

二.unordered_map和map(与unordered_set和set的内容相同)

注:unordered_multimap/unordered_multiset⽀持Key冗余。

三.哈希表

1.哈希概念

1>本质就是通过哈希函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过哈希函数计算出Key存储的位置,进⾏快速查找。

2.直接定址法

1>本质:⽤关键字计算出⼀个绝对位置或者相对位置

2>使用条件:关键字的范围⽐较集中

3>缺点:当关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤

3.哈希冲突

1>定义:两个不同的key映射到同⼀个位置,这种情况就叫哈希冲突

注:实际上哈希冲突是不可避免的,所以在设计哈希函数时要尽量减少冲突

2>负载因子:假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么负载因⼦= N/M。

负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低

4.将关键字转为整数

将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整数

5.哈希函数

注:好的哈希函数要尽量做到让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中

1>除法散列法/除留余数法

(1)方法介绍:假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key)=key%M

(2)M的选取:M尽量避免去2的幂,10的幂之类的数。建议M取不太接近2的整数次幂的⼀个素数。

例:key%2^X本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了

2>乘法散列法:1.⽤关键字K乘上常数A(0<A<1),并抽取出k*A的⼩数部分。2.后再⽤M乘以k*A的⼩数部分,再向下取整。

注:1.此法对于M没有取值限制

2.h(key)=floor(M×((A×key)%1.0)),A建议取黄金分割点

3>全域散列法:给散列函数增加随机性,hab (key)=((a×key+b)%P)%M,P需要选⼀个⾜够⼤的质数,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了⼀个P*(P-1)组全域散列函数组。

注:每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使⽤,后续增删查
改都固定使⽤这个散列函数

6.处理哈希冲突

1>开放定址法

(1)方法介绍:当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于0.7的。这⾥的规则有三种:线性探测、⼆次探测、双重探测。

(2)线性探测:

规则介绍:从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。

公式:hc(key,i)=hashi=(hash0+i) % M, i = {1,2,3,...,M−1}

缺点:可能会出现你抢占我的位置,我去抢占他人位置的堆积/群集问题

(3)二次探测:

规则介绍:从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表尾的位置

公式:hc(key,i)=hashi=(hash0±i) % M, i = {1,2,3,..., M/2 }

⼆次探测当hashi=(hash0−i*i)%M时,当hashi<0时,hashi+=M

(4)双重探测:

规则介绍:第⼀个哈希函数计算出的值发⽣冲突,使⽤第⼆个哈希函数计算出⼀个跟key相关的偏移量值,不断往后探测,直到寻找到下⼀个没有存储数据的位置为⽌。

公式:hc(key,i)=hashi=(hash0+ i∗h (key)) % M, i = {1,2,3,...,M}

要求:h (key)<M且h (key)和M互为质数,

取值⽅法:

1、当M为2整数幂时,h (key)从[0,M-1]任选⼀个奇数;

2、当M为质数时,h (key) = key % (M−1) + 1,保证h (key)与M互质是因为根据固定的偏移量所寻址的所有位置将形成⼀个群

2>链地址法(哈希桶)

(1)内容:哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯。

(2)链地址法的负载因⼦没有限制,可以⼤于1。stl中unordered_xxx的最⼤负载因⼦基本控制在1,⼤于1就扩容。

(3)极端情况:当桶的⻓度超过⼀定阀值(8)时就把链表转换成红⿊树。⼀般情况下,不断扩容

四.哈希表的模拟实现:以开放地址法中的线性探测解决哈希冲突

1.哈希表每个位置对应的状态:存在,删除,空

enum State
{
	Empty,
	Delete,
	Exist
};

2.构造函数:为了保证扩容后哈希表的存储空间仍然是接近2的整数幂的素数,直接将所有可能情况列举出来,直接调用,以减少哈希冲突

//哈希表存储空间的大小:M
inline unsigned long __stl_next_prime(unsigned long n)
{
	// Note: assumes long is at least 32 bits.
    static const int __stl_num_primes = 28;
	static const unsigned long __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
	};
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list +__stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}

HashTable()
{
	_table.resize(__stl_next_prime(_table.size()));
}

3.插入:

1>因为不支持冗余,所以应该先查找kv是否在哈希表中出现

2>插入要增加数据个数,为了减小哈希冲突,需要根据负载因子判断是否需要扩容

扩容时可以新开一个待扩容大小的哈希表,将原来的数据拷贝进去,在与旧的哈希表交换,更加更方便

3>寻找插入位置

4>将kv插入指定位置,修改该位置的相关属性

bool Insert(const pair<K, V>& kv)
{
	if (Find(kv.first)) return false;

	//检查是否需要扩容:当负载因子大于等于0.7时,哈希冲突的可能性会增加,此时通过扩容来减小哈希冲突
	if (_size * 10 / _table.size() >= 7)
	{
		HashTable<K,V,Hash> newtable;
		newtable._table.resize(__stl_next_prime(_table.size()));
		for (size_t i = 0; i < _table.size(); i++)
		{
			if (_table[i]._state == Exist)
		    {
				newtable.Insert(_table[i]._kv);
			}
		}
		_table.swap(newtable._table);
	}

	//插入
	Hash hs;
	size_t hashi = hs(kv.first) % _table.size();//确定对应的映射位置
	while (_table[hashi]._state != Empty)
	{
		++hashi;
		hashi %= _table.size();
	}
	_table[hashi]._kv = kv;
	_table[hashi]._state = Exist;

	_size++;
	return true;
}

4.查找:找到key对应的映射下标,若该处有元素,进行判断,依次向后查找

HashData<K,V>* Find(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _table.size();//确定对应的映射位置
	while (_table[hashi]._state != Empty)
	{
		if (_table[hashi]._state == Exist && _table[hashi]._kv.first == key)
			return &_table[hashi];

		++hashi;
		hashi %= _table.size();
	}
    return nullptr;
}

5.删除:先判断key是否在哈希表内,若在将key对应位置的状态改成Delete,表明此处的值已被删除

bool Erase(const K& key)
{
	HashData<K, V>* ret = Find(key);
	if (ret == nullptr) return false;
	else
	{
		ret->_state = Delete;
		return true;
	}
}

6.将key转为整型

1>为了便于找到key对应的位置,需要将key强转成size_t类型,此时可以利用仿函数来实现这一功能

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		//key要求强转成整型
		return (size_t)key;
	}
};

2>假设key为string类型,要单独写一个仿函数来实现key强转成size_t类型的目的,但是我们也可以对之前的仿函数特化,当key为string类型时,先调用特化类型

//特化:处理字符串存哈希表的情况
template<>
struct HashFunc<string>
{
	//用字符串中所有字符的ASCII码值之和来确定所在位置
	//为了避免相同ASCII码值之和的情况,可以在加上字符的ASCII码值前,对和*131
	size_t operator()(const string& s)
	{
		size_t ret=0;
		for (auto ch : s)
		{
			ret *= 131;
			ret += ch;
		}
		return ret;
	}
};

注:若是key为不常见类型,可以单独写一个仿函数,传给HashTable就可以了

7.完整代码

#include<iostream>
#include<vector>
using namespace std;

enum State
{
	Empty,
	Delete,
	Exist
};

template<class K,class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = Empty;
};

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		//key要求强转成整型
		return (size_t)key;
	}
};

//特化:处理字符串存哈希表的情况
template<>
struct HashFunc<string>
{
	//用字符串中所有字符的ASCII码值之和来确定所在位置
	//为了避免相同ASCII码值之和的情况,可以在加上字符的ASCII码值前,对和*131
	size_t operator()(const string& s)
	{
		size_t ret=0;
		for (auto ch : s)
		{
			ret *= 131;
			ret += ch;
		}
		return ret;
	}
};

template<class K,class V,class Hash = HashFunc<K>>
class HashTable
{
public:
	//扩容
	inline unsigned long __stl_next_prime(unsigned long n)
	{
		// Note: assumes long is at least 32 bits.
	    static const int __stl_num_primes = 28;
		static const unsigned long __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
		};
		const unsigned long* first = __stl_prime_list;
		const unsigned long* last = __stl_prime_list +__stl_num_primes;
		const unsigned long* pos = lower_bound(first, last, n);
		return pos == last ? *(last - 1) : *pos;
	}

	HashTable()
	{
		_table.resize(__stl_next_prime(_table.size()));
	}

	HashData<K,V>* Find(const K& key)
	{
		Hash hs;
		size_t hashi = hs(key) % _table.size();//确定对应的映射位置
		while (_table[hashi]._state != Empty)
		{
			if (_table[hashi]._state == Exist && _table[hashi]._kv.first == key)
				return &_table[hashi];

			++hashi;
			hashi %= _table.size();
		}

		return nullptr;
	}

	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first)) return false;

		//检查是否需要扩容:当负载因子大于等于0.7时,哈希冲突的可能性会增加,此时通过扩容来减小哈希冲突
		if (_size * 10 / _table.size() >= 7)
		{
			HashTable<K,V,Hash> newtable;
			newtable._table.resize(__stl_next_prime(_table.size()));
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]._state == Exist)
			    {
					newtable.Insert(_table[i]._kv);
				}
			}
			_table.swap(newtable._table);
		}

		//插入
		Hash hs;
		size_t hashi = hs(kv.first) % _table.size();//确定对应的映射位置
		while (_table[hashi]._state != Empty)
		{
			++hashi;
			hashi %= _table.size();
		}
		_table[hashi]._kv = kv;
		_table[hashi]._state = Exist;

		_size++;
		return true;
	}

	bool Erase(const K& key)
	{
		HashData<K, V>* ret = Find(key);
		if (ret == nullptr) return false;
		else
		{
			ret->_state = Delete;
			return true;
		}
	}

private:
	vector<HashData<K, V>> _table;
	int _size = 0;//哈希表内存储数据的个数
};

五.哈希表的模拟实现:以链地址法解决哈希冲突问题

1.哈希节点

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

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

2.构造函数

//扩容
		inline unsigned long __stl_next_prime(unsigned long n)
		{
			// Note: assumes long is at least 32 bits.
			static const int __stl_num_primes = 28;
			static const unsigned long __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
			};
			const unsigned long* first = __stl_prime_list;
			const unsigned long* last = __stl_prime_list + __stl_num_primes;
			const unsigned long* pos = lower_bound(first, last, n);
			return pos == last ? *(last - 1) : *pos;
		}

	public:
		HashTable()
		{
			_table.resize(__stl_next_prime(_table.size()),nullptr);
		}

3.析构函数:逐个桶释放

~HashTable()
{
	for (size_t i = 0; i < _table.size(); i++)
	{
		//逐个桶释放
		Node* cur = _table[i];
		while (cur)
		{
			Node* tmp = cur->_next;
			delete cur;
			cur = tmp;
		}
		_table[i] = nullptr;
	}
}

4.插入

1>计算待插入的位置坐标

2>判断是否需要扩容:若负载因子为1,则扩容

创建一个大小为待扩容大小的指针数组,将原数组中的值一次复制过来后,交换两个数组

bool Insert(const pair<K, V>& kv)
{
	Hash hs;
	size_t hashi = hs(kv.first) % _table.size();

	if (_n == _table.size())//负载因子为1,扩容
	{
		vector<Node*> newtables(__stl_next_prime(_tables.size()), nullptr);
		for (size_t i = 0; i < _table.size(); i++)
	    {
			Node * cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
			    // 旧表中节点,挪动新表重新映射的位置 
				size_t hashi = hs(cur->_kv.first) %newtables.size();
				// 头插到新表 
			    cur->_next = newtables[hashi];
				newtables[hashi] = cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
		 _tables.swap(newtables);
	}

	//插入
	Node* tmp = new Node(kv);
	tmp->_next = _table[hashi];
	_table[hashi] = tmp;
	_n++;
	return true;
}

5.查找

Node* Find(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _table.size();
	Node* cur = _table[hashi];
	while (cur)
	{
		if (cur->_kv.first == key) return cur;
		cur = cur->_next;
	}
    return nullptr;
}

6.删除

		bool Erase(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					_n--;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值