哈希表概念及简单实现

目录

1 什么是哈希表

2 哈希冲突

2.1 冲突解决

2.1.1 重新设计哈希函数

2.1.2 闭散列(开放地址法)

 2.1.3 开散列(拉链法)

2.2 哈希表扩容

3 代码实现

3.1闭散列

3.1.1 查找

3.1.2 插入

3.1.3 删除

3.2 开散列

3.2.1 查找

3.2.2 插入

3.2.3 删除


1 什么是哈希表

哈希表的数据结构是一个顺序表,在存储内容时通过数组下标与数据值一一映射的方式记录。这里的一一映射关系可以用函数来表示,这样的函数叫做哈希转换函数。


2 哈希冲突

        从上可以看出由于可能出现多个值映射到顺序表的同一个位置,这样就导致该数组存储数据位置发生冲突,这样的情况称为哈希冲突。

2.1 冲突解决

2.1.1 重新设计哈希函数

        哈希函数包括:直接定制法、 除留余数法等多种方法,其实都是根据需要对映射关系作规定,设计一个好的哈希函数可减少哈希冲突。

2.1.2 闭散列(开放地址法)

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

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

        如下图由于新插入44,按除留余数法映射位置在下标4位置,由于4位置已有数据,就逐个向后探测,一直到下标8位置有了空位就插入数组。

        二次探测:从发生冲突的位置开始,按二次函数的序列增加(i*i,i=1,2,3,4....)向后探测,直到寻找到下一个空位置为止。

        举个例子如下,14映射下标位置为4,同样也有位置冲突。第一次i=1,增加i*i=1个位置,即下标5的位置,再次发生冲突,继续向后探测。这时i=2,i*i=4,向后4个位置在下标8的位置,该位置没有数据则插入成功。

 2.1.3 开散列(拉链法)

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

2.2 哈希表扩容

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

        对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-0. 8以下。超过0. 8,查表时的CPU缓存不命中(cachemissing)按照指数曲线上升。在载荷因子过高时应对哈希表扩容。

        对于拉链法,最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。


3 代码实现

3.1闭散列

        该哈希表的实现,哈希函数使用除留余数法,探测方法为一次探测。载荷因子阈值为0.7,使用vector作为数据结构,初始容量设置为10。

        字符串类型由于不是一个整数无法使用除留余数法映射到数组上,于是使用Hashfunc仿函数处理。在哈希表中的数组每个位置设置三个状态(空、已删除、满)方便后面的插入删除等实现。私有成员变量不是指哈希表的大小,而是表中的有效数据个数,哈希表的大小使用_ht.size()来取到。

#pragma once
#include <vector>
#include <string>
namespace Close_Hash
{
	template<class K>
	struct Hash
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};
	template<>
	struct Hash<std::string>
	{
		size_t operator()(const std::string& s)
		{
			size_t res = 0;
			for (auto c : s)
			{
				res *= 31;
				res += c;
			}
			return res;
		}
	};

	enum State { EMPTY, EXIST, DELETE };
	template<class K, class V,class HashFunc = Hash<K>>
	class HashTable
	{
		struct Elem
		{
			std::pair<K, V> _val;
			State _state = EMPTY;
		};
	public:
		// 插入
		bool Insert(const std::pair<K, V>& val);
		// 查找
		std::pair<size_t, bool> Find(const K& key);
		// 删除
		bool Erase(const K& key);
		size_t Size()const
		{
			return _size;
		}
		bool Empty() const
		{
			return _size == 0;
		}
	private:
		std::vector<Elem> _ht;
		size_t _size;
	};
}

3.1.1 查找

        计算出映射值,依照数组存储值和状态值按照探测方式向后查找,while循环中的条件不能是_ht[index]._state == EXIT,这样就会在状态是DELETE数据时就跳出循环,然而其实还是要向后寻找的。

		std::pair<size_t, bool> Find(const K& key)
		{
			if (_ht.empty())
				return std::make_pair(0,false);
			Hash<K> hs;
			size_t start = hs(key) % _ht.size();
			size_t i = 0;
			size_t index = start;
			while (_ht[index]._state != EMPTY)
			{
				if (_ht[index]._val.first == key
					&& _ht[index]._state == EXIST)
					return std::make_pair(index, true);
				i++;
				index = start + i;
				index %= _ht.size();
			}
			return std::make_pair(0, false);
		}

