当前位置: 首页 > news >正文

c++进阶之----哈希(桶)

本篇主要讲解哈希桶的实现,线性探测和开放寻址法在下一篇博客中讲解

1.概念

哈希(Hash)是一种将任意长度的输入数据映射到固定长度的输出数据的方法。哈希函数是哈希的核心,它负责将输入数据转换为哈希值。哈希值通常是一个整数,用于快速查找和比较数据。

  • 哈希函数:将输入数据转换为固定长度的输出数据(哈希值)的函数。

  • 哈希值:哈希函数的输出结果。

  • 哈希表:一种基于哈希的数据结构,用于快速查找、插入和删除数据。

2. 负载因子

假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么 ,负载因子有些地方也翻译为载荷因子/装载因子等,

2.1 负载因子的作用

  • 影响冲突概率:负载因子越高,哈希表中的元素越密集,冲突的概率也越高。

  • 影响查找效率:负载因子越高,查找元素时需要遍历的链表或开放寻址的次数可能越多,查找效率降低。

2.2 负载因子对哈希表性能的影响

  • 负载因子过高

    • 冲突概率增加,查找效率下降。

    • 可能需要动态调整桶的数量以降低负载因子。

  • 负载因子过低

    • 桶的利用率不高,浪费内存空间。

    • 查找效率可能较高,但内存使用效率低

 3.实现

3.1 基本结构

和其他数据结构类似,我们还是先构建其结点,之后再由结点组成我们的哈希桶,这里我们用了链地址法,可以参考下图

像一条拉链一样,我们在构造它时需要以指针数组为底层逻辑,每一个小table都有一个指针“挂钩”,方便我们映射元素并进行存储。__stl_next_prime(0)表示质数表的第一个数,相当于造的时候就给出了一定的空间,但为什么要是质数呢?因为我们要减少哈希冲突,即减少映射在同一个位置上的数据,同时可以提高探测效率,而且当哈希表的大小为质数时,输入数据的步长(如某些模式化的输入)不容易与表的大小产生因数关系,从而减少因数相关的冲突

static const int __stl_num_primes = 28;
static const unsigned long __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
};
inline unsigned long __stl_next_prime(unsigned long n)
{
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}
template<class  k>
struct HashFunc
{
	size_t operator()(const k& key)
	{
		return key;
	}
};
namespace hash_bucket
{
	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=HashFunc<k> >
	class HashTable
	{
		typedef HashNode<k, v> Node;
	public:
		HashTable(size_t size = __stl_next_prime(0))
			:_tables(size, nullptr)
		{}

		~HashTable()
		{
			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;
			}
		}

	private:
		vector<Node*> _tables;  //指针数组
		size_t _n = 0;
	};

}

3.2 查找函数

思路很简单,就是先算出要插入元素的映射值,之后让cur定位到这一列,上例中,比如我要找30,那么我算出来映射在8的位置,那就让cur先来到8这里,相当于是‘8’这条链表的头结点,之后像链表一样查找就OK了。

参考代码:

		Node* find(const k& key)
		{
			hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;   //找到了
				}
				cur = cur->_next;
			}
			return nullptr;
		}

 3.3 删除函数

这个思路也和删除链表一样,还是先算映射,比如删30,映射到8这个位置,之后就在“8”这个链表里面去寻找30就好,这里值得注意的是,如果我们要删的是96的话,删了之后“8”就和链表“脱钩”了,所以我们得让“8”这个位置(头结点)与cur->next连上,其余就参考链表删除代码就好了

