【数据结构】-----闭散列、开散列(哈希桶)实现(C++)

目录

闭散列的实现

结构

①枚举状态常量

②位置存储结构

③闭散列表结构 

插入

删除

查找 

 完整实现代码

开散列(哈希桶)的实现

整体结构

插入

查找

删除

析构

开散列与闭散列的比较


闭散列的实现

这里主要实现线性探测解决冲突,采用的哈希函数为除留取余法!

结构

①枚举状态常量

每一个位置除了存储数据以外,还需要存储该位置的状态。

	enum State
	{
		EXIST, //存在数据
		EMPTY, //无数据
		DELETE //原有的数据已被删除
	};

目的:为了能够处理删除和查找的情况。

举个例子:比如我们现在将在下标为1的数字1删除并置为空,那么当我们查找11时,根据哈希函数进行位置映射时,会下标1的位置,而此时的位置被置为空,就不会向后继续查找了,最后返回不存在!可是11被放在了5下标的位置呀!

②位置存储结构

存数据和状态,初始状态默认为空!

	template <class K, class V>
	struct HashDate
	{
		pair<K, V> _kv;
		State _state = EMPTY;//状态
	};

③闭散列表结构 


template <class K, class V,class Hash=HashFunc<K>>
class HashTable
{
private:
	vector<HashDate<K, V>> _t;//表
	size_t _n=0;//有效数据个数;
};

 这里第三个参数是一个仿函数,因为除留取余法需要整数相除算出位置,那么对于string类型,该如何转化为整数?直接强转并不可行(自定义类型和内置类型不能直接强转,需要通过显示转化函数才能进行),这就是这个仿函数存在的意义,该仿函数就是能够将字符串string类转化成整形!但是如果仅仅只给出string的仿函数,那其他类型怎么办?所以这里就需要使用模板以及模板特化

先给出适合除了string类的其他类型的通用模板,在此基础上特化出仅适合string类的模板 

//适合int,int* ,……
template<class K>
struct  HashFuc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//特化出适合string类的类模板
template<>
struct  HashFuc<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto a : s)
		{
			hash *= 131;
			hash += a;
		}

		return hash;
	}
};

值得一提的是这里将string转化为整形采用的方法是将所有字符串中的字符ASCII码值依次相加,在乘上因子131!这是一种字符串哈希算法,当然还有很多可以点击☞字符串哈希算法

插入

这里的逻辑就是:

  • 通过哈希函数获取关键码key的位置
  • 若该位置中没有元素则直接插入新元素,若有发生哈希冲突,使用线性探测找到下一个空位置再放入数据

注意:在哈希一文中提到的数据堆积问题,需要扩容操作,对于闭散列,扩容的条件是:负载因子超过0.7时扩容

这里的扩容核心思路就是开一个比旧表大2倍(不严谨)的新表将旧表内容重新负载到扩容后的新表中,最后让旧表指向新表即可!负载的步骤和插入的类似,都是先算key在新表的位置,在判断是否冲突,所以这里直接采用复用的手段。实现如下

bool Insert(const pair<K, V>& kv)
{
	//不允许重复
	if (Find(kv.first))
		   return false;

	//可能会扩容
	//负载因子在0.7-0.8之间扩容
	if (_n*10 / _t.size() >= 7)
	{
		HashTable<K, V,Hash> newhash;//新的哈希表
		newhash._t.resize(_t.size() * 2);
		//重新建立映射关系,直接复用Insert
		for (size_t i = 0; i < _t.size(); i++)
		{
			if (_t[i]._state == EXIST)
			{
				newhash.Insert(_t[i]._kv);//旧表原本数据到插入新表
			}
		}
 
		_t.swap(newhash._t);//交换
     }

	Hash hs;//仿函数
	//线性探测
	size_t hashi = hs(kv.first) % _t.size();//算出位置
    //找到空位置为止
	while (_t[hashi]._state == EXIST)
	{
		++hashi;
		//防止越界
		hashi %= _t.size();
	}
	++_n;
	_t[hashi]._kv = kv;
	_t[hashi]._state = EXIST;
 
	return true;
}

