【C++】哈希表的开散列、闭散列的代码实现

0. 概念

哈希表,又名 散列表

哈希(Hash)是一种方法:将储存元素的 key存储位置 建立映射(关联)关系

建立映射关系的方法具体有:

1. 直接定址法
使用:范围比较集中,每个而数据分配一个唯一位置
2. 除留余数法
适用:范围不集中,分布分散
hashi = key % 10; key 根据存储位置关系模出来的,不同的值映射到了同一个位置(哈希冲突/碰撞)

如何解决哈希冲突问题?

🎯闭散列,又名 开放定址法
在自己的开放空间找下一个位置
a. 线性探测,直接找下一个位置。比较暴力,一些相邻聚集的位置会连续冲突,可能形成“踩踏”
b. 二次探测,模表的长度,冲突后 + i 2。可以缓解线性探测的踩踏
闭散列本质还是一个零和游戏,总不是占了别人的位置
🎯开散列,又名 链地址法 / 开链法 / 拉链法 / 哈希桶
首先对关键码集合,用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

1. 建立 哈希表 用 开放定址法 解决冲突

分析【线性探测】:冲突时找下一个位置

查找

从映射位置开始找,直到空结束

删除

删除后的位置怎么处理?只能单独做一个标识符,给数值和 NULL 都不对

解决:每个存储位置的状态标识【空、存在、删除】

扩容

哈希表在什么情况下扩容?怎么扩?

解决:设置 负载因子 / 载荷因子(反映表存储数据量的程度、百分比),需要控制在0.7~0.8以下

ps:扩容扩的是 size,扩 capacity 没用,没被size 覆盖到的地方 [] 会报非法访问(vector 的特性)。

pps:且会导致映射关系变化,需要单独处理。

namespace OpenAddress
{
	enum State 
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V>
	class HashTable
	{
	public:

		bool Insert(const pair<K, V>& kv)
		{
			// 去冗
			if (Find(kv.first))
			{
				return false;
			}
			// 负载因子超过0.7就扩容2倍
			//if (_n / _tables.size() >= 0.7)
			//{
			//	// 1. 需要处理空表
			//	// 2. 光扩capacity没法用[]访问,size没变
			//  // 3. 扩完容还需要...重置hash表内数据,要新开一个表,不能在原位置哦
			//	_tables.reserve(_tables.capacity() * 2);	// ×
			//}
			//if (_tables.size() == 0 ||_n * 10 / _tables.size() >= 7)
			//{
			//	size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			//	vector<HashData> newtables(newsize);
			//	// 遍历旧表,重新映射到新表
			//	for (auto& data : _tables)
			//	{
			//		if (data._state == EXIST)
			//		{
			//			// 重新算在新表的位置
			//			// 就是下面的代码复用,逻辑如此,但有没有更简单的方法咧?
			//			size_t i = 1;
			//			size_t index = hashi;
			//			while (newtables[hashi]._state == EXIST)
			//			{
			//				index = hashi + i;
			//				index %= newtables.size();	// 超过表就回到前面
			//				++i;
			//			}
			//			newtables[index]._kv = data._kv;
			//			newtables[index].state = EXIST;
			//		}
			//	}
			//	_tables.swap(newtables);
			//}

			// 扩容
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newht;
				newht._tables.resize(newsize);		// 用新的hash表对象复用insert
				// 遍历旧表,重新映射到新表
				for (auto& data : _tables)
				{
					newht.Insert(data._kv);
				}
				_tables.swap(newht._tables);
		}
		
			// 映射位置
			size_t hashi = kv.first % _tables.size();	// vector 的特性,模 capacity 后,超出 size 的部分是非法访问!!!自己写还能行,用 vector 就只能模 size
		
			size_t i = 1;
			size_t index = hashi;

			// [线性探测]
			while (_tables[index]._state == EXIST)
			{
				index = hashi + i;
				index %= _tables.size();	// 超过表就回到前面
				++i;
			}

			_tables[index]._kv = kv;
			_tables[index]._state = EXIST;
			_n++;

			return true;
		}
	
		// 注意:表中没有EMPTY的情况!!
		// 插入数据后,在扩容前,删除一 部分数据,在插入数据,并且数据正好占据其他空位,导致表里面,除了存在就是删除
		HashData<K, V>* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				//return false;
				return nullptr;
			}

			size_t hashi = key % _tables.size();

			size_t i = 1;
			size_t index = hashi;

			// [线性探测]
			while (_tables[index]._state != EMPTY)
			{
				if (_tables[index]._state == EXIST
					&& _tables[index]._kv.first == key)
				{
					return &_tables[index];
				}
				index = hashi + i;
				index %= _tables.size();
				++i;
		
				//如果已经找了一圈了,说明表里没有 EMPTY,break 就可以
				if (index == hashi)	
				{
					break;
				}
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;	// 伪删除法:不是正真删除了数据,而是把数据位置标识成删除状态~~
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}

	private:
		/*HashDate* tables;
		size_t _size;
		size_t _capacity;*/			// 用现成的容器很方便呀
	
		vector<HashData<K, V>> _tables;
		size_t _n = 0;					// 存取的数据个数
	};

	// 基本测试
	void testHashTable1()
	{
		int a[] = { 3,33,2,13,5,12,1002 };
		HashTable<int, int> ht;

		for (auto e : a)
		{
			ht.Insert(make_pair(e,e));
		}

		ht.Insert(make_pair(15, 15));

		if (ht.Find(13))
		{
			cout << "13在" << endl;
		}
		else 
		{
			cout << "13不在" << endl;
		}

		ht.Erase(13);

		if (ht.Find(13))
		{
			cout << "13在" << endl;
		}
		else
		{
			cout << "13不在" << endl;
		}
	}

}


