哈希表(底层结构剖析--下)

news2024/12/23 16:36:17

文章目录

  • 开散列哈希桶的模拟实现
    • 哈希桶的基本框架
    • 增加仿函数将数据类型转换为整型
    • 哈希桶的插入函数
    • 哈希桶的删除函数
    • 哈希桶的查找函数
    • 哈希桶的析构函数
    • 建议哈希表的大小为素数
  • 开散列与闭散列比较
  • 哈希桶的时间复杂度及其测试
  • 开散列哈希桶的模拟实现完整代码

开散列哈希桶的模拟实现

哈希桶的基本框架

由于哈希桶的本质就是一个存有结点指针的数组.所以哈希桶存储的数据类型便是结点指针类型.
哈希结点中包括两个内置成员:
1:K,V模型组成键值对pair<K,V>.(也可以是K模型)

2: 指向下一个结点的指针.

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

哈希桶的本质是一个指针数组.
其内置成员有两个:
1: 装有结点指针的vector.

2: 记录哈希桶的有效数据个数_size.

template < class K, class V>
struct HashTable
{
    public:
      //...
    private:
     vector<Node*> _table;   //哈希桶.
	 size_t _size = 0;       //记录哈希桶有效数据个数.
}
    

增加仿函数将数据类型转换为整型

此处仿函数与闭散列的仿函数一致并且接下来的插入,查找,删除等函数都调用仿函数,以下为仿函数代码:

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  val = 0;
		for ( auto& ch : key ) //遍历string,将一个个字母替换成ASCI码进行相加.
		{ 
		    val *= 131;
			val += ch;
		}
		return val;
	}
};

template < class K, class V,class Hash = HashFunc<K>>   //模板参数增加一个仿函数.
struct HashTable
{
    public:
      //...
    private:
     vector<Node*> _table;   //哈希桶.
	 size_t _size = 0;       //记录哈希桶有效数据个数.
}

哈希桶的插入函数

哈希桶的插入主要分为3个步骤:
1: 去重.

2: 扩容

3: 插入.

以下就这3个步骤展开详谈:
1: 去重
(1)调用find函数,如果找到了,就说明哈希表中已经有了该数据,不需要插入,返回false.

(2)如果没找到,就将该数据插入.

2: 扩容
(1):计算新表的长度,创建新表.

(2): 遍历旧表,将旧表的数据按照头插的方式插入到哈希桶中.

(3): 调用vector中的swap函数,新表与旧表交换.

3: 插入
(1): 通过哈希函数计算映射位置.

(2) 将旧表数据头插移入到新表中.

(3) 更新_size.
在这里插入图片描述

以下为插入函数完整代码:

bool Insert( const pair<K, V>& kv )
		{
			if (Find(kv.first))                         //去重
			{
				return false;
		 	}
            //方法一:                               调用Insert函数
           //HashTable<K, V> newHT; 
          //newHT._table.resize(_table.size());
          //for (auto cur : _table)
          //{
          //    while (cur)
         //    {
         //        newHT.Insert(cur->_kv);
         //        cur = cur->_next;
        // }
                  //}

           // _table.swap(newHT._table);
            //方法二:                                //直接将旧表数据移入到旧表中.
			if (_size == _table.size())                                //扩容.
			{
			//	size_t newSize = _table.size() == 0 ? 10 : 2 * _table.size();
				vector<Node*> newTable;
				newTable.resize(GetNextPrime(_table.size()), nullptr);    //扩容

				Hash hash;
				//从旧表结点移动映射新表.
				for (size_t i = 0; i < _table.size(); ++i)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hash( cur->_kv.first) % newTable.size();
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}
					_table[i] = nullptr;


				}
				_table.swap(newTable);


			}
			Hash hash1;
			size_t hashi = hash1(kv.first) % _table.size();
			Node* newNode = new Node(kv);
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;
			++_size;
			return true;
		}

注意:
方法一调用了Insert函数这样虽然可以解决创建新的哈希表导致每个数据的位置打乱,导致要重新计算插入位置的问题.可是,通过insert复用的办法,是生成了新的结点插入,函数栈帧结束后,旧表的数据结点通过调用析构函数销毁,可是,这样就会造成老的结点没有被充分运用的问题.

