哈希算法以及容器实现

news2024/12/25 14:47:07

哈希

  • 一,哈希算法
    • 1.什么是哈希
    • 2.哈希产生的原因
    • 3.常见哈希算法
    • 4.闭散列( 哈希表)
      • 1.线性探测
      • 2.二次探测
    • 5.开散列(哈希桶)
      • 1.开散列插入
      • 2.开散列扩容
  • 二,代码实现
    • 1.哈希表
    • 2.哈希桶
      • 1.迭代器的实现
      • 2.底层容器的选择
      • 3.重要接口
  • 三,利用哈希桶封装unordered_map&set
    • 1.unordered_set
    • 2.unordered_map

一,哈希算法

1.什么是哈希

哈希算法是一种将输入数据(或称为“消息”)转换为固定长度的散列值(哈希值)的算法。它广泛应用于数据存储、加密、数据完整性检查等领域。
讲通俗点就是将分散的数据重新映射,使其重新分布在某些区间,方便储存和统计。

2.哈希产生的原因

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
本质哈希就是通过时间换空间的方式进行更快的访问。

3.常见哈希算法

1.通过取模加密的方式使得那些距离很分散的值的填入目标区间
如下数据:

2 3 6 19 20 10004 8

这些数据中大部分分布在20之内,但有一个超过一万,那我们难道开结构为10000的数组吗?答案是否定的。
在这里插入图片描述

如果通过取余的方式进行重新映射
在这里插入图片描述
在这里插入图片描述

则通过10个空间可以全部映射
2.哈希冲突
那么如果两个数据同时映射到一个位置怎么办?这就被称为哈希冲突。
解决哈希冲突常见有两种方式:闭散列和开散列

4.闭散列( 哈希表)

1.线性探测

  • 插入
    如果在某映射位置上发生冲突,那么可以选择在此之后,依次向后探测,直到寻找到下一个空位置为止。我们要控制负载因子,负载因子指的是插入数据与空间的比值,当负载因子到达一定时选择扩容,减少冲突。
    如果我们在对以上例子插入一个4则可以变成这样
    在这里插入图片描述
    再插入一个14继续探测
    在这里插入图片描述
  • 删除
    采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素10004,如果直接删除掉,4查找起来可能会受影
    响。因此线性探测采用标记的伪删除法来删除一个元素。

2.二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法
为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i =
1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

5.开散列(哈希桶)

1.开散列插入

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

2.开散列扩容

为了保持每个桶里的数量不过多,桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

二,代码实现

1.哈希表

1.哈希表的底层我们可以选择一个节点类型的vector数组

		vector<HashData<K, V>> _table;
		size_t _n = 0;

这里节点数我们暂且使用key_value模型,方便后续测试

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

我们给每个节点定义一种状态,有存在,空,删除三种状态,方便在线性探测时区分

	enum state
	{
		EMPTY,
		EXIST,
		DELETE
	};

2.数据插入
在线性探索之前我们需要先解决一个问题,如果数据不是整型怎么办?这里我们借助仿函数

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

如果是整型我们将其强转为无符号整型,避免负数的出现,若遇到字符串我们使用一个特化,将其使用一种哈希算法*131+=的方法,方法不唯一,经过这样处理之后,基本映射之后不会出现冲突。
插入函数

bool insert(const pair<K, V>& kv)

首先检查该数据是否插入过,进行查重

		if (find(kv.first))
		   return false;

然后检查负载因子是否过载,为了将表的效率最大化,负载因子不能大不能小,如果过大则需要频繁线性探测,如果太小,则造成空间浪费,这里我们将其设置为0.7,如果大于等于0.7,则进行扩容,重新映射

//_n/_table.size() >= 0.7 变形
if ( _n * 10 / _table.size() >= 7)
{
	reseve(_table.size() * 2);
}

扩容逻辑

