深剖STL——哈希表(内含unordered_map和unordered_set模拟实现)

1.什么是哈希

哈希(Hash)是指将任意长度的输入通过哈希函数(Hash Function)转换为固定长度的输出的过程。哈希函数可以将输入数据映射为较短的固定长度的值,通常用一串数字和字母来表示,这串值就被称为哈希值或者哈希码。

哈希函数具有以下特性:

  1. 输入相同的数据经过哈希函数得到的哈希值是唯一确定的。 即使输入数据只有微小的改动,得到的哈希值也会发生很大的变化。
  2. 无法从哈希值中逆向推导出原始的输入数据。
  3. 不同的输入数据得到的哈希值一般是不同的,但由于哈希值的长度是固定的,所以可能会发生不同的输入数据得到相同的哈希值,这种情况被称为哈希冲突。

2.哈希函数

一般来说,哈希函数,只要你想,什么样的哈希函数都可以,但是考虑到实际的存储和哈希冲突,一般都采用主流的哈希函数。

比如,这个大佬写的博客上讲解了很多种的字符串哈希函数:
字符串hash函数
这种哈希函数是针对存储数据是字符串类型的情况。
常见的哈希函数:(hashi是哈希表存储位置的下标,key表示要存储的值的关键字)

  1. 直接定址法:
    哈希函数:取关键字的某个线性函数为散列地址:hashi=A*key+B
    优点:简单、均匀
    缺点:需要事先知道关键字的大小范围;如果出现最大的数很大且分散,会导致大量空间浪费
    适用场景:查找比较小且连续的数
  2. 除留余数法:
    哈希函数:设散列表的大小为m,去一个不大于m,但最接近m的质数p作为除数:hashi=key%p(p<=m)
    优点:无需知道关键字的大小范围,手动控制空间大小
    缺点:关键字不均匀时容易发生哈希冲突;只适用于整型,其他类型使用需要先转成整型
    适用场景:关键字分布较为均匀的时候
  3. 平方取中法:
    哈希函数:将关键字进行平方运算,取平方结果的中间几位作为哈希值:hashi=MSMethod(key*key)
    优点:简单易实现,均匀性较好
    缺点:对关键字长度敏感;哈希冲突风险
    适用场景:中小规模数据集;整数类型的关键字;哈希冲突处理要求不严格的场景
  4. 折叠法:
    哈希函数:其基本原理是将关键字按固定长度分割,然后相邻分段相加得到哈希值。
    优点:对于大型关键字适用性强,相对简单易实现
    缺点:可能会导致哈希冲突,特别是在分割长度选择不当的情况下;效率可能不如其他更复杂的哈希函数
    适用场景:适合处理较大关键字的哈希需求;在对哈希函数的均匀性要求不是特别严格的情况下适用
  5. 随机数法:
    哈希函数:通过随机选择一个数作为哈希值:hashi=random()
    优点:简单快速,实现简单,不需要复杂的计算;哈希冲突较少,由于随机选择哈希值,冲突风险较低
    缺点:不确定性,哈希值是随机生成的,不具有可预测性;哈希表大小限制,需要确保随机数的范围在哈希表大小范围内
    适用场景:小规模数据集;无需持久化的临时哈希需求;对哈希值唯一性要求不高
  6. 数学分析法:
    哈希函数:通过对关键字进行数学运算得到哈希值
    优点:控制性强,设计者可以根据实际需求对哈希函数进行精细调整,以满足特定要求;可预测性,相比随机数法,数学分析法的哈希值具有一定的可预测性
    缺点:设计复杂度高,需要设计者对数据特征和哈希表特点有深入理解,才能设计出高效的数学分析法哈希函数;哈希冲突风险,不当的数学分析法设计可能导致哈希冲突的增加
    适用场景:定制化需求;对哈希值具有一定可预测性要求;充分理解数据特征的场景

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

3.哈希冲突

哈希冲突是多个关键字通过哈希函数映射得到的哈希值相同导致的。

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

3.1闭散列

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

3.1.1线性探测法

