哈希表的实现(下)

目录

前言

 开散列概念

开散列实现

Insert

优化

 Find

Erase 


前言

上一章节我们用闭散列实现了一下哈希表,但存在一些问题,比如空间浪费比较严重,如果连续一段空间都已经存放值,那么在此位置插入新值的时候就会一直挪动,时间浪费也比较严重,当然可以通过二次探测来解决此问题,但今天我们来讲解一下更优秀的解决方案。

 开散列概念

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

像这样,我们在插入的时候,遇到哈希冲突,不用将它向后移动找到空位置,而是通过链表的形式,将他们一一连接起来,从而达到不影响其他位置的效果。开散列中每个桶中放的都是发生哈希冲突的元素。这样就能有效解决查找和插入的问题。

开散列实现

开散列的实现需要一个指针数组,通过这个数组将插入值进行链接,就像上面的图片。

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

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

首先先定义一个节点,节点里面包含下一个位置的指针,一个_kv,接下来开始实现。

	template<class K, class V>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_tables.resize(10);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

	private:
		vector<Node*> _tables;
		size_t _n = 0;
	};

在HashTable中定义一个指针数组vector<Node*> _tables,统计插入个数_n,在构造函数中先开10大小的空间,析构函数中一次遍历该数组,如果不为空,依次进行delete,直到为空为止,然后将该数组位置置为nullptr。

Insert

在实现成基本结构后,我们来进行insert的实现。再拿出这个图来鞭尸。

 当插入15时,我们找到5的位置,之后在此位置进行头插操作,即可将15链接在这个桶上。

同样和闭散列一样,这种方式也需要考虑扩容的情况,但闭散列的空间利用率较低,负载因子控制在0.7左右,而开散列这种方式可以设置为1。

实现insert的方式有两种,大家可以先自己看一下哪一个更优秀。

1:

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

			// 负载因子最大到1
			if (_n == _tables.size())
			{
				size_t newSize = _tables.size() * 2;
				HashTable<K, V> newHT;
				newHT._tables.resize(newSize);
				// 遍历旧表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while(cur)
					{
						newHT.Insert(cur->_kv);
						cur = cur->_next;
					}
				}

				_tables.swap(newHT._tables);
			}

			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);

			// 头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;

			return true;
		}

2:


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

			if (_n == _tables.size())
			{
				vector<Node*> newTables;
				newTables.resize(_tables.size() * 2, nullptr);
				// 遍历旧表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while(cur)
					{
						Node* next = cur->_next;

						// 挪动到映射的新表
						size_t hashi = cur->_kv.first % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}

					_tables[i] = nullptr;
				}

				_tables.swap(newTables);
			}

			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);

			// 头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;

			return true;
		}

 第一种方法用了闭散列实现时的方式,直接复用一下insert,这样完成遍历后就可以得到扩容后的哈希表,第二种方法选择直接进行链接,然后将完成链接后的位置置空,同样也可以完成扩容操作。

那么哪种情况会更好呢?答案是第二种,因为我们发现insert会new一个新节点然后再将其链接到哈希表中,而第二种情况是直接将现成的节点插入到扩容后的哈希表中,减少了空间的浪费,所以这种方法更优。

在第二种方法中,我们首先定义一个新的指针数组,然后依次遍历旧表,将旧表中的值重新通过新的哈希函数进行转换,得到的位置就是新插入的位置,然后将它头插到表中即可,再将旧表位置置为空,完成便利后,我们只需要交换新旧表,这样_tables就是扩完容后的哈希表。

优化

通常,我们的K为整形,但K为string时就不能转换为关键字,这时就要用一个仿函数来进行完善。

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

// 11:46继续
//HashFunc<string>
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		// BKDR
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}

		cout << key << ":" << hash << endl;
		return hash;
	}
};

 我们特化了一个仿函数,当K为string时,直接调用下面的仿函数,里面用到了BKDR算法,它能够将字符串转化为关键字,并且尽可能的避免了哈希冲突的情况,这种方法被广泛应用。这样我们只需要在哈希表中增加一个模板即可完成string这种情况下转为关键字的情况。

template<class K, class V, class Hash = HashFunc<K>>

 Find

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

				cur = cur->_next;
			}

			return NULL;
		}

将要寻找的key转换为关键字,该关键字可以映射哈希表中的一个位置,这时找到该位置,依次遍历该链表,若能找到返回该节点指针,若到空还是不能找到,返回空。

Erase 

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

					return true;
				}

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

			return false;
		}

 因为我们的节点里面只有next的指针,是一个单项链表,所以我们在删除时要定义一个prev来指向前一个节点的位置。和前面类似,先通过哈希函数进行转换,转换为关键字后找到哈希表中对应的位置,进行查找,则会出现三种情况:

1.找到了该位置,前驱节点prev为空,说明该节点就是头节点,只需要将哈希表中该位置链接到cur的next节点即可。

2.找到了该位置,前驱节点prev不为空,说明前面还有值,那么将prev的next更新为cur的next

即可完成链接。

3.循环结束后还没返回,说明没有此值。

erase还可以先用find查询一下是否存在,若存在继续进行删除操作,不存在直接返回false。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值