void reseve(int newszie)
{
	HashTable<K, V> newtable;
	newtable._table.resize(newszie);
	for (size_t i = 0; i < _table.size(); ++i)
	{
		if (_table[i]._state == EXIST
		{
			newtable.insert(_table[i]._kv);
		}

	}
		_table.swap(newtable._table);
}

这里我们使用现代写法,创建一张新表,将原表数据一一插入,随后交换新旧表。
插入及线性探测

	size_t hashi = hs(kv.first) % _table.size();
	//线性探索
	while (_table[hashi]._state == EXIST)
	{
		++hashi;
		hashi %= _table.size();
	}
		_table[hashi]._kv = kv;
		_table[hashi]._state = EXIST;
		++_n;
        return true;

使用取模操作,映射下标,如果该位置存在则继续向后探测,否则进行插入。
3.查找

HashData<K, V>* find(const K& key)
{
	    HashFunc<K> hs;
		size_t hashi = hs(key)  % _table.size();

		while (_table[hashi]._state != EMPTY)
		{
			if (_table[hashi]._state == EXIST &&
			hs(key) == hs(_table[hashi]._kv.first))
     		{
				return &_table[hashi];
			}
    			++hashi;
				hashi %= _table.size();
		}
				return nullptr;
}

为了防止删除的数据空缺影响搜索探测,遍历条件改为不为空就探索,而识别条件需要同时满足存在和相等。
简单测试

void test1()
	{
		int ar[11] = { 2,4,1,10,9,7,11,8,13,0,11 };
		HashTable<int, int> ht;
		for (auto e : ar)
		{
			ht.insert({ e,e });
		}
		cout << ht.size();
		cout << endl;

		cout << ht.find(2)->_kv.second << endl;
		cout << ht.find(10) << endl;
		cout << ht.find(13) << endl;
		cout << ht.find(19) << endl;

	}

在这里插入图片描述
插入和查重均成功

2.哈希桶

由于STL里unorldered_map,unorldered_set都选择哈希桶,所以这里着重强调哈希桶,并完成迭代器的封装

1.迭代器的实现

template<class K, class T,class Ptr,class Ref, class KeyOfT, class Hash>
struct Hash_Iterator

由于我选择的是友元类的形式,所以这里模板参数传6个,k,T,keyof是map,set在封装重要选择,ptr,ref是迭代器便于生成const,非const类型的前缀,hash是在内部结构,需要遍历的逻辑

  • 提前重命名一下
typedef HashNode<T> node;
typedef Hash_Iterator<K,T,Ptr,Ref,KeyOfT,Hash> Self;
  • 迭代器的构造
    由于在遍历时需要找到,下一个位置,也许要跳着表找,所以还需要将哈希表的指针传入
node* _node;
const HashTable<K,T, KeyOfT,Hash>* _hst;

Hash_Iterator(node* node,const HashTable<K, T, KeyOfT, Hash>* hst)
       		:_node(node)
			,_hst(hst)
			{}
  • ++遍历
Self& operator++()
		{
			//桶里有元素
			if (_node->_next)
			{
				_node = _node->_next;
			}
			//桶里无元素,需要寻找下一个桶
			else
			{
				KeyOfT kot;
				Hash hs;
				size_t hashi = hs(kot(_node->_data)) % _hst->_table.size();
				++hashi;
				for(;hashi < _hst->_table.size();++hashi)
				{
					if (_hst->_table[hashi])
						break;
				}

				if (hashi == _hst->_table.size())
					_node = nullptr;
				else
					_node = _hst->_table[hashi];
			}

			return *this;
		}

如果桶里有下一个节点那么直接迭代即可,否则需要利用哈希表,重新计算下一个位置

  • 访问
		Ref operator*()
		{
			return _node->_data;
		}

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

2.底层容器的选择

这里我们可以选择vector<List>或vector<node*>如果使用LIst作为实例数据则封装迭代时稍微有些麻烦,所以这里直接选择原生指针,作为桶内逻辑

private:
		vector<node*> _table;
		size_t _n;

这里的哈希节点我们统一使用T,方便后续map,set封装

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

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

3.重要接口

  • 默认构造
HashTable()
{
	_table.resize(10, nullptr);
	_n = 0;
}
  • 析构函数
~HashTable()
{
     for (size_t hashi = 0; hashi < _table.size(); hashi++)
      {
			node* cur = _table[hashi];
			while (cur)
			{
				node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_table[hashi] = nullptr;
		}
}

先通过表进行遍历,如果空位中的哈希桶含有元素便进行链式访问释放。

  • 插入
pair<Iterator, bool> Insert(const T& data)

返回值包括插入后或插入位置的迭代器以及布尔类型的结果
1.防止重复插入

Iterator it = Find(kot(data));
		if (it != End())
		return make_pair(it, false);

2.扩容

			if (_n == _table.size())
			{
				//重新建立新表,将原来数据挪动
				vector<node*> newtable;
				newtable.resize(_n * 2);

				for (size_t hashi = 0; hashi < _table.size(); hashi++)
				{
					node* cur = _table[hashi];
					while (cur)
					{
						node* next = cur->_next;
						size_t hashi = hs(kot(cur->_data)) % newtable.size();

						cur->_next = newtable[hashi];
						newtable[hashi] = cur;

						cur = next;
					}

					_table[hashi] = nullptr;
				}

				_table.swap(_newtable);

			}

这里扩容不同于哈希表的线性结构,由于哈希桶中有链式结构,如果重新建表太麻烦了,所以我们决定挪动数据
在这里插入图片描述
3.插入新数据

	size_t hashi = hs(kot(data)) % _table.size();

	node * newnode = new node(data);
	newnode->_next = _table[hashi];
	_table[hashi] = newnode;
	++_n;
	return make_pair(Iterator(newnode,this),true);

计算哈希下标,进行对应位置头插,将新数据的next指向原哈希下标,然后将该数据赋给该位置

  • 删除
bool Erase(const K& key)
{
	Hash hs;
	KeyOfT kot;
	size_t hashi = hs(key) % _table.size();

	node* cur = _table[hashi];
	node* prev = nullptr;
	while (cur)
	{
		if (kot(cur->_data) == key)
		{
			if (prev == nullptr)
			{
				_table[hashi] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			    delete cur;
				return true;
		}
		else
		{
			prev = cur;
			cur = cur->_next;
		}
	}
		return false;

}

首先计算映射下标,然后考虑是否桶内第一个值是否为key,否则遍历
若为值,则直接将头数据转让给下一个
如果不是key,则记录当前prev,然后遍历,直到找到后,prev->_next = cur->_next

  • 查找
Iterator Find(const K& key)
		{
			Hash hs;
			KeyOfT kot;
			size_t hashi = hs(key) % _table.size();
			node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
					return Iterator(cur,this);

				cur = cur->_next;
			}


			return End();
		}

三,利用哈希桶封装unordered_map&set

相比于map,set来说,unordered_map&unordered_set更强调无需,搜索速度更快,由于映射则查找特定元素仅需O(1)的时间复杂度

1.unordered_set

	template<class K, class Hash = HashFunc<K>>
	class unordered_set

定义: unordered_set 是一个不重复元素的集合。它只存储键,不存储值。
特点:

  • 所有元素都是唯一的,不允许重复。
  • 不保证顺序,基于哈希函数来存储元素。
  • 支持快速的查找、插入和删除,平均时间复杂度是 O(1)。
    首先构造一个仿函数,便于在底层数据中取到key
		struct setkeyofT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

成员变量构建

private:
	HashBucket::HashTable<K, K, setkeyofT, Hash> _ht;

ps:这里Hashbucket是命名作用域
提前取出并重命名迭代器

typedef typename HashBucket::HashTable<K, K, setkeyofT, Hash>::Iterator iterator;
typedef typename HashBucket::HashTable<K, K, setkeyofT, Hash>::ConstIterator const_iterator;

ps:typename是为让编译器进入底层取出迭代器,如果不加,编译器不会擅自进入未实例化底层的
接下来就只需接入各个模块

		iterator begin()
		{
			return _ht.Begin();
		}
		iterator end()
		{
			return _ht.End();
		}
		const_iterator begin() const 
		{
			return _ht.Begin();
		}
		const_iterator end() const 
		{
			return _ht.End();
		}
		pair< iterator,bool> insert(const K& key)
		{
			return _ht.Insert(key);
		}
		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

2.unordered_map

同上,贴出代码

template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
	public:
		struct mapkeyofset
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	
		typedef typename HashBucket::HashTable<K, pair<K,V>, mapkeyofset, Hash>::Iterator iterator;
		typedef typename HashBucket::HashTable<K, pair<K,V>, mapkeyofset, Hash>::ConstIterator const_iterator;


		iterator begin()
		{
			return _ht.Begin();
		}
		iterator end()
		{
			return _ht.End();
		}
		const_iterator begin()const
		{
			return _ht.Begin();
		}
		const_iterator end()const 
		{
			return _ht.End();
		}
		pair<iterator,bool> insert(const pair<K, V>& data)
		{
			return _ht.Insert(data);
		}
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}
	private:
		HashBucket::HashTable<K, pair<K,V>, mapkeyofset,Hash> _ht;
	};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2170188.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ --- 模板为什么不能分离编译?