哈希桶的删除函数

能不能传pair键值对,调用find找到该结点进行删除呢?

不能,因为我们所设计的哈希桶为单链表(节省内存),find只能找到目标结点的位置,不能找到前一个结点的位置.因此,删除后删除结点的前后结点不能顺利连接.

删除函数主要分为3个步骤:
(1): 通过哈希函数确定删除目标结点哈希桶位置.

(2): 对哈希桶进行遍历,寻找删除目标结点.

(3)如果找到就删除该结点,如果没找到,就保留当前结点的位置,查找下一个结点.

在这里插入图片描述

	bool  Erase(const K& key)
		{
			if (_table.size() == 0)
			{
				return true;
			}
			Hash hash;
			size_t hashi = hash(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur)                      //找到哈希桶的位置,开始寻找目标结点.
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					--_size;
					return true;
				}

				prev = cur;                //没找到就保存这个结点
				cur = cur->_next;         
			}
			return false;
		}

哈希桶的查找函数

哈希桶的查找步骤如下:
(1): 如果表为空,表明找不到了,返回空.

(2): 通过哈希函数计算目标结点哈希桶的位置.

(3): 遍历哈希桶,如果找到,返回当前结点指针,没找到返回空.

Node* Find(const K& key)
		{
			if (_table.size() == 0)        //表为空,就返回nullptr.
			{
				return nullptr;
			}
			Hash hash;
			size_t hashi = hash(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if ( cur->_kv.first == key )
				{
					return cur;
				}
				cur = cur->_next;

			}
			return nullptr;

		}

哈希桶的析构函数

当函数栈帧结束时,由于哈希表会调用它自己的析构函数,所以我们就不用对它额外显示写了.
但是,哈希桶中的结点没有析构函数无法析构,此时则需要我们额外对哈希桶显示写析构函数.
析构函数步骤如下:
(1): 遍历哈希表,找到每个哈希桶的头指针依次对哈希桶进行遍历删除(在删除时要提前保留下一个节点地址),直到整个表中的哈希桶全部删除.
(2) 删除完一个哈希桶就将该哈希桶的头指针设为nullptr.


		~HashTable()         //vvector会调用析构函数,但是哈希桶必须自己写.
		{
			for (size_t i = 0; i < _table.size(); ++i)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		//	cout << "~HushTable()" << endl;

		}

建议哈希表的大小为素数

根据实验显示,使用除留余数法时,哈希表的大小最好是素数,这样能够降低哈希冲突概率. 可是怎么才能快速取一个类似两倍关系的素数?

STL中特意创建了一个素数数组,当插入函数需要扩容时,则遍历素数数组找到第一个大于哈希表中的数据个数的素数,这个素数就为新表的大小.

size_t GetNextPrime(size_t prime)
		{
			const int PRIMECOUNT = 28;
			//素数序列
			const size_t primeList[PRIMECOUNT] =
			{
					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
			};
			size_t i = 0;
			for (i = 0; i < PRIMECOUNT; i++)
			{
				if (primeList[i] > prime)         //从这个数组里面取第一个大于prime的值.
					return primeList[i];
			}
			return primeList[i];                 //虽然数据不可能那么大,但是也有可能不会走if判断,
			                                     // 所以从语法上来说还要考虑所给值prime大于素数数组整个数据的情况.
		}

注意:
一般来讲,因为内存限制,我们不太可能需要一个最大素数大小的哈希表,但是有可能我们传的值会比最大素数还大.但是,从语法上考虑,当这种情况发生,我们应该也要返回一个值,这里返回素数数组中最大素数.

开散列与闭散列比较

应用链地址法(开散列)处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于闭散列必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多(闭散列存储数据类型为pair键值对与状态构成的对象所以比指针大的多),所以使用链地址法(开散列)反而比闭散列节省存储空间

哈希桶的时间复杂度及其测试

我们知道,在最坏情况下,所给的数据都发生哈希冲突,此时的时间复杂度为O(N),但是这种情况出现的概率极低,那么哈希桶的时间复杂度是多少呢?

