C++_哈希表的学习

1. unordered系列关联式容器

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

2. unordered_map

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

2.1 unordered_map的接口说明

1. unordered_map的构造
unordered_map
构造不同格式的 unordered_map 对象
2. unordered_map的容量
bool empty() const
检测 unordered_map 是否为空
size_t size() const
获取 unordered_map 的有效元素个数
3. unordered_map的迭代器
begin
返回unordered_map第一个元素的迭代器
end
返回unordered_map最后一个元素下一个位置的迭代器
cbegin
返回 unordered_map 第一个元素的 const 迭代器
cend
返回 unordered_map 最后一个元素下一个位置的 const 迭代器
4. unordered_map的元素访问
operator[]
返回与 key 对应的 value ,没有一个默认值
注意:该函数中实际调用哈希桶的插入操作,用参数 key V() 构造的一个默认值往底层哈希桶中插入,如果key 不在哈希桶中,插入成功,返回 V(); 插入失败,说明 key 已经在哈希桶中,将key 对应的 valu 返回
5. unordered_map的查询
iterator find(const K& key)
返回 key 在哈希桶中的位置
size_t count(const K& key)
返回哈希桶中关键码为 key 的键值对的个数
注意:unordered_map key 是不能重复的,因此 count 函数的返回值最大为 1
6. unordered_map的修改操作
insert
向容器中插入键值对
erase
删除容器中的键值对
void clear()
清空容器中有效元素个数
void swap(unordered_map&)
交换两个容器中的元素
7. unordered_map的桶操作
size_t bucket_count()const
返回哈希桶中桶的总个数
size_t bucket_size(size_t n)const
返回 n 号桶中有效元素的总个数
size_t bucket(const K& key)
返回元素 key 所在的桶号

3. 底层结构

unordered 系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构

3.1 哈希

理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素 如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素,该方式即为哈希( 散列 ) 方法,哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称 为哈希表 (Hash Table)( 或者称散列表 )
哈希函数一般设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小

3.2 哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞 把具有不同关键码而具有相同哈希地址的数据元素称为 同义词”,哈希冲突很大程度上是因为哈希函数设置不够合理

3.3 哈希函数

哈希函数设计原则
1.哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在0 m-1 之间
2.哈希函数计算出来的地址能均匀分布在整个空间中
3.哈希函数应该比较简单
常见哈希函数
1. 直接定址法 --( 常用 )
        取关键字的某个线性函数为散列地址:Hash Key = A*Key + B
        优点:简单、均匀
        缺点:需要事先知道关键字的分布情况
        使用场景:适合查找比较小且连续的情况
2. 除留余数法 --( 常用 )
        设散列表中允许的地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数, 按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
3. 平方取中法
        假设关键字为1234 ,对它平方就是 1522756 ,抽取中间的 3 227 作为哈希地址
        平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4.  折叠法
        折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5. 随机数法
        通常应用于关键字长度不等时采用此法
6. 数学分析法
         数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

3.4 哈希冲突解决

解决哈希冲突 两种常见的方法是: 闭散列 开散列
3.4.1 闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把 key 存放到冲突位置中的 下一个 ” 空位置中去,根据如何寻找下一个位置又分为线性探测和二次探测
1. 线性探测
 从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入:
通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素
删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索 。比如删除元素 4 ,如果直接删除掉, 44 查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
线性探测优点:实现非常简单
线性探测缺点: 一旦发生哈希冲突,所有的冲突连在一起,容易产生数据 堆积 ,即:不同 关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
2. 二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法 为: H_i= (H_0+i^2)%m, 或者: H_i =(H_0- i^2)%m 。其中: i = 1,2,3… $H_0$ 是通过散列函数 Hash(x) 对元素的关键码 key 进行计算得到的位置, m 是表 的大小
研究表明: 当表的长度为质数且表装载因子 a 不超过 0.5 时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子 a 不超过 0.5 如果超出,必须考虑增容。因此,比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
3.4.2 开散列
开散列法:又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中,开散列中每个桶中放的都是发生哈希冲突的元素

4. 模拟实现

这里模拟实现的是开散列形式的哈希表

4.1 哈希表的改造

1. 对unordered_mapunordered_set不同传参的处理
// K:关键码类型
// V: 不同容器V的类型不同
//    如果是unordered_map,V代表一个键值对pair<const K,V>
//    如果是unordered_set,V 为 K
// KeyOfValue: 因为V的类型不同,通过value取key的方式就不同
// HF: 哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能取模,
//不同类型的key,方法不同
template<class K, class V, class KeyOfValue, class HF = HashFunc<K>>
class HashBucket;
2. unordered_map和unordered_set对KeyOfValue的处理
//unordered_set
struct KeyOfValue
{
	const K& operator()(const K& data)
	{
		return data;
	}
};