从发生冲突的位置开始,依次向后探测,直到找到一个为空的位置为止。
(1)插入:
在这里插入图片描述
假设这个数组是空的,容量为9,哈希函数是除留余数法
此时,有一个key为3的数据要插入,根据除留余数法,hashi=3%9=3,即映射到下标为3的位置
之后,又有一个key为30的数据要插入,根据除留余数法,hashi=30%9=3,即映射到下标为3的位置,可是下标为3的空间已经被key为3的数据给占了,那怎么办呢?——根据线性探测法,从当前映射位置,向后遍历

while(!Empty(hashtable,hashi))//Empty判断hashtable数组下标hashi的位置是否为空,为空返回true,不为空返回false
{
    hashi++;
    hashi%=9;
}//当循环结束时,就找到了空位置的hashi

当循环结束,key为30的数据就会在当前下标hashi的位置进行存储。
当要寻找某个数据时,也要严格遵守这个操作,即通过哈希函数找到指定下标,如果发生哈希冲突,就往后遍历寻找,直到找到下一个为空的地方(被删除过的位置不算)。
(2)删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,因为在上文,我们说了寻找某个元素的循环终止条件是遇见下一个空,如果所要寻找的值存储在一个被删除的空间后,就会受到影响。
因此,线性探测法采用标记的伪删除法来删除一个元素。

//创建一个枚举变量,包含3种状态,使数组存储的元素和枚举变量进行组合,对数组的每一个空间进行描述
enum State
{
	EXIST,//表示该位置已有数据
	DELETE,//表示该位置之前有数据,之后被删除
	EMPTY//表示该位置没有过数据,为空
}

模拟实现线性探测法(源码):

#include<iostream>
#include<vector>
using namespace std;
//定义枚举常量
enum status
{
	EXIST,
	DELETE,
	EMPTY
};

//哈希结点
template<class k, class v>
struct hashdata
{
	pair<k, v> _data;
	status _status = EMPTY;
};

//哈希仿函数
template<class k>
struct hashfunc
{
	size_t operator()(const k& key)
	{
		return (size_t)key;
	}
};

//使用除留余数法进行映射的哈希表
template<class k, class v, class hash = hashfunc<k>>
class hashtable
{
public:
	typedef hashdata<k, v> node;

	hashtable(size_t size = 10)
	{
		_ht.resize(size);
	}

	node* find(const k& key)
	{
		hash hs;
		size_t hashi = hs(key) % _ht.size();
		//在遇见空结点之前一直寻找
		while (_ht[hashi]._status != EMPTY)
		{
			if (_ht[hashi]._data.first == key && _ht[hashi]._status == EXIST)
				return &_ht[hashi];
			hashi++;
			hashi %= _ht.size();
		}
		//没找到就返回空
		return nullptr;
	}

	bool insert(const pair<k, v>& kv)
	{
		//如果已经存在,则无法插入
		if (find(kv.first))
			return false;
		//如果负载因子超过0.7要进行扩容
		if (_size * 10 / _ht.size() >= 7)
		{
			//创一个新的哈希表,并扩容为原来的两倍
			hashtable<k, v, hash> newht(_ht.size() * 2);
			//遍历原哈希表
			for (auto& e : _ht)
			{
				//把旧哈希表的值重新映射到新哈希表上
				if (e._status == EXIST)
				{
					newht.insert(e._data);
				}
			}
			//把两个哈希表交换
			_ht.swap(newht._ht);
		}
		hash hs;
		//线性探测
		size_t hashi = hs(kv.first) % _ht.size();
		//找到删除位置或者空位置停下
		while (_ht[hashi]._status == EXIST)
		{
			hashi++;
			hashi %= _ht.size();
		}
		_ht[hashi]._data = kv;
		_ht[hashi]._status = EXIST;
		_size++;
		return true;
	}

	bool erase(const k& key)
	{
		hash hs;
		size_t hashi = hs(key) % _ht.size();
		while (_ht[hashi]._status != EMPTY)
		{
			if (_ht[hashi]._data.first == key && _ht[hashi]._status ==EXIST)
			{
				_ht[hashi]._status = DELETE;
				return true;
			}
			hashi++;
			hashi %= _ht.size();
		}
		return false;
	}

private:
	vector<node> _ht;
	size_t _size = 0;
};