哈希桶随机数测试:
但测试数据为100000个时,此时负载因子为0.677594,其中在62681个桶中最长的桶的长度才为2,平均每个桶的长度才为1.06283.
在这里插入图片描述
当测试数据为200000个时,此时负载因子为0.660307可见和上次相比这次测试已经扩了容,但是最长的桶的长度也仅为2,平均桶的长度为1.03515.
在这里插入图片描述
综合以上情况: 在平均情况下,哈希表中每个位置的哈希桶的数据个数大概为常数个,所以时间复杂度一般为O(1).

开散列哈希桶的模拟实现完整代码

#include <map>
#include <vector>
#include <string>
#include <iostream>
using namespace std;                     
namespace HashBucket
{
	template < class K >                   //仿函数的默认值,如果不显示传就会默认调用.
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	template< >                                 //1:对于常见类型,为了方便,我们可以对类模板进行特化.
	struct HashFunc <string>                    //并且根据实参所传类型,优先走特化版本.	                                     
	{
		size_t operator()(const string& key)
		{
			size_t  val = 0;
			for (auto& ch : key) //遍历string,将一个个字母替换成ASCI码进行相加.
			{
				val += ch;
			}
			return val;
		}
	};
	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> >
	struct HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		size_t GetNextPrime(size_t prime)
		{
			const int PRIMECOUNT = 28;
			//素数序列
			const size_t primeList[PRIMECOUNT] =
			{
				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
			};
			size_t i = 0;
			for (i = 0; i < PRIMECOUNT; i++)
			{
				if (primeList[i] > prime)         //从这个数组里面取第一个大于prime的值.
					return primeList[i];
			}
			return primeList[i];                 //虽然数据不可能那么大,但是也有可能不会走if判断,
			                                     // 所以从语法上来说还要考虑所给值prime大于素数数组整个数据的情况.
		}

		~HashTable()         //vvector会调用析构函数,但是哈希桶必须自己写.
		{
			for (size_t i = 0; i < _table.size(); ++i)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		//	cout << "~HushTable()" << endl;

		}

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

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

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))                         //去重
			{
				return false;
			}

			if (_size == _table.size())                                //扩容.
			{
			//	size_t newSize = _table.size() == 0 ? 10 : 2 * _table.size();
				vector<Node*> newTable;
				newTable.resize(GetNextPrime(_table.size()), nullptr);    //扩容

				Hash hash;
				//从旧表结点移动映射新表.
				for (size_t i = 0; i < _table.size(); ++i)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hash( cur->_kv.first) % newTable.size();
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}
					_table[i] = nullptr;


				}
				_table.swap(newTable);


			}
			Hash hash1;
			size_t hashi = hash1(kv.first) % _table.size();
			Node* newNode = new Node(kv);
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;
			++_size;
			return true;
		}

		Node* Find(const K& key)
		{
			if (_table.size() == 0)        //表为空,就返回nullptr.
			{
				return nullptr;
			}
			Hash hash;
			size_t hashi = hash(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if ( cur->_kv.first == key )
				{
					return cur;
				}
				cur = cur->_next;

			}
			return nullptr;

		}

		size_t size()                  //哈希表的数据个数
		{
			return _size;
		}
		size_t TablesSize()          //表的长度
		{
			return  _table.size();
		}
		
		size_t BucketNum()           //有多少个桶被用了.
		{
			size_t Num = 0;
			for (size_t i = 0; i < _table.size(); ++i)
			{
				if (_table[i])
				{
					++Num;
				}
			}
			return Num;
		}
		size_t MaxBucketLenth()          //哈希桶的最大桶长
		{
			size_t maxLen = 0;
			for (size_t i = 0; i < _table.size(); ++i)
			{
				size_t len = 0;
				Node* cur = _table[i];
				while (cur)
				{
					++len;
					cur = cur->_next;
				}
				if ( len > maxLen )
				{
					maxLen = len;
				}
			}
			return maxLen;
		}
	private:
		vector<Node*> _table;
		size_t _size = 0;
	};
	void TestHT3()                          //哈希桶时间复杂度测试.
	{

		int n = 200000;
		vector<int> v;
		v.reserve(n);
		srand(time(0));
		for (int i = 0; i < n; ++i)
		{
			//v.push_back(i);
			v.push_back(rand() + i);  // 重复少
			//v.push_back(rand());  // 重复多
		}

		size_t begin1 = clock();
		HashTable<int, int> ht;
		for (auto e : v)
		{
			ht.Insert(make_pair(e, e));
		}
		size_t end1 = clock();

		cout << "数据个数:" << ht.size() << endl;
		cout << "表的长度:" << ht.TablesSize() << endl;
		cout << "桶的个数:" << ht.BucketNum() << endl;
		cout << "平均每个桶的长度:" << (double)ht.size() / (double)ht.BucketNum() << endl;
		cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
		cout << "负载因子:" << (double)ht.size() / (double)ht.TablesSize() << endl;
	}
}

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

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