//unordered_map
struct KeyOfValue
{
	const K& operator()(const pair<K, V>& data)
	{
		return data.first;
	}
};
3. unordered_map和unordered_set对哈希函数HashFunc<K>的处理
//正常模板,能直接算Hashi的类型T
template<class K>
class HashFunc
{
public:
	size_t operator()(const K& val)
	{
		//size_t防止出现负数val
		return (size_t)val;
	}
};

//string特化
template<>
class 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;
			str++;
		}
		return hash;
	}
};
#pragma once
#include<vector>
#include<iostream>
using namespace std;

//正常模板,能直接算Hashi的类型T
template<class K>
class HashFunc
{
public:
	size_t operator()(const K& val)
	{
		//size_t防止出现负数val
		return (size_t)val;
	}
};

//string特化
template<>
class 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;
			str++;
		}
		return hash;
	}
};

namespace OpenHash
{
	template<class T>
	struct HashBucketNode
	{
		HashBucketNode(const T& data)
			:_pNext(nullptr)
			, _data(data)
		{}

		HashBucketNode<T>* _pNext;
		T _data;
	};

	// 声明HashBucket
	template<class K, class T, class KeyOfValue, class HF>
	class HashBucket;

	// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
	template <class K, class T, class KeyOfValue, class HF>
	struct HBIterator
	{
		typedef HashBucket<K, T, KeyOfValue, HF> HashBucket;
		typedef HashBucketNode<T>* PNode;
		typedef HBIterator<K, T, 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 (_pHt->_table[bucketNo])
					{
						_pNode = _pHt->_table[bucketNo];
						break;
					}
				}
				// 后面没有桶了
				if (bucketNo == _pHt->BucketCount())
				{
					_pNode = nullptr;
				}
			}

			return *this;
		}

		//后置++
		Self operator++(int)
		{
			PNode cur = _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->_table[bucketNo])
						break;
				}
			}
			return cur;
		}

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

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

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

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

		// HBIterator内部封装了一个指向HashBucket的指针和HashBucket里的当前结点指针
		PNode _pNode;             // 当前迭代器关联的节点
		HashBucket* _pHt;         // 哈希桶--主要是为了找下一个空桶时候方便
	};

	// 这里实现的哈希桶中key是唯一的
	template<class K, class T, class KeyOfValue, class HF = HashFunc<K>>
	class HashBucket
	{
		template<class K, class T, class KeyOfValue, class Hash>
		friend struct HBIterator;
		typedef HashBucketNode<T> Node;
		typedef Node* PNode;
		typedef HashBucket<K, T, KeyOfValue, HF> Self;

	public:
		typedef HBIterator<K, T, KeyOfValue, HF> iterator;

		HashBucket(size_t capacity=10)
			: _table(capacity), _size(0)
		{}

		~HashBucket()
		{
			Clear();
		}

		// 找到第一个桶的第一个节点
		iterator begin()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i])
				{
					return iterator(_table[i], this);
				}
			}

			return end();
		}

		// 返回以空结点构成的iterator
		iterator end()
		{
			return iterator(nullptr, this);
		}

		// 哈希桶中的元素不能重复
		pair<iterator, bool> Insert(const T& data)
		{
			KeyOfValue kot;
			//Find返回非空指针说明表里已经有data了,不能插入
			iterator found = Find(kot(data));
			if (found!=end())
				return make_pair(found, false);
			CheckCapacity();

			size_t Bucket = HashFunc(kot(data));
			//头插到对应桶中
			Node* cur = _table[Bucket];
			Node* newnode = new Node(data);
			newnode->_pNext = cur;
			_table[Bucket] = newnode;
			_size++;
			return make_pair(iterator(newnode,this), true);
		}

		// 删除哈希桶中为data的元素(data不会重复)
		// 返回指向删除位置后的iterator
		iterator Erase(iterator position)
		{
			KeyOfValue kot;
			size_t Bucket = HashFunc(kot(*position));//*position -> data
			Node* cur = _table[Bucket];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur == position._pNode)
				{
					Node* next = cur->_pNext;
					//如果prev为空,说明该值是桶的第一个结点
					if (prev == nullptr)
						_table[Bucket] = next;
					else
						prev->_pNext = next;

					delete cur;
					_size--;
					return iterator(next);
				}
				prev = cur;
				cur = cur->_pNext;
			}
			return iterator(nullptr);
		}

		bool Erase(const K& key)
		{
			KeyOfValue kot;
			//Find返回空,说明表里没有data,不能删除
			if (Find(key) == nullptr)
				return false;
			size_t Bucket = HashFunc(key);
			Node* cur = _table[Bucket];
			Node* prev = nullptr;
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					//如果prev为空,说明该值是桶的第一个结点
					if (prev == nullptr)
						_table[Bucket] = cur->_pNext;
					else
						prev->_pNext = cur->_pNext;

					delete cur;
					_size--;
					return true;
				}
				prev = cur;
				cur = cur->_pNext;
			}
		}

		iterator Find(const K& key)
		{
			KeyOfValue kot;
			//找到对应的桶
			size_t Bucket = HashFunc(key);
			Node* cur = _table[Bucket];

			//遍历这个桶
			while (cur)
			{
				//找到就返回该指针
				if (kot(cur->_data) == key)
					return iterator(cur, this);
				cur = cur->_pNext;
			}
			//该桶为空或桶里找不到就返回空
			return iterator(nullptr, this);
		}

		size_t Count(const K& key)
		{
			if (Find(key))
				return 1;
			else
				return 0;
		}

		size_t Size()const
		{
			return _size;
		}

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

		void Clear()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				//遍历每一个非空桶,并把该桶中的每一个非空结点释放
				while (cur)
				{
					//保存下一个结点
					Node* nextcur = cur->_pNext;
					delete cur;
					cur = nextcur;
				}
				_table[i] = nullptr;
			}
		}

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

		size_t BucketSize(const K& key)const
		{
			size_t Bucket = HashFunc(key);
			Node* cur = _table[Bucket];
			size_t Bucketsize = 0;
			while (cur)
			{
				Bucketsize++;
				cur = cur->_pNext;
			}
			return Bucketsize;
		}

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

	private:
		//class HF = HashFunc<V> 套了两层
		size_t HashFunc(const K& key)
		{
			return HF()(key) % _table.size();
		}

		void CheckCapacity()
		{
			KeyOfValue kot;
			//负载因子到1就扩容
			if (_size == _table.size())
			{
				//创建新的_table,并把之前的数据给新表
				vector<Node*> new_table(_table.size() * 2, nullptr);
				//遍历每一个桶,是空桶就跳过,不是空再遍历这个桶
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					//遍历每一个非空桶,并把该桶中的每一个非空结点头插到新表中
					while (cur)
					{
						//保存下一个结点
						Node* nextcur = cur->_pNext;
						/*size_t hashi = cur->_data % new_table.size();*/
						size_t hashi = HashFunc(kot(cur->_data));
						cur->_pNext = new_table[hashi];
						new_table[hashi] = cur;
						cur = nextcur;
					}
				}
				_table.swap(new_table);
			}
		}

	private:
		vector<Node*> _table;
		size_t _size;      // 哈希表中有效元素的个数
	};
}

