哈希表(闭散列)的实现

目录

概念及定义

闭散列的介绍

闭散列底层实现

哈希表的定义

哈希表的构造

哈希表扩容

哈希表插入

哈希表删除

哈希表查找


概念及定义

哈希表,也成为散列表,在C++中unordered_set和unordered_map的底层实现依靠的都是哈希表。

map和set的底层是红黑树。unordered_set和unordered_map的底层是哈希表,unordered说明其数据的存储时没有顺序的。

哈希表是通过将插入数据的大小和位置建立联系,从而缩短查找数据的时间。

哈希表在进行数据的删除,查找,插入的时候不需要进行循环,其时间复杂度平均是O(1)。但是实际在使用的时候,其效率与红黑树差不多。

常见的哈希函数有:直接定值法,除留余数法,平方取中法....

此篇博客将会采用除留余数法进行哈希表的实现。

哈希表底层使用数组实现,除留余数法:将插入的整数模上数组大小得到的余数就是其对应的下标位置。

哈希冲突:对于哈希表来说必定会出现不同的数据映射到相同位置,值和位置就出现了多对一的关系,将这种现象称为哈希冲突。

解决哈希冲突的方法:1)闭散列(线性探测,二次探测);2)开散列(链地址法);3)多次散列。

此篇文章将对闭散列的实现方式进行分析。

闭散列的介绍

闭散列,也叫开放定值法,当出现哈希冲突的时候,即需要插入数据的位置被占用时,按规则去找下一个空位置,即去占别人的位置。

可以看到插入44的时候,下标为4的位置已经被占的,所以先后找空位置进行插入。

闭散列底层实现

依据除留余数法来确定数据的地址,再用开放定址法解决哈希冲突。

通过除留余数法得出数据对应的插入位置,如果该位置已经被占用,则先后找,直到找到空位置后停止。同理在查找数据是否存在的时候,也是先找到对应位置,如果不是再向后查找,直到找到或到空位置停止。

思考:在查找数据时,如果一个数据被删除,则该位置是空,那还继续向后查找吗???

如图,当将5删除后,查找44时,到5位置为空就停止了,不再继续往后走了。这是不符合预期的,所以对每个数据要有三个状态:存在数据,不存在数据,数据被删除。

哈希表的定义

哈希表底层的数组,此处直接使用vector来代替。

enum STATE
{
	EXIST,    //存在数据
	DELETE,   //数据被删除
	EMPTY    //不存在数据
};

template<class K,class V>
struct HashDate
{
	pair<K, V> _kv;   //存放数据
	STATE _state;      //存储状态
};

template<class K, class V>
class HashTable
{
	typedef HashDate<K, V> HashDate;


private:
	vector<HashDate> _table;
	size_t _n;    //存储数组中有效个数
};

哈希表的构造

使用vector的resize函数为vector开初始空间;实现HashDate的构造,在每次插入新数据的时候,其HashDate的状态都是EXIST存在。


template<class K,class V>
struct HashDate
{
    //默认构造函数
    HashDate()
	    :_state(EMPTY)
    { }

	HashDate(const pair<K,V>& kv)   //构造函数
		:_kv(kv)
		,_state(EXIST)
	{ }
}


template<class K, class V>
class HashTable
{
	typedef HashDate<K, V> HashDate;
public:
	HashTable()
	{
		_table.resize(10);   //vector初始有10个数组空间
		_n = 0;
	}
}

哈希表扩容

当对哈希表插入数据,发生哈希冲突时,要向后找空位置进行插入。所以如果数组空间比较满就会出现一直向后找空位置的现象,这也导致插入效率降低。

所以哈希表不能太满,太满会导致查找和插入的效率降低。当哈希表满时,需要对哈希表进行及时扩容。

哈希表中引入载荷因子来控制哈希表是否扩容。

载荷因子 = 填入数据/哈希表的长度。

对于开放开放定址法:载荷因子需要控制在0.7~0.8左右。此处在代码实现时,我们会严格控制在0.7以下。

当对哈希表进行扩容时,也就意味着数据的映射位置会发生改变。原本冲突的数据可能扩容后不再冲突,原本不冲突的数据扩容后可能会出现冲突。所以在每次扩容时,需要对数据进行重新插入。

//对哈希表进行扩容
void More()
{ 
	//此处需要强转为double类型,否则无法得到小数
	if ((double)_n / _table.size() >= 0.7)
	{
		//进行扩容
		//采用创建新的哈希表的方法
		HashTable newtable;
		newtable._table.resize(_table.size() * 2);  //扩容时,直接扩为原数据的2倍
		//将数据进行插入
		for(auto e:_table)
		{
			newtable.Insert(e);
		}
		//将两个数组进行交换
		_table.swap(newtable._table);
	}
}

哈希表插入

bool Insert(const pair<K, V> kv)
{
	//判断是否需要扩容
	More();

	size_t hashi = kv.first % _table.size();
	while (_table[hashi]._state == EXIST)
	{
		if (_table[hashi]._kv.first == kv.first)
		{
			//哈希表中已经存在,不能再插入
			return false;
		}
		++hashi;
		hashi = hashi % _table.size();
	}
	//找到空位置,进行插入
	_table[hashi] = kv;
    _n++;
	return true;
}

哈希表删除

哈希表的删除:在找到数据后,世界将该数据的状态改变即可,将EXIST改为DELETE。

//删除
bool Erase(const K& key)
{
	size_t hashi = key % _table.size();
	while (_table[hashi] == EXIST)
	{
		if (_table[hashi]._kv.first == key)
		{
			//找到了,将该位置进行删除,直接改变状态即可
			_table[hashi]._state = DELETE;
			return true;
		}

		++hashi;
		hashi = hashi % _table.size();
	}
	//没找到
	return false;
}

哈希表查找

根据开头分析:哈希表的查找,在遇到DELETE和EXIST状态的数据时,均不停止,在遇到EMPTY状态的时候才会停止。

bool Find(const K& key)
{
	size_t hashi = key % _table.size();
	while (_table[hashi]._state != EMPTY)
	{
		if (_table[hashi]._kv.first == key && _table[hashi]._state == EXIST)
		{
			return true;
		}
		++hashi;
		hashi = hashi % _table.size();
	}
	//没找到
	return false;
}

到此哈希表的基本功能已经实现,但是会发现:但是存储string类型的时候,string是不能取模的,关于该问题将会在后面《哈希表的封装》中进行详细分析。

评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半桔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值