模板为甚么不能分离编译&#xff0c;但普通函数却可以&#xff1f; 一、前置知识二、普通函数能分离编译的原因三、模板不能分离编译的原因 一、前置知识 编译阶段: 源代码到目标代码&#xff1a; 编译器首先将源代码&#xff08;如C/C文件&#xff09;翻译成汇编语言&#x…

初学51单片机之I2C总线与E2PROM

首先先推荐B站的I2C相关的视频I2C入门第一节-I2C的基本工作原理_哔哩哔哩_bilibili 看完视频估计就大概知道怎么操作I2C了&#xff0c;他的LCD1602讲的也很不错&#xff0c;把数据建立tsp和数据保持thd&#xff0c;比喻成拍照时候的摆pose和按快门两个过程&#xff0c;感觉还是…

CentOs-Stream-9 设置静态IP外网访问

CentOs-Stream-9 设置静态IP&#xff0c;实现外网访问。这里面有些需要注意的地方&#xff0c;比如IP网段跟我们的宿主机不一样&#xff0c;需要查看具体的网络适配器网段&#xff0c;这样可以快速实现网络互通&#xff1b;另外它的网络配置文件也是不一样的。网络适配器对应的…

放弃 startActivityForResult,Activity Result API 优雅使用

放弃 startActivityForResult&#xff0c;Activity Result API 优雅使用 Activity Result API 是 androidx 中的一个新 api&#xff0c;旨在替代原有的 startActivityForResult 方法&#xff0c;用于在两个 Activity 或 Fragment 交换数据、获取返回结果。 过去如果 Activity…