删除

步骤:先找到该关键码在哈希表中位置,不存在就返回空,删除失败;若存在,就将值对应位置的状态置为delete即可!

一定要注意:这里的删除是伪删除,数据并没有被抹除掉,还在表中,只不过状态置为已删除!!下次插入数据可直接覆盖掉!(原因看上文枚举常量部分!)

		bool Erase(const K& key)
		{
			//找
			HashDate<K, V>* ret = Find(key);
			if (ret == nullptr)
			{
				return false;//找不到就返回空
			}
			else
			{
				ret->_state = DELETE;//状态置为删除
				--_n;
				return true;
			}
		}

查找 

步骤:首先通过传入的关键码key算出位置,从该位置一直先后找,找到就返回对应位置,直到遇到空位置就停止。

需要注意一点细节,因为删除是伪删除,数据还存在表中,需要多加一条判断,即值存在并且位置状态也是存在才可行若不加,可能就是造成即使值已经删除,但是还是能找到的现象!

HashDate<K, V>* Find(const K& key)
{
	Hash hs;
	//找位置
	size_t hashi = hs(key) % _t.size();
	//找到空就停止
	while (_t[hashi]._state != EMPTY)
	{
	    //因为删除是伪删除,所以多加了一条判断
		if (_t[hashi]._state== EXIST && _t[hashi]._kv.first == key)
		{
			return &_t[hashi];
		}
		++hashi;
		hashi %= _t.size();
	}
	return nullptr;
}

 完整实现代码

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

enum State
{
	EXIST, //存在数据
	EMPTY, //无数据
	DELETE //原有的数据已被删除
};

template <class K, class V>
struct HashDate
{
	pair<K, V> _kv;
	State _state = EMPTY;//状态
};

//适合int,int* ,……
template<class K>
struct  HashFuc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//特化出适合string类的类模板
template<>
struct  HashFuc<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto a : s)
		{
			hash *= 131;
			hash += a;
		}

		return hash;
	}
};

template <class K, class V,class Hash=HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_t.resize(10);
		}
		bool Insert(const pair<K, V>& kv)
		{
			//不允许重复
			if (Find(kv.first))
				return false;
			//可能会扩容
			//负载因子在0.7-0.8之间扩容
			if (_n*10 / _t.size() >= 7)
			{
				HashTable<K, V,Hash> newhash;//新的哈希表
				newhash._t.resize(_t.size() * 2);
				//重新建立映射关系,直接复用Insert
				for (size_t i = 0; i < _t.size(); i++)
				{
					if (_t[i]._state == EXIST)
					{
						newhash.Insert(_t[i]._kv);
					}
				}
 
				_t.swap(newhash._t);
			}

			Hash hs;//仿函数
			//线性探测
			size_t hashi = hs(kv.first) % _t.size();
			while (_t[hashi]._state == EXIST)
			{
				++hashi;
				//防止越界
				hashi %= _t.size();
			}

			++_n;
			_t[hashi]._kv = kv;
			_t[hashi]._state = EXIST;
 
			return true;

		}

		HashDate<K, V>* Find(const K& key)
		{
			Hash hs;
			//找位置
			size_t hashi = hs(key) % _t.size();
			//找到空就停止
			while (_t[hashi]._state != EMPTY)
			{
				//因为删除是伪删除,所以多加了一条判断
				if (_t[hashi]._state== EXIST && _t[hashi]._kv.first == key)
				{
					return &_t[hashi];
				}
				++hashi;
				hashi %= _t.size();
			}
			return nullptr;
		}
		//伪删除,实际上的数据还在表中
		bool Erase(const K& key)
		{
			//找
			HashDate<K, V>* ret = Find(key);
			if (ret == nullptr)
			{
				return false;
			}
			else
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
		}
	private:
		vector<HashDate<K, V>> _t;
		size_t _n=0;//有效数据个数;
};

开散列(哈希桶)的实现