(3)扩容

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

α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性越大;反之,α越小,表明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数。

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

3.1.2二次探测法

线性探测法的缺陷就是产生冲突的数据堆积在一起,这是因为每次插入的位置都是下一个空位置,所以冲突的数据都是连着插入。
二次探测法在此进行改进,不采取向后挨个挨个遍历式的寻找空位置,当发生哈希冲突时:
hashi=(hashi+(-1)k+1 * i2 ) %m
解释:k=1,2,3,…(探测次数),i=1,2,3,…,每寻找两次,i++,m为表的容量
循环终止:当新的哈希值所在空间的State为EMPTY时停止循环
在这里插入图片描述
数组为空,插入key为11的数据,根据除留余数法,11%10=1,得到哈希值为1,hashtable[1].State == EMPTY,插入成功。
接着插入key为1的数据,根据除留余数法,1%10 = 1,得到哈希值为1,hashtable[1].State == EXIST,发生哈希冲突。
根据二次探测法,hashi=(hashi+(-1)1 * 12)%10=(1+1)%10=2,hashtable[2].State == EMPTY,因此key为1的数据插入到哈希表下标为2的空间。
在这里插入图片描述

3.2开散列

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

3.2.1哈希桶的实现(包含模版和迭代器)

#include<iostream>
#include<vector>
#include<utility>
using namespace std;
template<class T>
//对值转化为size_t
struct HashFunc
{
public:
	size_t operator()(const T& val)
	{
		return (size_t)val;
	}
};

//对string类型进行特化,特化规则是每个字母下标*133+字母ASCLL
template<>
struct HashFunc<string>
{
public:
	size_t operator()(const string& s)
	{
		const char* str = s.c_str();
		unsigned int seed = 131; // 31 131 1313 13131 131313
		unsigned int hash = 0;
		while (*str)
		{
			hash = hash * seed + (*str++);
		}
		return hash;
	}
};

//哈希桶结点
template<class K>
struct HashBucketNode
{
	HashBucketNode(const K& data)
		: _pNext(nullptr), _data(data)
	{}
	HashBucketNode<K>* _pNext;
	K _data;
};

// 为了实现简单,在哈希桶的迭代器类中需要用到hashBucket本身,
template<class K, class V, class KeyOfValue, class HF>
class HashBucket;

// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
template <class K, class V, class KeyOfValue, class HF>
struct HBIterator
{
	typedef HashBucket<K, V, KeyOfValue, HF> HashBucket;
	typedef HashBucketNode<V>* PNode;
	typedef HBIterator<K, V, KeyOfValue, HF> Self;

	HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr)
		:_pNode(pNode)
		, _pHt(pHt)
	{}

	//前置++
	Self& operator++()
	{
		// 当前迭代器所指节点后还有节点时直接取其下一个节点
		if (_pNode->_pNext)
			_pNode = _pNode->_pNext;
		else
		{
			// 找下一个不空的桶,返回该桶中第一个节点
			size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode->_data)) + 1;
			for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
			{
				if (_pNode = _pHt->_table[bucketNo])
					break;
			}
		}
		return *this;
	}

	//后置++
	Self operator++(int)
	{
		PNode node = _pNode;
		if (_pNode->_pNext)
			_pNode = _pNode->_pNext;
		else
		{
			// 找下一个不空的桶,返回该桶中第一个节点
			size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode->_data)) + 1;
			for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
			{
				if (_pNode = _pHt->_ht[bucketNo])
					break;
			}
		}
		return Self(node, _pHt);
	}

	V& operator*()
	{
		return _pNode->_data;
	}

	V* operator->()
	{
		return &(_pNode->_data);
	}

	bool operator==(const Self& it) const
	{
		return _pNode == it._pNode;
	}

	bool operator!=(const Self& it) const
	{
		return !(operator==(it));
	}
	PNode _pNode;
private:             // 当前迭代器关联的节点
	HashBucket* _pHt;// 哈希桶--主要是为了找下一个空桶时候方便
};

