哈希与哈希冲突

哈希本质上是一种映射关系,利用映射关系进行快速查找,它的查找效率可以到O(1)的程度,因此在c++11中引进了unordered_set和unordered_map来进行使用,因为在大数据下,map和set的查找效率不是很高,所以引进了以哈希为底层结构的关联容器。

怎么理解哈希的映射呢?哈希的底层是数组,因此其实更多的是对于数组能够随机访问的支持,将数据以数组下标的形式进行一一对应,那么直接用数组下标进行访问数据,查找的效率岂能不快?

但是上述做法是名为直接定制法的哈希函数,一般我们都是使用除留余数法来进行映射的,很明显的意思,就是一个数据的余数是多少,那我们就放到相应的位置去。这里的除数一般都是数组的大小。

很明显,如果使用直接定制法这样,那空间的浪费肯定很大,要存一个一亿和一,就要开一亿个空间,这样的开销谁都受不了,所以一般都会使用除留余数法,并且在不同的场景里,也有不同的哈希函数,不只是这两种。

但是如果要节省空间,那么一定会产生一种情况,多个值映射到了一个位置上,这种情况称之为哈希冲突,而哈希的最大问题就是如何去解决哈希冲突。

常用两种方法:闭散列,开散列

闭散列又称开放寻址法,思想很简单,找到映射的位置,然后如果有位置,那就往后走,直到找到一个空的位置,放入,简单来说,就是位置如果有人了,就直接往后走,直到找到没人的空间,住下来,抢了别人的位置,然后找的时候,就顺着找,直到为空停止,因为如果有空,数据肯定占了,因此到空还没就是没这个数据。

这种实现方式有个问题是,如果说我将4和14和104这三个都是除10余4的数放入,然后发生哈希冲突,顺位放置,那么如果我将14删除了,那么到14后空了,就等于结束查找了,但是104是存在的,因此我们需要将删除的空两种状态给区分了。

#pragma once
#include <iostream>
#include <vector>

using namespace std;


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

enum State
{
	EMPTY,
	EXITS,
	DELETE,
};

template <class T>
struct HashData
{
	T _data;
	State _state;
};

template <class K,class T,class KOfT>
class HashTable
{
	typedef HashData<T> HashData;
public:
	bool insert(const T& d)
	{
		KOfT koft;
		
		
		if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7)
		{
			//第一种方法扩容
			vector<HashData> newtables;
			size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			newtables.resize(newsize);

			for (int i = 0; i < tables.size(); i++)
			{
				if (_tables[i]._state == EXITS)
				{
					int index = koft(_tables[i]._data) % newsize;

					while (newtables[index]._state == EXITS)
					{
						++index;

						if (index == newtables.size())
						{
							index = 0;
						}
					}

					newtables[index] = tables[i];
				}
			}


			//第二种方法
			HashTable<K, T, KOfT> ht;
			size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;

			ht._tables.resize(newsize);

			for (int i = 0; i < tables.size(); i++)
			{
				if(_tables[i]._state == EXITS)
				ht.insert(_tables[i]._data);
			}



			_tables.swap(newtables);
		}


		//线性探测
		size_t index = koft(d) % _tables.size();

		while (_tables[index]._state == EXITS)
		{
			if (koft(_tables[index]._data) == koft(d))
			{
				return false;
			}

			++index;

			if (index == _tables.size())
			{
				index = 0;
			}
		}

		_tables[index]._data = d;
		_tables[index]._state = EXITS;
		_num++;


		//二次探测
		size_t start = koft(d) % _tables.size();
		size_t index = start;
		int i = 1;

		while (_tables[index]._state == EXITS)
		{
			if (koft(_tables[index]._data) == koft(d))
			{
				return false;
			}

			index = start + i * i;
			i++;

			index %= _tables.size();
		}


		_tables[index]._data = d;
		_tables[index]._state = EXITS;
		_num++;
		return true;
	}

	HashData* find(const K& key)
	{
		KOfT koft;

		size_t index = key % _tables.size();

		while (_tables[index]._state != EMPTY)
		{
			if (koft(_tables[index]._data) == key)
			{
				if (_tables[index]._state == EXITS)
				{
					return &_tables[index];
				}
				else if (_tables[index]._state == DELETE)
				{
					return nullptr;
				}
			}

			index++;
			if (index == _tables.size())
			{
				index = 0;
			}
		}
	}

	bool erase(const K& key)
	{
		HashData* ret = find(key);

		if (ret)
		{
			ret->_state = DELETE;
			return true;
		}
		else
		{
			return false;
		}
	}

