【数据结构】哈希表的模拟实现(开散链)

文章详细介绍了开散链哈希桶的实现原理,包括哈希函数的选择,如何处理哈希冲突,以及如何通过仿函数获取键值。在哈希表的插入和查找操作中,文章阐述了如何避免冲突和进行扩容。此外,还提供了迭代器的模拟实现和上层封装的map与set的区别。
摘要由CSDN通过智能技术生成

先一.开散链哈希桶的实现原理

在理解开散链之前首先需要理解哈希的概念,在哈希表中可以不经过任何比较,一次直接从表中得到要搜索的元素。通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。这种哈希映射的观念是哈希表实现的基本。在插入元素的时候通过关键码计算出元素的储存位置,这样在查找的时候就可以之间通过key值经由hashfunc计算出储存位置,从而直接找到结果。

但是假如我们用正常vector来储存各个元素值的话会出现一个问题,有可能两个不同的值会映射到同一个位置。我们可以把相同地址的关键码归于一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。这样一个位置就可以放多个值了

 

二.哈希函数的选取

一般来说,如果传入的key值可以直接转化成int类型我们就可以直接取值,或者用它asc码作为key值。这种时候就可以使用下面这种哈希函数

template<class K>
struct HashFunc          
{
	size_t operator()(const K& key)   //因为不知道传入值的类型,通过这个函数转换确保能取到key值
	{
		return (size_t)key;
	}
};

但如果传入的key值无法直接转换成int例如string类型,这时候我们就要特殊提供一个特化版本的哈希函数,专门针对这种类型。这里是取string的每个位置乘上131最后求和所得的值作为哈希值,当然你也可以设计不同的转换方式,这取决于个人。

struct HashFunc<string>   //对string类型的特化
{
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)    //尽量减少冲突的一种做法
		{
			val *= 131;
			val += ch;
		}

		return val;
	}
};

三.取值函数

由于哈希表的上层会封装unorederd_set和unorederd_map,这导致在上层传参的时候,data值有可能是pair结构也有可能就是单纯的本身,我们需要通过创建一个仿函数来获取到正确的key值,一般这段代码是在哈希表的上层结构提供并传入哈希表当中,

truct MapKeyOfT               //取key值的仿函数
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};

四.模拟实现哈希表

我们可以通过vector来模拟表的结构并在表中储存node的指针,把node实现成单链表的形式,双链表也可以不过本质没有什么区别。

template<class T>
	struct HashNode
	{
		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{}
	
		T _data;
		HashNode<T>* _next;
	};

 这段代码内部的迭代器会在稍后说明,

查找的思路:首先判断当前哈希表是否为空并作单独处理,在通过hash的仿函数计算出应该储存在哈希表的那个位置,并在当前位置的桶内寻找,(注意比较时需要通过keyofT,也就是上层传递下来的取值函数来和key值进行比较,因为cur是指向当前桶节点的指针,哈希表并不知道data的类型,有可能是pair,也有可能直接是key值)

插入的思路:先通过查找去重,在判断负载因子,如果负载因子到1在进行扩容,扩容我们可以把原先的值一次插入,这里采用另一种方法,创建一个新的表,并把原来表上每个桶节点转移到新表的新的位置上。最后把旧表的节点置为空,并和新表交换,最后在头插新的节点。

template<class K,class T,class Hash,class KeyOfT>
	class HashTable
	{
		typedef HashNode<T> Node;
		template<class K, class T, class Hash, class KeyOfT>
		friend struct _HashIterator;
	public:

		typedef _HashIterator<K, T, Hash, KeyOfT> iterator;

		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])
				{
					return iterator(_tables[i], this);
				}
			}
			return end();
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _size; i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		iterator Find(const K& key)
		{
			Hash hash;
			KeyOfT kot;

			if (_tables.size() == 0)
			{
				return end();
			}

			size_t hashi = hash(key) % _tables.size();				//计算出对应key应该存在的位置
			Node* cur = _tables[hashi];								//创建一个指针指向哈希表中的对应位置

			while (cur)       //在当前的桶内遍历
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);  //找到的话返回指向当前节点的迭代器
				}
				cur = cur->_next;
			}
			return end();
		}

		pair<iterator, bool> Insert(const T& data)
		{
			Hash hash;
			KeyOfT kot;

			iterator check = Find(kot(data));				//查找相同值去重
																
			if (check != end())
			{
				return make_pair(check, false);
			}

			if (_size == _tables.size())//当负载因子为1,进行扩容(开散链的有效数据个数不由表的节点数决定,因为桶的存在)
			{
				vector<Node*> newTables;
				size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				newTables.resize(newSize, nullptr);

				for (size_t i = 0; i < _tables.size(); i++)    //转移节点,把原来的节点挂到新的vector中
				{
					Node* cur = _tables[i];
					while (cur)                       //一次挂一个节点头插
					{
			 			Node* next = cur->_next;   //提前保存下一个节点的位置

						size_t hashi = hash(kot(cur->_data)) % newTables.size();		//计算出新的位置
																			//头插过去让原来的节点成为新的哈希表内部的节点
						cur->_next = newTables[i];
						newTables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);           //让新的结构替换原来的结构
			}

			size_t hashi = hash(kot(data)) % _tables.size();   //同样的原理再插入要插入的值
			Node* newnode = new Node(data);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			_size++;

			return make_pair(iterator(newnode, this), true);
		}

		 bool Erase(const K& key)
		{
			if (_size == 0)
			{
				return false;
			}
			Hash hash;
			KeyOfT kot;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)             //有两种情况,头删和中间删
			{
				if (kot(cur->_data) == key)  //找到对应的桶
				{
					if (prev == nullptr)			//如果当前节点的没有上一个节点,证明他是头节点,直接让下一个节点成为头节点
					{
						_tables[hashi] = cur->_next;
					}
					else                       //若当前节点有上一个节点,则把当前节点解下来
					{
						prev->_next = cur->_next;
					}
					delete(cur);
					cur = nullptr;
					_size--;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;

		}

		size_t Size()const
		{
			return _size;
		}

		bool Empty()const
		{
			return 0 == _size;
		}

		size_t BucketCount()const
		{
			size_t num = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i])
				{
					++num;
				}
			}

			return num;
		}

		size_t BucketSize(const K& key)const
		{
			size_t ret = 0;
			KeyOfT kot;
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					if (kot(cur->_data) == key)
					{
						ret++;
					}
					cur = cur->_next;
				}
			}
			
			return ret;
			
		}

	private:
		vector<Node*> _tables;
		size_t _size;
	};

