[数据结构] 开散列法 && 闭散列法 模拟实现哈希结构(二)

标题:[数据结构] 开散列法 && 闭散列法 模拟实现哈希结构

个人主页:@水墨不写bug



本文讲解的是实现开散列法的哈希,闭散列法的哈希参考本篇文章:

        《[数据结构] 开散列法 && 闭散列法 模拟实现哈希结构(一)》 

目录

 一、开散列法

数据的存储 

二、开散列的接口设计

1.Find

仿函数的理解

2.Erase

3.Insert

扩容逻辑


正文开始:

 一、开散列法

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

数据的存储 

        开散列法需要我们自己设计一个存储数据的节点:HashNode;节点里面存储数值域和指向下一个节点的指针,具体实现如下:

//实现为单链表的结构
template<class V>
struct HashNode
{
	typedef HashNode<V> Node;
	
	HashNode(const V& data)
		:_data(data)
		,_next(nullptr)
	{}

	V _data;//结构的数值域
	Node* _next;//指向下一个节点的指针
};

        上面的仅仅是链表的一个节点,这些节点可以链接成链表。链表的头节点存储在一个顺序表中,使用vector;则vector里面存储的是节点的指针类型的头节点。

        由于我们在《[数据结构] 开散列法 && 闭散列法 模拟实现哈希结构(一)》 讲解过具体仿函数KeyOfV,Hash的具体作用,这里不再赘述;

哈希表类型的类设置如下:

template<class K,class V,class KeyOfV,class Hash = HashFunc<K>>
class HashTable
{
	typedef HashNode<V> Node;
public:

	//默认构造,开10个存储位置,默认存放10个nullptr
	HashTable()
		:_n(0)
	{
		_table.resize(10,nullptr);
	}

private:
	vector<Node*> _table;
	size_t _n;
};

二、开散列的接口设计

1.Find

     首先求得数据通过映射后在哈希表中属于的桶位置,遍历这个桶(单链表)同时对比key即可,如果找到,返回对应节点的指针;如果找不到,则返回空指针。

//根据key查找
Node* Find(const K& key)
{
	Hash hs;
	KeyOfV kov;
	size_t hashi = hs(key) % _table.size();
	Node* cur = _table[hashi];
	while (cur)
	{
		if (kov(cur->_data) == key)
			return cur;

		cur = cur->_next;
	}
	return nullptr;
}

仿函数的理解

        但是其实重要的还是仿函数hs,kov的理解:

        hs是Hash类实例化的一个对象:Hash类重载了operator()(),所以通过hs()调用就相当于调用Hash类内部的operator()()。

        kov是KeyOfV类实例化的一个对象:KeyOfV上层传下来的一个类类型,其内部也重载了operator()(),通过kov()调用就相当于调用上层(也许是多个不同的类)的类内部的operator()()。

2.Erase

         与闭散列的思路差别较大;首先需要通过映射找到元素在哈希表的对应的桶的位置,接下来遍历这个链表找目标节点即可,如果找到了,直接删除节点并返回true;如果没有找到,则返回false表示删除失败。

//根据key删除
bool Erase(const K& key)
{
	Hash hs;
	KeyOfV kov;
	//找到对应的链表
	size_t hashi = hs(key) % _table.size();
	
	Node* cur = _table[hashi];
	Node* prev = nullptr;
	while (cur)
	{
        //遍历桶(链表)若匹配成功,进入删除逻辑
		if (kov(cur->_data) == key)
		{
			//删除特殊判断,cur如果是第一个节点,则直接让表存储cur下一个节点即可
			if (prev == nullptr)
			{
				_table[hashi] = cur->_next;
			}
			else//否则,prev->next指向cur下一个节点
			{
				prev = cur->_next;
			}

            //删除cur
			delete cur;
			--_n;//哈希表中节点数--

			return true;
		}
        //匹配失败,cur向后移动
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

3.Insert

    基本的插入逻辑比较简单:按照映射找到元素在哈希表中对应的桶,对这个链表进行头插即可:

//插入逻辑
bool Insert(const V& data)
{
	Hash hs;
	KeyOfV kov;

	//如果存在重复元素,则插入失败
	if (Find(kov(data)))
	{
		return false;
	}

    //扩容
    //...


	size_t hashi = hs(kov(data)) % _table.size();
	Node* newNode = new Node(data);
	newNode->_next = _table[hashi];
	_table[hashi] = newNode;
	++_n;

	return true;
}

扩容逻辑

        对于开散列的哈希而言,是靠先寻址,再挂桶的过程插入的。如果一个桶内部的节点过多,也会降低哈希的查找效率,所以开散列的哈希也需要扩容。

        这里我们需要根据哈希结构桶内部节点数的多少来决定扩容的。

        对于理想状况,当每一个哈希地址处刚好挂一个桶时,是最优的结构,这时的查找效率最高。所以根据这样的情景,我们就在哈希内部桶的节点总数等于哈希表长度的时候选择扩容:

        在扩容时,内部节点数是不变的;变的是节点的挂载位置。我们需要new一个新的vector,遍历旧表,同时计算旧表中节点在新表中的位置,将旧表中的数据挂载到新表即可:

//插入逻辑
bool Insert(const V& data)
{
	Hash hs;
	KeyOfV kov;
	//如果存在重复元素,则插入失败
	if (Find(kov(data)))
	{
		return false;
	}
	//扩容逻辑,根据桶内部节点的总数 决定什么时候扩容
	if (_n == _table.size())
	{
		vector<Node*> newTable;
		//两倍容量
		newTable.resize(_table.size()*2,nullptr);

        //遍历旧表
		for (size_t i = 0; i < _table.size(); ++i)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;

				//找到旧表中节点对应新表的位置
				size_t hashi = hs(kov(cur->_data)) % newTable.size();
				//插入新表
				cur->_next = newTable[hashi];
				newTable[hashi] = cur;
				
				cur = next;
			}
			_table[i] = nullptr;//旧表中节点置空
		}
        //交换vector,不必再手动释放旧表
        //旧表的vector被交换到newtable,而newtable是局部变量,出作用域自动销毁
		_table.swap(newTable);
	}

	size_t hashi = hs(kov(data)) % _table.size();

	Node* newNode = new Node(data);
	newNode->_next = _table[hashi];
	_table[hashi] = newNode;
	++_n;
	return true;
}

完~

未经作者同意禁止转载 

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

水墨不写bug

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

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

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

打赏作者

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

抵扣说明:

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

余额充值