// 本文所实现的哈希桶中key是唯一的
template<class K, class V, class KeyOfValue, class HF>
class HashBucket
{
public:
	typedef KeyOfValue KOF;
	typedef HashBucketNode<V> Node;
	typedef Node* PNode;
	typedef HashBucket<K, V, KOF, HF> Self;
	typedef HBIterator<K, V, KOF, HF> iterator;
	friend iterator;
public:
	HashBucket(size_t capacity = 10)
		: _table(GetNextPrime(capacity))//以奇数素组为容量进行扩容
		, _size(0)
	{}

	~HashBucket()
	{
		Clear();
	}

	iterator begin()
	{
		for (int i = 0; i < BucketCount(); i++)
		{
			if (_table[i] != nullptr)
				return iterator(_table[i], this);
		}
		return iterator(nullptr, this);
	}

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

	// 哈希桶中的元素不能重复
	pair<iterator, bool> Insert(const V& data)
	{
		//先Find,如果找到了返回结点,没找到返回空,返回空就可以插入
		KOF kof;
		PNode node = Find(kof(data))._pNode;
		//node不为空,返回结点
		if (node)
			return pair<iterator, bool>(iterator(node, this), false);
		//控制负载因子<0.7
		if (10 * _size / BucketCount() >= 7)
		{
			//扩容
			CheckCapacity();
		}
		//映射
		size_t hashi = HashFunc(kof(data));
		node = new Node(data);
		node->_pNext = _table[hashi];
		_table[hashi] = node;
		_size++;
		return pair<iterator, bool>(iterator(node, this), true);
	}

	// 删除哈希桶中为data的元素(data不会重复)
	iterator Erase(iterator& it)
	{
		//找到了删,没找到返回false
		KOF kof;
		if (Empty())
			return iterator(nullptr, this);
		size_t hashi = HashFunc(kof(*it));
		PNode cur = _table[hashi];
		PNode pre = nullptr;
		while (cur)
		{
			//如果是桶的首节点
			if (cur == _table[hashi])
			{
				_table[hashi] = _table[hashi]->_pNext;
				delete cur;
				return iterator(_table[hashi], this);
			}
			if (cur->_data == data)
			{
				pre->_pNext = cur->_pNext;
				delete cur;
				return iterator(pre->_pNext, this);
			}
			pre = cur;
			cur = cur->_pNext;
		}
		return iterator(nullptr, this);
	}

	//查询操作
	iterator Find(const K& key)
	{
		//Find规则,找到了返回结点,没找到返回空
		//映射桶的位置
		KOF kof;
		size_t hashi = HashFunc(key);
		PNode cur = _table[hashi];
		while (cur)
		{
			if (kof(cur->_data) == key)
				return iterator(cur, this);
			cur = cur->_pNext;
		}
		return iterator(nullptr, this);
	}

	//有效元素个数
	size_t Size()const
	{
		return _size;
	}

	//判空
	bool Empty()const
	{
		return 0 == _size;
	}

	void Clear()
	{
		for (int i = 0; i < BucketCount(); i++)
		{
			PNode cur = _table[i];
			while (cur)
			{
				_table[i] = cur->_pNext;
				delete cur;
				cur = _table[i];
			}
		}
	}

	size_t BucketCount()const
	{
		return _table.capacity();
	}

	void Swap(Self& ht)
	{
		_table.swap(ht._table);
		swap(_size, ht._size);
	}

	size_t BucketSize(const K& key)
	{
		PNode cur = Find(key);
		if (cur == nullptr)
			return 0;
		size_t hashi = HashFunc(key);
		cur = _table[hashi];
		size_t count = 0;
		while (cur)
		{
			count++;
			cur = cur->_pNext;
		}
		return count;
	}

	size_t Count(const K& key)
	{
		KOF kof;
		size_t hashi = HashFunc(key);
		PNode cur = _table[hashi];
		size_t count = 0;
		while (cur)
		{
			if (kof(cur->_data) == key)
				count++;
			cur = cur->_pNext;
		}
		return count;
	}

private:
	size_t HashFunc(const K& key)
	{
		return HF()(key) % _table.capacity();
	}