4.2 unordered_map的模拟实现

#pragma once
#include"HashTable.h"

namespace bit
{
	// unordered_map中存储的是pair<K, V>的键值对,K为key的类型,V为value的类型,HF哈希函数类型
	// unordered_map在实现时,只需将hashbucket中的接口重新封装即可
	template<class K, class V, class HF= HashFunc<K>>
	class unordered_map
	{
		// 通过key获取value的操作
		struct KeyOfValue
		{
			const K& operator()(const pair<K, V>& data)
			{
				return data.first;
			}
		};

	public:
		typedef typename OpenHash::HashBucket<K, pair<const K, V>, KeyOfValue, HF> HT;
		typedef typename HT::iterator iterator;
	public:
		unordered_map() : _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(make_pair(key, V()));
			return ret.fisrt->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>& 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;
	};

}

4.3 unordered_set的模拟实现

#pragma once
#include"HashTable.h"

namespace bit
{
	// unordered_set中存储的是K类型,HF哈希函数类型
	// unordered_set在实现时,只需将hashbucket中的接口重新封装即可
	template<class K, class HF = HashFunc<K>>
	class unordered_set
	{
		// 通过key获取value的操作
		struct KeyOfValue
		{
			const K& operator()(const K& data)
			{
				return data;
			}
		};

	public:
		//typedef HashBucket<K, K, KeyOfValue, HF> HT;
		//typename typedef HT::Iterator iterator;
		typedef typename OpenHash::HashBucket<K, const K, KeyOfValue, HF> HT;
		typedef typename 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
		iterator erase(iterator position)
		{
			return _ht.Erase(position);
		}

		bool erase(const K& data)
		{
			return _ht.Erase(data);
		}

		// bucket
		size_t bucket_count()
		{
			return _ht.BucketCount();
		}

		size_t bucket_size(const K& key)
		{
			return _ht.BucketSize(key);
		}
	private:
		HT _ht;
	};

}

 

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值