相关文章

Java -- ELK之从nacos获取logback.xml配置信息

背景&#xff1a; 之前本地搭建好ELK后&#xff0c;随便起一个项目&#xff0c;可正常日志上送&#xff0c;但是后面我把elk部署到测试环境中发现&#xff0c;本地项目的日志就无法正常上送了&#xff0c;之前我是把上送地址配置到nacos上的&#xff0c;logback.xml中读取nacos…

项目管理中的必不可少的强大工具有哪些?

在项目管理中&#xff0c;我们总是想寻求一套功能强大的工具&#xff0c;来满足我们多样化的需求。但往往事与愿违&#xff0c;这样强大的工具总是费用高&#xff0c;操作复杂&#xff0c;需安装多个插件。下面&#xff0c;我就给大家推荐一款项目管理软件 ~Zoho Projects&…

Transformer 位置编码代码解析

Transformer 位置编码代码解析 Transformer 的 Multi-Head-Attention 无法判断各个编码的位置信息。因此 Attention is all you need 中加入三角函数位置编码&#xff08;sinusoidal position embedding&#xff09;&#xff0c;表达形式为&#xff1a; P E ( p o s , 2 i ) …

超强版干货投递!Milvus 的部署心得、运维秘籍都在这里了!

好奇 Milvus 读链路的演进&#xff1f;不知如何优化 Milvus&#xff1f;提到 Milvus 的业务场景只能靠想象&#xff1f;想获得其他人的部署经验&#xff1f;困惑于 Zilliz Cloud&#xff1f; 不藏了&#xff0c;摊牌了&#xff0c;对于上述的所有问题&#xff0c;你都可以在今天…

【Leetcode -206.反转链表 -876.链表的中间结点】

Leetcode Leetcode -206.反转链表Leetcode-876.链表的中间结点 Leetcode -206.反转链表 题目&#xff1a;给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1, 2, 3, 4, 5] 输出&#xff1a;[5…

移动端性能优化:GPU加速、图片优化与缓存策略

在移动端开发中&#xff0c;性能优化是一个至关重要的环节。本文将为您介绍如何通过 GPU 加速、图片优化和缓存策略来提高移动端性能。让我们开始吧&#xff01; 1. GPU 加速 在移动设备上&#xff0c;GPU 能够快速完成图形渲染任务。我们可以通过 CSS 属性来实现 GPU 加速&a…

单链表C语言实现 (不带头 带头两个版本)

链表就是许多节点在逻辑上串起来的数据存储方式 是通过结构体中的指针将后续的节点串联起来 typedef int SLTDataType;//数据类型 typedef struct SListNode//节点 {SLTDataType data;//存储的数据struct SListNode* next;//指向下一个节点地址的指针 }SLTNode;//结构体类型的…

机器学习材料性能预测与材料基因工程应用实战

一、背景: 传统的材料研发技术是通过实验合成表征对材料进行试错和验证&#xff0c;而过去的计算手段受限于算法效率&#xff0c;无法有效求解实际工业生产中面临的复杂问题。近几年随着大数据和人工智能介入&#xff0c;通过采用支持向量机、神经网络等机器学习算法训练数据集…

Vue核心 MVVM模型 数据代理

1.6.MVVM 模型 MVVM模型 M&#xff1a;模型 Model&#xff0c;data中的数据V&#xff1a;视图 View&#xff0c;模板代码VM&#xff1a;视图模型 ViewModel&#xff0c;Vue实例 观察发现 data中所有的属性&#xff0c;最后都出现在了vm身上vm身上所有的属性及Vue原型身上所有…

用友nc6 如果用户长时间没有任何操作,如何设置会话的失效时间?