五.迭代器的模拟

我们可以用一个指向哈希表本身的指针和一个指向哈希表节点的指针来实现迭代器

自增的思路:现在自己的的桶内遍历,若是桶已经为空,则遍历整个哈希表找到下一个有桶的节点,并让这个指针指向这个桶的第一个元素。遍历整个哈希表也没有值的情况下让指向哈希表节点的值为空

template<class K, class T, class Hash, class KeyOfT>
	struct _HashIterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K,T,Hash, KeyOfT> HT;
		typedef _HashIterator<K, T, Hash, KeyOfT> Self;

		_HashIterator(Node* node, HT* ht)
			:_node(node)
			, _ht(ht)
		{
		}

		T& operator*()
		{
			return _node->_data;
		}

		T* operator->()
		{
			return &_node->_data;
		}

		Self& operator++()
		{
			if (_node->_next)    //处理当前桶没有遍历完的情况,直接继续遍历即可
			{
				_node = _node->_next;
			}
			else
			{
				Hash hash;            //创建仿函数 用来取key值和哈希值
				KeyOfT kot;
				size_t i = hash(kot(_node->_data)) % _ht->_tables.size();   //计算出当前节点在哈希桶中的位置
				i++;                                                      //并进入到当前桶的下一个位置
				for (; i < _ht->_tables.size(); i++)   //从这个位置开始直到整个哈希表的末尾
				{
					if (_ht->_tables[i])    //如果遇到不为空的节点意味着那个节点的哈希桶不为空,把当前节点更新成新节点即可
					{
						_node = _ht->_tables[i];
						break;
					}
				}
				if (i == _ht->_tables.size())  //如果跑到哈希表末尾仍不为空则需要手动置空
				{
					_node = nullptr;
				}
				return *this;
			}
		}

		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}

	private:
		Node* _node;
		HT* _ht;

	};

六.上层封装

上层封装map和set的区别,我们已经知道了需要在实现map和set的时候主动传入一个在data内部取值的函数。但是在map和set使用哈希表的时候还是有一些区别的。map有两个值k和v所以在map内部应该使用这样的哈希表。细节是不仅要传pair还要传k,不仅仅是因为我们实现的模板这么要求,而是因为哈希表需要k的类型,比如说查找的时候,我们需要接受k类型的参数。

Bucket::HashTable< K, pair<K, V>, Hash, MapKeyOfT> _ht;

那么set该如何实现呢

	Bucket::HashTable< K, K, Hash, MapKeyOfT> _ht;

为了满足模板的要求我们把k值传两次就可以了,因为哈希表底层使用的是第二个参数,而这个参数类型不管是pair还是k本身都可以。其实本质和map没有任何区别

至于底层直接调用哈希表对应的函数就可以了

下面是map的实现逻辑

iterator begin()
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}

	pair<iterator, bool> Insert(const pair<K, V>& kv)
	{
		return _ht.Insert(kv);
	}

	V& operator[](const K& key)
	{
		pair<iterator,bool> ret = _ht.Insert(make_pair(key, V()));
		return ret.first->second;
	}

	size_t size()const 
	{ 
		return _ht.Size(); 
	}

	bool empty()const 
	{ 
		return _ht.Empty(); 
	}

	size_t bucket_count() 
	{ 
		return _ht.BucketCount(); 
	}

	iterator find(const K& key) 
	{ 
		return _ht.Find(key); 
	}

	bool erase(const K& key)
	{
		return _ht.Erase(key);
	}

	size_t bucket_size(const K& key) 
	{ 
		return _ht.BucketSize(key); 
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值