bool erase(const k& key)
{
	//跟删除链表的方法一样

	hash hs;
	size_t hashi = hs(key) % _tables.size();
	Node* prev = nullptr;
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			if (prev == nullptr)
			{
				_tables[hashi] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			--_n;
			delete cur;
			return true;
		}
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

3.4 插入函数

 插入的逻辑也比较好理解,还是先判断哈希表满没满,满了扩容方法及注释在代码里面,这里不在赘述,之后就是映射,插入,跟链表的插入差不多。

bool insert(const pair<k, v>& kv)
{
	if (find(kv.first))
	{
		return false;        //有相同的键了,不能再插入了
	}
	hash hs;
	// 负载因子到1,再扩容
	if (_n == _tables.size())        //满了要扩容
	{
		 也可以,但是扩容新开辟节点,释放旧节点,有点浪费
		//HashTable<k, v> newHT(__stl_next_prime(_tables.size() + 1));     
        //这里不同于之前链表之类的二倍扩容,我们通常采用质数来扩容
		//for (size_t i = 0; i < _tables.size(); i++)
		//{
		//	Node* cur = _tables[i];
		//	while (cur)
		//	{
		//		newHT.insert(kv.first);
		//		cur = cur->_next;
		//	}
		//}
		//_tables.swap(newHT._tables);    //我们本意是想给this的哈希表扩容,只不过是借用另一 
                                            个哈希表来转移数据罢了,搞好了就要过河拆桥了
		vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
		for (size_t i = 0;i < _tables.size(); i++)
		{
			Node* cur = _tables[i];      //可以认为tables[i]就是图中横着的那些,cur的使命就 
                                           是把可以映射到这里的位置的数一个一个的挂上去
			while (cur)
			{
				// 旧表的节点挪动下来
				// 插入到映射的新表位置
				Node* next = cur->_next;
				size_t hashi = hs(cur->_kv.first) % newtables.size();
				cur->_next = newtables[hashi];
				newtables[hashi] = cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newtables);
	}
	size_t hashi = hs(kv.first) % _tables.size();
	Node* newnode = new Node(kv);

	//头插
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	++_n;
	return true;
}

4.代码汇总

这是.h文件

#pragma once
#include<utility>
#include<vector>
#include <string>
using namespace std;
//扩容用
static const int __stl_num_primes = 28;
static const unsigned long __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
};
inline unsigned long __stl_next_prime(unsigned long n)
{
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}


template<class  k>
struct HashFunc
{
	size_t operator()(const k& key)
	{
		return key;
	}
};
// 特化
template<>
struct HashFunc<string> {
	size_t operator()(const string& key)
	{
		size_t hashi = 0;
		for (auto ch : key)
		{
			hashi *= 131;
			hashi += ch;
		}
		return hashi;
	}
};
namespace hash_bucket
{
	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=HashFunc<k> >
	class HashTable
	{
		typedef HashNode<k, v> Node;
	public:
		HashTable(size_t size = __stl_next_prime(0))
			:_tables(size, nullptr)
		{}

		~HashTable()
		{
			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;
			}
		}

		bool insert(const pair<k, v>& kv)
		{
			if (find(kv.first))
			{
				return false;        //有相同的键了,不能再插入了
			}
			hash hs;
			// 负载因子到1,再扩容
			if (_n == _tables.size())        //满了要扩容
			{
				 也可以,但是扩容新开辟节点,释放旧节点,有点浪费
				//HashTable<k, v> newHT(__stl_next_prime(_tables.size() + 1));     //这里不同于之前链表之类的二倍扩容,我们通常采用质数来扩容
				//for (size_t i = 0; i < _tables.size(); i++)
				//{
				//	Node* cur = _tables[i];
				//	while (cur)
				//	{
				//		newHT.insert(kv.first);
				//		cur = cur->_next;
				//	}
				//}
				//_tables.swap(newHT._tables);    //我们本意是想给this的哈希表扩容,只不过是借用另一个哈希表来转移数据罢了,搞好了就要过河拆桥了
				vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
				for (size_t i = 0;i < _tables.size(); i++)
				{
					Node* cur = _tables[i];      //可以认为tables[i]就是图中横着的那些,cur的使命就是把可以映射到这里的位置的数一个一个的挂上去
					while (cur)
					{
						// 旧表的节点挪动下来
						// 插入到映射的新表位置
						Node* next = cur->_next;
						size_t hashi = hs(cur->_kv.first) % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newtables);
			}
			size_t hashi = hs(kv.first) % _tables.size();
			Node* newnode = new Node(kv);

			//头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

		Node* find(const k& key)
		{
			hash hs;
			size_t hashi = hs(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 hs;
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					--_n;
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

	private:
		vector<Node*> _tables;  //指针数组
		size_t _n = 0;
	};

}

这是.cpp测试文件

#include"hashtable.h"
#include<string>
#include<iostream>
using namespace std;
namespace hash_bucket
{
	void test1()
	{
		int a[] = { 19, 30, 5, 36, 13, 20, 21, 12, 58, 111 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.insert({e,e});
		}

		ht.erase(5);
		ht.erase(58);
		ht.erase(111);
		HashTable<string, string> dict;
		dict.insert({ "sort", "排序" });
		dict.insert({ "string", "字符串" });
	}
}

int main()
{
	hash_bucket::test1();

    HashFunc<string> hf;
	cout << hf("bcad") << endl;
	cout << hf("aadd") << endl;
	cout << hf("abcd") << endl;
	return 0;
}

 

相关文章:

  • 决策树原理详解
  • 3月30号
  • Windows10 下QT社区版的安装记录
  • 在 Vue 项目中快速集成 Vant 组件库
  • 磁盘冗余阵列
  • KMeans算法案例
  • 微服务架构中的精妙设计:服务注册/服务发现-Eureka
  • MySQL执行计划分析
  • MATLAB中rmfield函数用法
  • 中国网络安全产业分析报告
  • ngx_get_options
  • 鸿蒙HarmonyOS NEXT设备升级应用数据迁移流程
  • 算法刷题-最近公共祖先-LCA
  • 元编程思想
  • MySQL8.4 NDB Cluster 集群配置安装
  • 《K230 从熟悉到...》圆形检测
  • 推荐系统(二十):TensorFlow 中的两种范式 tf.keras.Model 和 tf.estimator.Estimator
  • playwright解决重复登录问题,通过pytest夹具自动读取storage_state用户状态信息
  • 【深度学习】不管理论,入门从手写数字识别开始
  • Vue3 其它API Teleport 传送门
  • 一诺网站建设/网络搭建是干什么的
  • 网站title 在哪里设置/网络整合营销4i原则
  • 建设特效网站/学生个人网页制作成品代码
  • 自己服务器建网站 备案/steam交易链接怎么获取
  • 甘肃省作风建设年活动有网站/seo上海优化
  • 生物商城网站建设/百度应用商店app下载