1.web应用(新开的) NC中间件环境下的web profile和NC中间件没有关系&#xff0c;NC中间件只不过是个J2EE运行环境&#xff0c;是个Container&#xff0c;当你的web项目启动后&#xff0c;NC中间件创建web容器&#xff0c;其web应用的会话超时时间由你的web部署描述符&#xff…

电脑卡顿反应慢怎么办?这几招教给你!

电脑使用时间长了&#xff0c;电脑中的各种缓存文件也会就越来越多&#xff0c;这些文件的堆积会占用电脑内存从而导致电脑变得卡顿。还有在电脑中安装了许多软件&#xff0c;若这些软件都设置为开机自启动&#xff0c;这会占用大量的电脑内存&#xff0c;影响电脑的运行速度&a…

PMP项目管理备考资料都有哪些?

当今复杂多变的项目管理环境中&#xff0c;项目管理从业者在各种各样的项目环境中工作&#xff0c;一定会采用不同的项目方法。PMP认证试图覆盖业界所有有效的项目管理方法&#xff0c;PMP考试范围会覆盖预测型生命周期&#xff08;即瀑布式开发模式&#xff09;为代表的项目管…

什么是 MVVM?MVVM和 MVC 有什么区别?什么又是 MVP ?

目录标题 一、什么是MVVM&#xff1f;二、MVC是什么&#xff1f;三、MVVM和MVC的区别&#xff1f;四、什么是MVP&#xff1f; 一、什么是MVVM&#xff1f; MVVM是 Model-View-ViewModel的缩写&#xff0c;即模型-视图-视图模型。MVVM 是一种设计思想。 模型&#xff08;Model…

PerformanceTest, monitoring command

PerformanceTest, monitoring command 1、数据库 #查看最大连接数 show variables like max_connections; #例如:查看mysql连接数 show status like Threads%; 说明: threads_cached //查看线程缓存内的线程的数量 threads_connected //查看当前打开的连接的数量(打开的…

【Linux】6、在 Linux 操作系统中安装软件

目录 一、yum 命令二、安装 wget 一、yum 命令 类似 Linux 中的应用商店 &#x1f4c3;① yum 是 RPM 软件包管理器 ✏️ Red-Hat Package Manager &#x1f4c3;② yum 用于自动化安装、配置 Linux 软件&#xff08;可自动解决依赖问题&#xff09; &#x1f4c3;③ 语法&a…

面试2个月没有一个offer?阿里技术官的800页知识宝典打破你的僵局~

在经历了一波裁员浪潮后&#xff0c;大环境似乎有所好转&#xff0c;但对于面试者来说&#xff0c;面试愈发困难&#xff0c;现在面试官动不动就是底层原理&#xff0c;动不动就是源码分析&#xff0c;面试一定会抓你擅长的地方&#xff0c;一直问&#xff0c;问到你不会为止。…

MySQL之内置函数

目录 一 日期函数 主要实现的功能&#xff1a; 主要函数&#xff1a; 示例&#xff1a; 应用 二 字符串函数 主要实现的功能 1转换或者显示相关 2切割&#xff0c;插入&#xff0c;替换&#xff0c;连接&#xff0c;比较等功能性质的 3 其他 三 数学函数 1 运算 2 …

MySQL-运算符的使用解析

运算符的使用解析 1 运算符概述2 算数运算符3 比较运算符3.1 等于运算符&#xff08;&#xff09;3.2 安全等于运算符&#xff08;<>&#xff09;3.3 不等于运算符&#xff08;<> 或者 &#xff01;&#xff09;3.4 小于等于运算符&#xff08;<&#xff09;3.5…

Jmeter基础教程合集

环境搭建 1.安装java 8.0以上版本 2.下载jmeter并安装。安装参考网址&#xff1a;https://blog.csdn.net/wust_lh/article/details/86095924 3.打开JMeter中bin目录下面的jmeter.bat文件即可打开JMeter了&#xff0c;打开的时候会有两个窗口&#xff0c;Jmeter的命令窗口和Jme…

【数据结构】哈希表——闭散列 | 开散列(哈希桶)

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《数据结构与算法》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 哈希表 &#x1f3af;哈希&#x1f94a;直接定址法&#x1f94a;除留余数法&#x1f94a;哈…