数据结构之哈希表

前言

本节是对于STL库中的unordered_map和unordered_set的底层的哈希表的一个基本的模仿实现,有助于读者理解unordered_map和unordered_set的底层逻辑

底层的哈希表

哈希表的原理

首先,先来理解哈希表的实现原理,其实就是通过哈希函数建立一一对应的映射关系,这样就可以通过o(1)的次数实现数据的查找,如图所示

建立对应的直接映射关系,这种是直接映射方式,但是如果数据集合当中有一个99999,那么就会造成空间的浪费,于是采用除留余数法,设散列表中允许的地址数位m,取一个不大于m,但是最接近或者是等于m的质数p作为除数,按照哈希函数:Hash(key)=key&p,将关键码转换成哈希地址

  但是,有一个问题,就是如果两个关键码对应的同一个哈希地址怎么处理,比如说4和14,在除数都是10的情况下,余数都是4,这样就发生了哈希冲突。

哈希冲突的解决办法很多,这里采用开散列的方式来处理

开散列

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

      

形象得说,就是将地址重复的所有值全部用链表存储起来,然后将链表头放入哈希表当中存储起来,这样就能很好的解决问题,下面开始实现基本的代码,中间其实还有很多的细节问题,我们边写边分析边解决

代码实现

首先分析哈希表中应该存储什么,首先是基本的数据,其次就是下一个的位置,使用链表串联起来的。

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

   实现的代码比较长,中间的注释放到各个代码当中实现,但是由于代码的整体性,请读者先大致了解整个代码框架,然后在逐步分析,上下的紧密相关的,如果上面看不懂就先跳过,看到下面就自然而言就理解了

	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{}
	};

	//说明一下泛型参数,为了实现map和set的底层逻辑,在这里使用K和T,注意如果T传递的是pair<K,T>类型的话,其实就是map的KV类型
	// 如果T传递的是K,就是set类型。
	// 这里的仿函数的使用说明
	// KeyOfT 这个是为了pair<K,V>选取K而单独设置的仿函数
	// Hash防函数的作用,T是泛型,不一定支持%预算,如果是int,size_t类型自然支持,但是如果是string或者是某些结构体,那么就要特殊处理
	// 如何特殊处理(当然大部分的情况是int或者是size_t,少部分是string,其他的结构体就要遇到在处理了)
	// 列举一个,比如说一个学生对象,我们可以以他们的学号作为比较,就要只要是具有唯一性的都可以。
	// 
	// 有关负载因子的介绍
	// 在HashTable这个类里面由vector<Node*>和_size这两个成员变量,而负载因子的一般设置成1,也就是_size == _tables.size()
	// 负载因子的意义是什么,其实最理想的情况就是每一个哈希地址都只挂了一个节点,但是这中情况就很少,所以一般是有一些哈希地址上没有任何节点
	// 但是在某一些哈希地址上却挂了很多个节点,这个时候访问的效率就会大大降低,这个时候,如果当_size == _tables.size()的时候,我们就扩大
	// _tables.size(),并且重新定义地址映射,这样就是减少一个哈希地址上所挂的节点数量
	// 举一个例子 ,比如一开是_tables.size()=20;这个时候,1,11,21,41,61都是挂到一个哈希地址下面的,我们一般将_tables.size()扩大两倍
	// _tables.size()=40,这个时候1,11,41分开,21和61挂在一起,这样就分散了,提高效率
	// 前置声明
	template<class K, class T, class Hash, class KeyOfT>
	class HashTable; //注意HashTable的前置声明的方式方法

	template<class K, class T, class Hash, class KeyOfT>
	struct __HashIterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K, T, Hash, KeyOfT> HT; //这个地方使用了HashTable<K,T,Hash,KeyOfT>,所以要前置什么一下,避免编译不通过
		typedef __HashIterator<K, T, Hash, KeyOfT> Self;//注意这种写法,在内核当中经常使用,主要是为了简化写法。

		Node* _node; //这个是封装的核心对象
		HT* _pht;

		__HashIterator(Node* node, HT* pht)
			:_node(node)
			, _pht(pht) 
		{}
		T& operator*()
		{
			return _node->_data; //注意这个地方的_data是泛型T
		}

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

		Self& operator++()//注意哈希表的特性,每一个地址中是挂了一个链表,当这个链表遍历完了以后是要走到下一个地址上去的,所以就是要传递HashTable的原因
		{
			if (_node->_next)
			{
				// 当前桶中迭代
				_node = _node->_next;
			}
			else
			{
				// 找下一个桶
				Hash hash;
				KeyOfT kot;
				size_t i = hash(kot(_node->_data)) % _pht->_tables.size();//注意这个地方,上面有说明
				++i;
				for (; i < _pht->_tables.size(); ++i)
				{
					if (_pht->_tables[i])//注意在哈希表的下一个位置其实不一定是有桶的
					{
						_node = _pht->_tables[i];
						break;
					}
				}

				// 说明后面没有有数据的桶了
				if (i == _pht->_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;
		}
	};

	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);//注意这个地方的this,为了迭代器++的时候的操作,这个地方是要传递整个哈希表的,也就是this指针
				}
			    

			return end();
		}

		iterator end()
		{
			return iterator(nullptr, this);//其实在底层并不是这样的,这里做了简化处理,就是直接认为最后是nullptr
		}

		~HashTable()//注意析构函数只需要将创建的节点释放,_tables是vector类型,默认会调用vector的析构函数
		{
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __stl_prime_list[__stl_num_primes] =
			{
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};

			for (size_t i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}
		//这里说明一下插入的返回值为什么是pair<iterator,bool>类型
		//首先,插入节点如果已经存在,那么bool值就是false
		//如果是新插入的,就返回true。
		pair<iterator, bool> Insert(const T& data)
		{
			Hash hash;
			KeyOfT kot;

			// 去重
			iterator ret = Find(kot(data));
			if (ret != end())//说明节点存在
			{
				return make_pair(ret, false);//注意make_pair是
			}

			// 负载因子到1就扩容,有关负载因子的概述,上面由叙述
			if (_size == _tables.size())
			{
				size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;//注意一开的 _tables.size()=0.所以这个地方要注意
				vector<Node*> newTables;
				newTables.resize(newSize, nullptr);
				// 旧表中节点移动映射新表
				for (size_t i = 0; i < _tables.size(); ++i)//重新构建映射关系
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(kot(cur->_data)) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);//注意这个地方的写法,交换_tables和newTables,因为newTables是在这个函数当中创建的,
				//出了这个函数作用域就会调用析构函数,我们交换之后,将一开始的_tables指向的空间给了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);
		}

		iterator Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return end();
			}

			Hash hash;
			KeyOfT kot;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);
				}

				cur = cur->_next;
			}

			return end();
		}

		bool Erase(const K& key)
		{
			if (_tables.size() == 0)
			{
				return false;
			}

			Hash hash;
			KeyOfT kot;
			size_t hashi = hash(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					// 1、头删
					// 2、中间删
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					--_size;

					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

		size_t Size()
		{
			return _size;
		}

		// 表的长度
		size_t TablesSize()
		{
			return _tables.size();
		}

		// 桶的个数
		size_t BucketNum()
		{
			size_t num = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i])
				{
					++num;
				}
			}

			return num;
		}

		size_t MaxBucketLenth()
		{
			size_t maxLen = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				size_t len = 0;
				Node* cur = _tables[i];
				while (cur)
				{
					++len;
					cur = cur->_next;
				}

				//if (len > 0)
					//printf("[%d]号桶长度:%d\n", i, len);

				if (len > maxLen)
				{
					maxLen = len;
				}
			}

			return maxLen;
		}

	private:
		vector<Node*> _tables;
		size_t _size = 0; // 存储有效数据个数
	};

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值