3.1.2 插入

        插入中有一个扩容的问题,扩容以后的映射关系发生改变。通过创建临时的哈希表,然后递归调用插入函数,最后将临时的哈希表与当前表交换即可完成哈希表的重新映射。在插入上找到正确的位置插入,同时更新表中有效个数_size即可。

		bool Insert(const std::pair<K, V>& val)
		{
			
			if (Find(val.first).second)
			{
				return false;
			}
			if (_ht.size() == 0 || _size * 10 / _ht.size() > 7)
			{
				//扩容
				int newsize = _ht.size() == 0 ? 10 : _ht.size() * 2;
				HashTable<K, V> newTable;
				newTable._ht.resize(newsize);
				//重新装入
				for (int i = 0; i < _ht.size(); i++)
				{
					if(_ht[i]._state == EXIST)
						newTable.Insert(_ht[i]._val);
				}
				_ht.swap(newTable._ht);
			}
			Hash<K> hs;
			size_t start = hs(val.first) %  _ht.size();
			size_t i = 0;
			size_t index = start;
			while (_ht[index]._state == EXIST)
			{
				i++;
				index = start + i;
				index %= _ht.size();
			}
			//找到空位置了
			_ht[index]._val = val;
			_ht[index]._state = EXIST;
			++_size;
			return true;
		}

3.1.3 删除

        删除就简单了,找到位置将该位置状态置为已删除即可,另外不要忘记更新_size。

		bool Erase(const K& key)
		{
			std::pair<size_t,bool> FindOut = Find(key);
			if (FindOut.second == false)
			{
				return false;
			}
			_ht[FindOut.first]._state = DELETE;
			--_size;
			return true;
		}

3.2 开散列

        开散列中就基本上都是链表的操作了。

#pragma once

#include <vector>
#include <string>
namespace Open_Hash
{
	template<class K>
	struct Hash
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};
	template<>
	struct Hash<std::string>
	{
		size_t operator()(const std::string& s)
		{
			size_t res = 0;
			for (auto c : s)
			{
				res *= 31;
				res += c;
			}
			return res;
		}
	};

	template<class K, class V, class HashFunc = Hash<K>>
	class HashTable
	{
		struct Node
		{
			Node(std::pair<K, V> kv)
				:_kv(kv)
				,next(nullptr)
			{}
			std::pair<K, V> _kv;
			Node* next;
		};
	public:
		// 插入
		bool Insert(const std::pair<K, V>& kv);
		// 查找
		Node* Find(const K& key);
		// 删除
		bool Erase(const K& key);
		size_t Size()const
		{
			return _size;
		}
		bool Empty() const
		{
			return _size == 0;
		}
	private:
		std::vector<Node*> _ht;
		size_t _size = 0;//有效数据的个数
	};
}

3.2.1 查找

        计算映射下标,再从链表头开始查找数据。

		Node* Find(const K& key)
		{
			if (_ht.empty())
				return nullptr;
			Hash<K> hs;
			size_t index = hs(key) % _ht.size();
			Node* cur = _ht[index];
			while (cur)
			{
				if (cur->_kv.first == key)
					return cur;
				cur = cur->next;
			}
			return nullptr;
		}

3.2.2 插入

        由于一个哈希桶中的数据没有顺序要求,使用头插效率高一些。在扩容上使用临时数组,把当前数组上挂的节点一个个头插到临时数组上 ,最后两个数组交换得到需要的数组。

		bool Insert(const std::pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			if (_ht.size() == _size)//负载因子到1
			{
				//扩容
				int newsize = _ht.size() == 0 ? 10 : _ht.size() * 2;
				std::vector<Node*> newTable;
				newTable.resize(newsize);
				//重新装入
				for (int i = 0; i < _ht.size(); i++)
				{
					Node* cur = _ht[i];
					while (cur)
					{
						Node* next = cur->next;
						Hash<K> hs;
						size_t index = hs(_ht[i]->_kv.first) % newTable.size();
						cur->next = newTable[index];
						newTable[index] = cur;

						cur = next;
					}
					_ht[i] = nullptr;
				}
				_ht.swap(newTable);
			}
			Hash<K> hs;
			size_t index = hs(kv.first) % _ht.size();
			Node* newNode = new Node(kv);
			newNode->next = _ht[index];
			_ht[index] = newNode;
			++_size;
			return true;
		}

3.2.3 删除

        找到数据节点直接删除即可,链表操作。

		bool Erase(const K& key)
		{
			if (_ht.empty())
				return false;
			Hash<K> hs;
			size_t index = hs(key) % _ht.size();
			Node* cur = _ht[index];
			Node* pre = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (pre == nullptr)//头删
						_ht[index] = cur->next;
					else
						pre->next = cur->next;
					delete cur;
					--_size;
					return true;
				}
				pre = cur;
				cur = cur->next;
			}
			return false;
		}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值