哈希函数、哈希冲突、开闭散列

哈希函数、哈希冲突、开闭散列

在实现哈希表之前需要先了解一些哈希的相关概念,什么是哈希函数?哈希冲突是什么,该怎么解决?开闭散列是啥?

一起往下看吧!

哈希函数

哈希函数指将哈希表中元素的关键键值映射元素存储位置的函数。哈希函数实现了将一个元素映射到一个特定的位置,为后面哈希表的实现打好了基础。

常见的哈希函数

下面介绍5中哈希函数,其中最常用的是直接定址法和除留余数法,那么这里也只重点介绍这两种。

1.直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况

  • 例如:一组数据{2,6,3,7,5,9} 取A=2,B=3;

2映射的位置为7

6映射的位置为15

2.除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

  • 例如:一组数据{2,6,3,15,7,5,12,9}

取p为10,那么2映射的位置为2,6映射的位置为6,3映射位置为3,15映射位置为5,7映射位置为7,5映射位置为5,12映射位置为12,9映射位置为9

由此可见:会出现有不同的数映射到同一个位置,这种现象就是下面介绍的哈希冲突问题了。

3.平方取中法

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4.折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址

  • 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5.随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。
通常应用于关键字长度不等时采用此法

哈希冲突

即使一个元素可以通过哈希函数映射一个位置,但是这个位置不一定就只属于这个元素,也有可能被其他的元素所映射,例如上面的除留余数法中的2 和 12 ,5 和15 都是两个不同的元素映射了同一个位置,这就是哈希冲突。
注意的是哈希冲突不可避免,但是可以优化哈希函数来降低哈希冲突的概率!!!

如何解决哈希冲突

方法一 闭散列

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

其中冲突后寻找新的额空的位置又有两种常见的方式,线性探测法和二次探测法。

线性探测
  • 例如:这里拿除留余数法作为哈希函数来举例

线性探测找空位的公式:hash(key)=key%p+i (i=1,2,3,…) 需要注意的时如果+i处理后hash(key)大于哈希表的长度就会越界所以得模上一个哈希表的长度!!!

在这里插入图片描述

  • 线性探测优点:实现非常简单
  • 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
    关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
    低。如何缓解呢?
  • 用下面的二次探测!!!
二次探测

二次探测找空位的公式:hash(key)=key%p+i*i (i=1,2,3,…) 需要注意的时如果 + i * i 处理后hash(key)大于哈希表的长度就会越界所以得模上一个哈希表的长度!!!

  • 二次探测往后找空位置时按照+i的平方找的,i就是冲突的元素个数,那么冲突的元素所对应的位置就不会是连续拥挤的,会比较分散,找某个冲突元素的时候就不需要判断很多次才能找到!!!

在这里插入图片描述

闭散列的模拟实现

字符串哈希拓展

在这里插入图片描述

闭散列模拟实现:

#define  _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<vector>
using namespace std;


//散列表形式的哈希表实现
enum state
{
	EXIST,
	EMPTY,
	DELETE,
};
template<class K,class V>
struct table_data
{
	table_data(const pair<K,V>& kv = make_pair(K(),V()))
		:_state(EMPTY)
		,_kv(kv)
	{}

	pair<K,V> _kv;
	state _state;

};


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

};

template<>
struct DefaultHash<int>
{
	size_t operator()(const size_t& key)
	{
		return key;
	}
};

//字符串哈希
template<>
struct DefaultHash<string>
{
	//size_t ret = 0;  这个变量不可以写在函数外面  否则就是成员变量了 对象每次调用一次该函数 该成员变量就会变化了 
	//这样就会使得相同的字符串映射的值不同了  必须写到函数里面做局部变量
	size_t operator()(const string& str)
	{
		size_t ret = 0;
		for (auto& e : str)
		{
			ret = ret * 131 + e;
		}
		return ret;
	}
};



template<class K,class V,class HashFunc=DefaultHash<K>>
class HashTable
{
public:
	typedef table_data<K, V> table_data;
	HashFunc HF;
	bool insert(const pair<K, V>& key)
	{
		if (find(key.first) != nullptr)
		{
			return false;
		}
		//扩容
		if (_table.size() == 0 || _size * 10 / _table.size() >= 7)
		{
			size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
			//HashTable<K,V,HashFunc> newHT;
			HashTable newHT;
			newHT._table.resize(newsize);
		
			for (auto& e : _table)
			{
				if(e._state==EXIST)//只有当数据对应的映射点存在的话就插入  空和删除不插入
				newHT.insert(e._kv);
			}
			newHT._table.swap(_table);
		}
		size_t starti = HF(key.first);
		starti %= _table.size();

		size_t hashi = starti;
		size_t i = 1;
		while (_table[hashi]._state == EXIST)//该映射位置被占用 往后找空的位置
		{
			hashi = starti+i;
			i++;
			hashi %= _table.size();
		}
		_table[hashi]._kv = key;
		_table[hashi]._state = EXIST;
		++_size;
		return true;

	}
	
	table_data* find(const K& key)
	{
		if (_table.size() == 0)
			return nullptr;

		size_t starti = HF(key);
		starti %= _table.size();
		//用key来建立映射

		size_t hashi = starti;
		size_t i = 1;
		while (_table[hashi]._state != EMPTY)//如果当前映射位置已经被占了就往后占位  线性寻址法  
		{
			if (_table[hashi]._state != DELETE && _table[hashi]._kv.first == key)
			{
				return &_table[hashi];
			}
			//hashi = hashi + i;
			hashi = starti + i;
			i++;
			hashi %= _table.size();//防止越界访问
		}
		return nullptr;

	}

	bool erase(const K& key)
	{
		table_data* ret = find(key);
		if (ret == nullptr)
			return false;

		ret->_state = DELETE;
		return true;
	}


private:
	vector<table_data> _table;
	size_t _size = 0;
};

void test_closedtable()
{
	HashTable<int, int>  hash;
	hash.insert(make_pair(1,1));
	hash.insert(make_pair(10, 10));
	hash.insert(make_pair(3, 3));
	hash.insert(make_pair(6, 6));
	hash.insert(make_pair(7, 7));
	hash.insert(make_pair(15, 15));
	hash.insert(make_pair(20, 20));
	hash.insert(make_pair(30, 30));
	hash.insert(make_pair(40, 40));
	hash.insert(make_pair(50, 50));

	DefaultHash<string> dfh;
	string str[] = { "apple","pear","orange","banana","watermallon","tamato","apple","apple","alppe","eapr","ranoge" };

	vector<string> v(str, str + 11);
	for (auto& e : v)
	{
		cout << dfh(e) << endl;
	}
	HashTable<string, string> HT;
	for(auto& e:v)
	HT.insert(make_pair(e,e));


}


int main()
{
	test_closedtable();
	return 0;
}

在这里插入图片描述

方法二 开散列

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

在这里插入图片描述

闭散列和开散列的比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:
由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=
0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

所以哈希表使用的是开散列的方式实现的。

本文结束! 下篇文章来模拟实现哈希表

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值