2. 建立 哈希表 用 哈希桶 解决冲突

对比开放定址,冲突得到了极大的降低

增删查改的时间复杂度:O(1)
这里是取的平均时间,为啥不取最坏了呢? 因为有扩容的发生,扩容则重新建立映射关系,最坏情况几乎不会出现
Question:
如果如果,某些桶还是特别长,怎么办
Answer:
  1. 负载因子控制
  2. 单个桶超过一定长度,这个桶改成挂红黑树(结构体 {联合体 {红黑树的指针, 链表的指针},长度} 解决)
namespace HashBucket
{
	template<class K, class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		pair<K, V> _kv;

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

	};

	template<class K>
	struct HashFunc
	{
		K operator()(const K& key)
		{
			return key;
		}
	};

	// 特化!!
	template<>
	struct HashFunc<string>
	{
		// BKDR hash
		size_t operator()(const string& s)
		{
			/*return s[0];*/	// 这样写的话,如果首字母相同就是冲突的 ,优化
			size_t hash = 0;
			for (auto ch : s)
			{
				hash += ch;
				hash *= 31;	// 也可以乘131、1313
			}
			return hash;
		}
	};

	template<class K, class V, class Hash = HashFunc<K>>	// Hush:把key转化成可以取模的整形,默认支持整形
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		~HashTable()
		{
			for (auto cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}


		// size_t newsize = GetNextPrime(_tables.size());
		size_t GetNextPrime(size_t prime)
		{
			// SGI 的写法:素数表优化
			const int PRIMECOUNT = 28;
			static const size_t primeList[PRIMECOUNT] =
			{
				53ul, 97ul, 193ul, 389ul, 769ul, 
				1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
				49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
				1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
				50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
				1610612741ul, 3221225473ul, 4294967291ul
			};
			size_t i = 0;
			for (; i < PRIMECOUNT; ++i)
			{
				if (primeList[i] > prime)
					return primeList[i];
			}
			return primeList[i];
		}

		Node* Find(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					// 删
					if (prev)
					{
						prev->_next = cur->_next;
					}
					else
					{
						_tables[hashi] = cur->_next;
					}
					--_n;
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

		// 负载因子越大,冲突的概率越高,查找的效率越低,空间利用率高
		// 负载因子越小,冲突的概率越低,查找的效率越高,空间利用率低
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			// 负载因子 == 1 时扩容
			// 原表的节点重新计算位置,挪动到新表
			Hash hash;
			if (_n == _tables.size())
			{
				//size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				size_t newsize = GetNextPrime(_tables.size());
				vector<Node*> newtables(newsize, nullptr);
				//for (Node*& cur : _tables)
				for (auto& cur : _tables)	// 给&,因为旧表挪下来后还要置空
				{
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hash(cur->_kv.first) % newtables.size();
						// 头插到新表
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;

						cur = next;
					}
				}
				_tables.swap(newtables);
			}

			size_t hashi = hash(kv.first) % _tables.size();
			// 头插就可以
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

		size_t MaxBucketSize()
		{
			size_t max = 0;
			for (size_t i = 0; i< _tables.size(); ++i)
			{
				auto cur = _tables[i];
				size_t size = 0;
				while (cur)
				{
					++size;
					cur = cur->_next;
				}

				//printf("[%d]->%d\n", i, size);
				if (size > max)
				{
					max = size;
				}
			}
			return max;
		}

	private:
		vector<Node*> _tables;	// 指针数组 
		size_t _n;				// 存储的有效数据个数
	};


	void testHashBucket1()
	{
		int a[] = { 3,33,2,13,5,12,1002 };
		HashTable<int, int> ht;

		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(15, 15));
		ht.Insert(make_pair(25, 25));
		ht.Insert(make_pair(35, 35));
		ht.Insert(make_pair(45, 45));
	}

	void testHashBucket2()
	{
		int a[] = { 3,33,2,13,5,12,1002 };
		HashTable<int, int> ht;

		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}
	
		ht.Erase(12);
		ht.Erase(3);
	}

	// 测试 BKDR哈希法
	struct HashStr
	{
		// BKDR hash
		size_t operator()(const string& s)
		{
			/*return s[0];*/	// 这样写的话,如果首字母相同就是冲突的 ,优化
			size_t hash = 0;
			for (auto ch : s)
			{
				hash += ch;
				hash *= 31;	// 也可以乘131、1313
			}
			return hash;
		}
	};


	void testHashBucket3()
	{
		//HashTable<string, string, HashStr> ht;
		HashTable<string, string> ht;
		ht.Insert(make_pair("string", "字符串"));
		ht.Insert(make_pair("tree", "树"));
		ht.Insert(make_pair("bucket", "桶"));
		ht.Insert(make_pair("sort", "排序"));
		ht.Insert(make_pair("", "排序"));

		HashStr hashstr;
		cout << hashstr("bacd") << endl;
		cout << hashstr("abcd") << endl;	// 相同...
		cout << hashstr("aadd") << endl;	// 不免这样,也是相同的...
		// BKDR hash 优化
	}


	void testHashBucket4()
	{
		size_t N = 100000;
		HashTable<int, int> ht;

		srand(time(0));
		for (size_t i = 0; i < N; ++i)
		{
			size_t x = rand()+i;
			ht.Insert(make_pair(x, x));
		}

		cout << ht.MaxBucketSize() << endl;
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值