实际上哈希桶的实现和闭散列的结构上类似,只不过哈希桶中的每一个桶(位置)都相当于一个单链表,将冲突的数据用链表链接起来,并不需要探测寻找下一个位置,此时无需设置任何状态!

整体结构

template<class K>
struct  HashFuc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct  HashFuc<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto a:s)
		{
			hash *= 131;
			hash += a;
		}

		return hash;
	}
};

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

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

template<class K,class V,class Hash=HashFuc<K>>
class HashTable
{
	typedef HashNode<K,V> Node;
public:
	HashTable();
	// 哈希桶的销毁
	~HashTable();


	// 插入值为data的元素,如果data存在则不插入
    bool Insert(const pair<K,V>& kv);
    
	// 在哈希桶中查找值为key的元素,存在返回true否则返回false
	Node* Find(const K& key);

	// 哈希桶中删除key的元素,删除成功返回true,否则返回false
	bool Erase(const K& key);

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

插入

整体步骤:

  • 根据关键码key算位置
  • 若该位置没有元素,直接放进,若有,说明发生冲突,直接采用头插法即可!

同样需要注意扩容问题,在上一文中提到,对于开散列,扩容的条件:负载因子等于1时扩容!

这里扩容的操作和闭散列的有所不同,这里不采用复用的手段(不是说不可以),直接采用复用的话涉及到资源浪费问题,因为在这个过程中我们需要创建相同数据的结点插入表中,插入完毕后还需要将旧表的结点给释放,没必要!所以不采用复用的方式,而是重新开比旧表大二倍的新表,将旧表的结点一个个取下来,重新根据哈希函数计算位置,在插入,冲突就头插法,就这样!

bool Insert(const pair<K,V>& kv)
{
	//不能存在重复
	if (Find(kv.first))
		return false;
	//负载因子为1时开始扩容
	if (_n == _h.size())
	{
		vector<Node*> newHash(_h.size() * 2, nullptr);
        //遍历旧表
		for (size_t i = 0; i < _h.size(); i++)
		{
			//将旧表的结点一个一个取下来重新映射至新表,节省空间
			Node* cur = _h[i];
			while (cur)
			{
				Node* next = cur->_next;
				//映射至新表中
				Hash hs;
				size_t newi = hs(cur->_kv.first) % newHash.size();//哈希函数又称散列函数
                //头插法
				cur->_next = newHash[newi];
				newHash[newi] = cur;

				cur = next;
			}
			//旧表置空
			_h[i] = nullptr;
		}
		//交换两个表的内容即可
		_h.swap(newHash);
	}

	Hash hs;
	size_t hashi = hs(kv.first) % _h.size();
	//头插
	Node* newnode = new Node(kv);
	newnode->_next = _h[hashi];
	_h[hashi] = newnode;
	++_n;
	return true;
}

查找

先通过哈希函数算出位置,在进行类似遍历单链表的操作。找到就放回结点位置,找不到就返回空!

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

删除

实际上就是单链表的删除操作而已!需要注意的是,我们要保存待删除结点的前一个结点位置,为了方便删除结点后再一次链接。

	bool Erase(const K& key)
	{
		Hash hs;
		size_t hashi = hs(key) % _h.size();//算位置
		Node* prev = nullptr;//保存待删除结点的前一个位置
		Node* cur = _h[hashi];//遍历链表
		while (cur)
		{
			if (cur->_kv.first == key)//找到目标节点
			{
				//删除的是第一个
				if (prev == nullptr)
					_h[hashi] = cur->_next;
				else
					prev->_next = cur->_next;
				delete cur;
                --_n;//数据个数-1
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}

析构

因为涉及到手动开空间,所以哈希需要手动写析构函数

操作简单,就是遍历结点一个个释放掉即可,最后在置空!

	// 哈希桶的销毁
	~HashTable()
	{
		for (size_t i = 0; i < _h.size(); i++)
		{
			Node* cur = _h[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_h[i] = nullptr;
		}
		_n = 0;
	}

开散列与闭散列的比较

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

所以实际上哈希桶(开散列)用的比较多,比如C++STL库中的unordered系列容器的底层结构就是哈希桶!!! 


  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值