	void CheckCapacity()
	{
		//开新桶
		KOF kof;
		Self newht(BucketCount());
		//进行重新连接
		for (int i = 0; i < BucketCount(); i++)
		{
			PNode cur = _table[i];
			while (cur)
			{
				//重新映射
				size_t hashi = kof(cur->_data) % newht.BucketCount();
				//原桶头结点指向下一个
				_table[i] = cur->_pNext;
				//当前结点映射链接到新桶上
				cur->_pNext = newht._table[hashi];
				newht._table[hashi] = cur;
				//cur继续遍历原桶
				cur = _table[i];
			}
		}
		//交换
		Swap(newht);
	}
    //以质数集合为容量进行扩容
	size_t GetNextPrime(size_t capacity)
	{
		const size_t arr[] = { 53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul,
			786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul,25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul,
			1610612741ul, 3221225473ul, 4294967291ul
		};
		for (int i = 0; i < sizeof(arr) / sizeof(size_t); i++)
		{
			if (capacity < arr[i])
				return arr[i];
		}
	}
private:
	vector<PNode> _table;
	size_t _size;      // 哈希表中有效元素的个数
};

4.unordered_map和unordered_set

unordered_map和unordered_set是C++11标准引入的,属于C++标准库中的无序关联容器(Unordered Containers)。它们提供了基于哈希表的数据存储结构,用于实现快速的查找、插入和删除操作。

4.1unordered_map的模拟实现

//底层的哈希表用的是上文实现的哈希桶结构
template<class K, class V, class HF = HashFunc<K>>//V对于map来说是第二种类型
class unordered_map
{
	struct MapOfValue
	{
		const K& operator()(const pair<K, V>& data)
		{
			return data.first;
		}
	};
	// 通过key获取value的操作
	typedef HashBucket<K, pair<K, V>, MapOfValue, HF> HT;
public:
	//对于HT和iterator,pair<K,V>是他们的V,而pair的second是map的V
	typedef typename HT::iterator iterator;
public:
	unordered_map(HT ht = HT())
		:_ht(ht)
	{}
	
	iterator begin() { return _ht.begin(); }
	iterator end() { return _ht.end(); }
	
	// capacity
	size_t size()const { return _ht.size(); }
	bool empty()const { return _ht.empty(); }
	
	// Acess
	V& operator[](const K& key)
	{
		pair<iterator, bool> ret = _ht.Insert(pair<K, V>(key, V()));
		return (*(ret.first)).second;
	}

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

	
	// lookup
	iterator find(const K& key) { return _ht.Find(key); }

	size_t count(const K& key) { return _ht.Count(key); }
	
	// modify
	pair<iterator, bool> insert(const pair<K, V>& value)
	{
		return _ht.Insert(value);
	}

	iterator erase(iterator position)
	{
		return _ht.Erase(position);
	}
	
	// bucket
	size_t bucket_count() { return _ht.BucketCount(); }
	size_t bucket_size(const K& key) { return _ht.BucketSize(key); }

private:
	HT _ht;
};

4.2unordered_set的模拟实现

//底层的哈希表用的是上文实现的哈希桶结构
template<class K, class HF = HashFunc<K>>
class unordered_set
{

	// 通过key获取value的操作
	struct SetOfValue
	{
		const K& operator()(const K& data)
		{
			return data;
		}
	};
	typedef HashBucket<K, K, SetOfValue, HF> HT;
public:
	typename typedef HT::iterator iterator;
public:
	unordered_set() : _ht()
	{}
	
	iterator begin() { return _ht.begin(); }
	iterator end() { return _ht.end(); }
	
	// capacity
	size_t size()const { return _ht.size(); }
	bool empty()const { return _ht.empty(); }
	
	// lookup
	iterator find(const K& key) { return _ht.Find(key); }
	size_t count(const K& key) { return _ht.Count(key); }
	
	// modify
	pair<iterator, bool> insert(const K& valye)
	{
		return _ht.Insert(valye);
	}

	iterator erase(iterator position)
	{
		return _ht.Erase(position);
	}

	// bucket
	size_t bucket_count() { return _ht.BucketCount(); }
	size_t bucket_size(const K& key) { return _ht.BucketSize(key); }
private:
	HT _ht;
};
  • 49
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值