private:
	vector<HashData>  _tables;
	size_t _num = 0;
};

以上为代码的实现,用闭散列的方式来处理哈希冲突,这里有几点细节要提。
第一,一般来说我们都是用key来进行比较的,所以制作一个仿函数,让参数无论传什么类型的,都可以正确识别。
第二,除了顺着放的线性探测方式,还有以二次方的距离的二次探测方式,线性探测相对来说,会将哈希冲突聚集在一块,造成这一块的冲突加剧,而二次探测可以使冲突更加分散。
第三,当数组越满的时候,冲突的几率越大,效率越低,因此我们会用负载因子来衡量表的满的程度,为数据除以容量,一般来说0.7是扩容的界限,但是同时也不能使得负载因子太小,因为这样空间也会浪费。
第四,扩容时候的拷贝,是将其存在的数据拷过来,给出了三种状态,存在,删除,空,利用这三种状态进行查找删除。

里面实现的细节方面具体阅读代码进行思考吧~

但是一般我们不怎么使用闭散列来进行处理,因为这种处理方式不高效,所以我们一般使用开散列来处理。

开散列又称拉链法,是将数组中每个元素存成一个链表的头节点,然后如果冲突就遍历链表找,这样子灵活且高效,而且扩容压力不是很大,一般来说负载因子为一的时候才会扩容。而每个子集也称为哈希桶。

本质上,开散列的方式就是将数组的随机访问,和链表的增容的灵活性结合起来,就是链表和数组的结合结构,但是如果说就是一个链表中挂了很多个数据,导致冲突很大,那么将链表换成红黑树,则是对这种情况的进一步优化,java中的hashmap就是这么做的,在链表数据超过8个的情况下,就会改挂成红黑树。

其实哈希的本质是,将数据的特性聚集在一起,来进行简化的搜索,对数据的特性化处理是哈希本质的思想,随便将具有某些相似的数据放在一起,这样的处理方式很值得思考与学习。

#pragma once
#include <vector>
using namespace std;


template <class T>
struct HashNode
{
	T _data;
	HashNode<T>* _next;

	HashNode(const T& data)
		:_data(data)
		,_next(nullptr)
	{}
};

template<class K,class T,class KOfT>
class HashTable
{
	typedef HashNode<T> Node;
public:
	bool insert(const T& d)
	{
		KOfT koft;

		if (_num == _tables.size())
		{
			vector<Node*> newtables;
			size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			newtables.resize(newsize);

			for (int i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];

				while (cur)
				{
					Node* next = cur->_next;
					int index = koft(cur->_data) % newtables.size();
					cur->_next = newtables[index];
					newtables[index] = cur;
					
					cur = next;
				}

				_tables[i] == nullptr;
			}
			_tables.swap(newtables);
		}




		size_t index = koft(d) % _tables.size();

		Node* cur = _tables[index];
		
		while (cur)
		{
			if (koft(cur->_data) == koft(d))
			{
				return false;
			}
			else
			{
				cur = cur->_next;
			}
		}

		Node* newnode = new Node(d);
		newnode->_next = _tables[index];
		_tables[index] = newnode;

		_num++;

		return true;

	}

	Node* find(const K& key)
	{
		KOfT koft;
		size_t index = key % _tables.size();
		Node* cur = _tables[index];

		while (cur)
		{
			if (koft(cur->_data) == key)
			{
				return cur;
			}
			else
			{
				cur = cur->_next;
			}
		}

		return nullptr;
	}

	bool erase(const K& key)
	{
		KOfT koft;
		size_t index = key % _tables.size();
		Node* cur = _tables[index];
		Node* prev = nullptr;

		while (cur)
		{
			if (koft(cur->_data) == key)
			{
				if (prev == nullptr)
				{
					_tables[index] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}


		return false;
	}

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

开散列的实现方式,只是将数组中的数据换成了链表,本质的实现方式没有什么区别,但是注意插入数据是链表插入。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值