了解独享IP的概念及其独特优势

在网络世界中&#xff0c;IP地址是用来识别和定位设备的标识符。独享IP是一种服务模式。使用代理服务器时&#xff0c;用户拥有一个不与其他用户共享的专用独立IP地址。与共享IP相比&#xff0c;独享IP为用户提供了更高的独立性和隐私保护。下面详细介绍独享IP的定义、工作原理…

OJ在线评测系统 后端 代码沙箱原生实现 初始化项目

代码沙箱Java原生实现 之前我们完成了快速的前端页面开发 重点是在后端 历史问题修复 Java原生代码沙箱实现 docker代码沙箱实现 解决历史遗留问题 代码编辑器切换语言失败 监听language属性 动态更改编辑器的语言 我们在这里实现的是一个线程形式的监听 watch(() > …

总结拓展十一:S4 HANA和ECC区别

第一节 S/4 HANA系统简介 SAP系统的产品线 R/1版本——主要财务模块R/3版本——基本实现全模块ECC6.0——2005年推出&#xff08;ECC是2004年推出&#xff09;HANA——数据库产品——属于内存数据库BW on HANA——HANA与数据分析相结合 拓展&#xff1a; 数据库类型&#x…

易盾滑块验证码

前言 这玩意我就搞定get请求和check请求&#xff0c;那个b接口的d参数还是有点问题&#xff0c;还有就是b接口的返回参数怎么用&#xff0c;是不是只是加了cookie我也不确定&#xff0c;所以有高手的话希望可以指导一下。我的虽然能够成功&#xff0c;但是只有前2次成功&#x…

ARM V8 A32常用指令集

文章目录 1. 算术指令1.1 加法命令ADD\ADDS1.2 带进位加法命令ADC\ADCS1.3减法命令SUB\SUBC1.4带借位减法命令SBC\SBCS 2.逻辑运算指令2.1逻辑与指令AND、ANDS2.2位清零指令BIC2.3逻辑或指令ORR\ORRS2.4逻辑异或指令2.5 逻辑左移LSL2.6逻辑右移LSR 3.比较指令3.1直接比较指令CM…

2024年华为杯研究生数学建模竞赛C题 波形机理建模+GBDT 完整文章代码|进阶可视化

2024年华为杯研究生数学建模竞赛C题 波形机理建模GBDT 完整文章代码|进阶可视化 全部问题已经更新完成&#xff0c;可视化图表20余张&#xff0c;代码量千余行&#xff0c;实在累到了… 由于篇幅原因&#xff0c;此处放出部分内容供参考~ 完整内容可以从底部名片的群中获取~ …

vue3监听子组件的生命周期

1.Vue3使用vue&#xff0c;vue2使用hook template:<compG vue:mounted"doSomething"></compG>script://监听子组件生命周期let doSomething (e: any) > {console.log("没有啊11", e);}; 2.打印结果

昇思MindSpore进阶教程--轻量化数据处理

大家好&#xff0c;我是刘明&#xff0c;明志科技创始人&#xff0c;华为昇思MindSpore布道师。 技术上主攻前端开发、鸿蒙开发和AI算法研究。 努力为大家带来持续的技术分享&#xff0c;如果你也喜欢我的文章&#xff0c;就点个关注吧 正文开始 在资源条件允许的情况下&#…

【趣学Python算法100例】数制转换

问题描述 给定一个M进制的数x&#xff0c;实现对x向任意一个非M进制的数的转换。 问题分析 要搞定这道题&#xff0c;关键在于学会不同数制之间的转换&#xff0c;主要是二进制、八进制、十六进制和十进制这几种。理解下面这几个概念非常重要&#xff1a; 基数&#xff1a;…

Go基础学习06-Golang标准库container/list(双向链表)深入讲解;延迟初始化技术;Element;List;Ring

基础介绍 单向链表中的每个节点包含数据和指向下一个节点的指针。其特点是每个节点只知道下一个节点的位置&#xff0c;使得数据只能单向遍历。 示意图如下&#xff1a; 双向链表中的每个节点都包含指向前一个节点和后一个节点的指针。这使得在双向链表中可以从前向后或从后…

Docker仓库搭建

目录 一、Docker Hub 二、私有Registry仓库搭建 1、下载并开启仓库镜像registry 2、Registry加密传输 3、建立一个registry仓库 4、为客户端建立证书 5、测试 6、为仓库建立登录认证 三、Harbor仓库搭建 Docker 仓库&#xff08;Docker Registry&#xff09; 是用于存…

8种数值变量的特征工程技术:利用Sklearn、Numpy和Python将数值转化为预测模型的有效特征

特征工程是机器学习流程中的关键步骤&#xff0c;在此过程中&#xff0c;原始数据被转换为更具意义的特征&#xff0c;以增强模型对数据关系的理解能力。 特征工程通常涉及对现有数据应用转换&#xff0c;以生成或修改数据&#xff0c;这些转换后的数据在机器学习和数据科学的…

书生大模型实战营学习[9] OpenCompass 评测 InternLM-1.8B 实践

准备工作 打开开发机&#xff0c;选择cuda11.7环境&#xff0c;A100选择10%&#xff0c;点击创建&#xff0c;然后进入开发机即可&#xff0c;和之前的操作一样。接下来创建环境&#xff0c;下载必要的依赖包 conda create -n opencompass python3.10 conda install pytorch2…

什么是网络安全自动化以及优势与挑战

目录 网络安全自动化的工作原理 网络安全自动化的好处 增强的安全功能 改善表现和姿势 降低安全成本 简化的安全合规性和审计 更好的端点管理 网络安全自动化的挑战 耗时且容易出错的安全流程 可见性降低&#xff0c;风险和成本增加 合规管理 有用的网络安全自动化…

2024年合肥市职业院校技能大赛(中职组)赛 网络安全 竞赛样题

2024年合肥市职业院校技能大赛(中职组)赛 网络安全 竞赛样题 (总分100分) 培训、环境、资料、考证 公众号&#xff1a;Geek极安云科 网络安全群&#xff1a;624032112 网络系统管理群&#xff1a;223627079 网络建设与运维群&#xff1a;870959784 极安云科专注于技能提升&am…

基于nodejs+vue的旅游管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…