哈希的简单介绍

news2024/11/16 9:27:55

unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和unordered_set进行介绍

unordered_map
unordered_map的简单介绍
  1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
  3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
  4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
  5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
  6. 它的迭代器至少是前向迭代器。

相比较于map,首先他们的底层结构不一样,map是红黑树,而unordered系列是由哈希的映射关系来实现的,他的迭代器是无序的,并且是一种单向的迭代器,同时unordered也对[]进行了重载,综合各种情况来说的话,unordered_map比map的性能更好,尤其是find的使用

unordered_map的接口说明

接口说明我们在之前的很多stl容器中都演示过,这里不做过多的介绍

unordered_map的构造

在这里插入图片描述

unordered_map的构容量

在这里插入图片描述

unordered_map的迭代器

由于迭代器是单向的,所以没有rbegin和rend
在这里插入图片描述

unordered_map的元素访问

在这里插入图片描述
注意:
该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V()插入失败,说明key已经在哈希桶中,将key对应的value返回

关于哈希桶我们后面会有介绍

unordered_map的查询

在这里插入图片描述
注意:unordered_map中key是不能重复的,因此count函数的返回值最大为1

unordered_map的修改操作

在这里插入图片描述

unordered_map的桶操作

在这里插入图片描述

unordered_set

关于unordered_set的介绍我就不进行讲解了,都差不多

链接: http://www.cplusplus.com/reference/unordered_set/unordered_set/?kw=unordered_set

关联式容器的底层结构

我们开头在介绍unordered系列的关联式容器时就有提到,之所以他的综合效率比较高,是因为其底层使用了哈希结构。下面我们就要正式展开哈希的讲解

哈希的概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数

这个时候我们就会想,要是可以不经过比较就能找到对应的元素,那岂不是快得很!这个时候哈希表就出来了!

哈希表是通过某种函数(hashFunc哈希函数)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素的一种顺序存储结构。

当向该结构中插入或者搜索元素时只需要对插入或者搜索的元素的关键码进行相对应的计算就可以得到该元素的适合的位置

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数构造出来的结构称为哈希表(Hash Table)(或者称散列表)

举一个例子:
数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
在这里插入图片描述
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

但是根据这个方法,你能想一想有什么问题吗?
如果我插入元素11,那么11%10的结果也是1,也要存到对应的位置,但是这个位置已经有元素1了,怎么办呢?

哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

那么该如何解决这个问题呢?

先不急,我们先把其他的概念了解完

哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理
哈希函数设计原则:
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间,并且哈希函数计算出来的地址能均匀分布在整个空间中

下面我们就对常见的哈希函数进行简单的介绍

常见的哈希函数

1. 直接定址法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

比较适合用于数据范围比较集中的集合,因为每个元素都会有一个位置,如果数据分布比较分散的话就会导致空间的浪费

优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况

2. 除留余数法–(常用)
设散列表中允许的地址数为m取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

该方法是用于数据分布比较分散的集合

3. 平方取中法–(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4. 折叠法–(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5. 随机数法–(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
通常应用于关键字长度不等时采用此法

6. 数学分析法–(了解)
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

哈希冲突的解决

解决哈希冲突两种常见的方法是:闭散列和开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去
那如何寻找下一个空位置呢?

线性探测
在这里插入图片描述

现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

1 通过哈希函数获取待插入元素在哈希表中的位置
2 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

在这里插入图片描述
那删除呢?
如果直接删除计算出来的对应位置的节点,就很有可能删除错误,比如我要删除44,却把4给删除了

为此我们可以标记每个点位,采用伪删除法来删除元素

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE}; 
线性探测的实现

我们用枚举常量标记每一个位置
刚开始将每个位置初始化未EMPTY
插入时先判断该key计算后的对应位置是否为空,如果为空就插入,不为空就放入下一个不为空的位置,删除时直接标记DELETE,伪删除即可

enum State { EMPTY, EXIST, DELETE };

template<class K,class V>
class HashTable
{
	struct Elem
	{
		pair<K, V> _val;
		State _state;
	};

public:

	HashTable(size_t capacity = 3)
		:_ht(capacity)
		, _size(0)
	{
		for (size_t i = 0; i < capacity; i++)
		{
			_ht[i]._state = EMPTY;
		}
	}

	bool Insert(const pair<K, V>& val)
	{
		// 检测哈希表底层空间是否充足
		// _CheckCapacity();
		size_t hashAddr = HashFunc(key);
		// size_t startAddr = hashAddr;
		while (_ht[hashAddr]._state != EMPTY)
		{
			if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first
				== key)
				return false;

			hashAddr++;
			if (hashAddr == _ht.capacity())
				hashAddr = 0;
			/*
			// 转一圈也没有找到,注意:动态哈希表,该种情况可以不用考虑,哈希表中元
 素个数到达一定的数量,哈希冲突概率会增大,需要扩容来降低哈希冲突,因此哈希表中元素是
 不会存满的
			if(hashAddr == startAddr)
				return false;
			*/
		}

		// 插入元素
		_ht[hashAddr]._state = EXIST;
		_ht[hashAddr]._val = val;
		_size++;
		return true;
	}
	int Find(const K& key)
	{
		size_t hashAddr = HashFunc(key);
		while (_ht[hashAddr]._state != EMPTY)
		{
			if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first
				== key)
				return hashAddr;

			hashAddr++;
		}
		return hashAddr;
	}
	bool Erase(const K & key)
	{
		int index = Find(key);
		if (-1 != index)
		{
			_ht[index]._state = DELETE;
			_size++;
			return true;
		}
		return false;
	}


	/*size_t size()const;
	bool empty() const;
	void swap(HashTable<K, V, HF>& ht);*/

private:
	size_t HashFunc(const K& key)
	{
		return key % _ht.capacity();
	}
private:
	vector<Elem> _ht;
	size_t _size;

};

那么什么时候哈希表会进行扩容呢?

我们的AVL树中有一个平衡因子,用来判断这棵树是否符合绝对平衡,那么哈希表中就有一个载荷因子

载荷因子 = 填入表中的元素个数 / 散列表的长度

一般情况下如果载荷因子超过0.7就要进行扩容,至于为什么我也不知道,可能经过了一系列的数学计算吧

而这里的扩容一般都是乘以一个素数,也是经过研究的,为了方便找素数,一办情况下就会有一个素数表
然后定义一个函数取最小的符合条件的素数

size_t GetNextPrime(size_t prime)
{
	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];
}

扩容实现如下:

void CheckCapacity()
{
	if (_size * 10 / _ht.capacity() >= 7)
	{
		HashTable<K, V, HF> newHt(GetNextPrime(ht.capacity));
		for (size_t i = 0; i < _ht.capacity(); ++i)
		{
			if (_ht[i]._state == EXIST)
				newHt.Insert(_ht[i]._val);
		}

		Swap(newHt);
	}
}

线性探测优点:实现非常简单

线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?

下面我们就来了解一个高效且常用的办法:开散列

开散列
开散列概念

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

这个桶就是我们上面提到的哈希桶

这时我们的这个散列就是一个指针数组

在这里插入图片描述
大家就可以发现,每个哈希桶中的元素都是发生了哈希冲突的元素

开散列实现

我们要记住,哈希桶中的元素是不能重复的

由于博主能力有限,大家仔细对代码进行解读

template<class V>
struct HashBucketNode
{
	HashBucketNode(const V& data)
		: _pNext(nullptr), _data(data)
	{}
	HashBucketNode<V>* _pNext;
	V _data;
};
// 本文所实现的哈希桶中key是唯一的
template<class V>
class HashBucket
{
	typedef HashBucketNode<V> Node;
	typedef Node* PNode;
public:
	HashBucket(size_t capacity = 3) : _size(0)
	{
		_ht.resize(GetNextPrime(capacity), nullptr);
	}

	// 哈希桶中的元素不能重复
	PNode* Insert(const V& data)
	{
		// 确认是否需要扩容。。。
  // _CheckCapacity();

		// 1. 计算元素所在的桶号
		size_t bucketNo = HashFunc(data);

		// 2. 检测该元素是否在桶中
		PNode pCur = _ht[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return pCur;

			pCur = pCur->_pNext;
		}

		// 3. 插入新元素
		pCur = new Node(data);
		pCur->_pNext = _ht[bucketNo];
		_ht[bucketNo] = pCur;
		_size++;
		return pCur;
	}

	// 删除哈希桶中为data的元素(data不会重复),返回删除元素的下一个节点
	PNode* Erase(const V& data)
	{
		size_t bucketNo = HashFunc(data);
		PNode pCur = _ht[bucketNo];
		PNode pPrev = nullptr, pRet = nullptr;

		while (pCur)
		{
			if (pCur->_data == data)
			{
				if (pCur == _ht[bucketNo])
					_ht[bucketNo] = pCur->_pNext;
				else
					pPrev->_pNext = pCur->_pNext;

				pRet = pCur->_pNext;
				delete pCur;
				_size--;
				return pRet;
			}
		}

		return nullptr;
	}

	PNode* Find(const V& data);
	size_t Size()const;
	bool Empty()const;
	void Clear();
	bool BucketCount()const;
	void Swap(HashBucket<V, HF>& ht;
	~HashBucket();
private:
	size_t HashFunc(const V& data)
	{
		return data % _ht.capacity();
	}
private:
	vector<PNode*> _ht;
	size_t _size;      // 哈希表中有效元素的个数
};
开散列扩容

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

在增容后,许多以前冲突的元素可能就不会冲突了,所以我们可以根据增容的大小来开辟一个新的开散列,然后把原来的开散列的元素重新插入到新的开散列中,再用swap函数交换即可

void _CheckCapacity()
{
    size_t bucketCount = BucketCount();
    if(_size == bucketCount)
   {
        HashBucket<V, HF> newHt(bucketCount);
        for(size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx)
       {
            PNode pCur = _ht[bucketIdx];
            while(pCur)
           {
                // 将该节点从原哈希表中拆出来
                _ht[bucketIdx] = pCur->_pNext;
                
                // 将该节点插入到新哈希表中
                size_t bucketNo = newHt.HashFunc(pCur->_data);
                pCur->_pNext = newHt._ht[bucketNo];
                newHt._ht[bucketNo] = pCur;
                pCur = _ht[bucketIdx];
           }
       }
        
        newHt._size = _size;
        this->Swap(newHt);
   }
}

好了,今天的分享到这里就结束了,感谢大家的支持!

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

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

相关文章

少儿编程 中国电子学会C++等级考试一级历年真题答案解析【持续更新 已更新82题】

C 等级考试一级考纲说明 一、能力目标 通过本级考核的学生&#xff0c;能对 C 语言有基本的了解&#xff0c;会使用顺序结构、选择结构、循环结构编写程序&#xff0c;具体用计算思维的方式解决简单的问题。 二、考核目标 考核内容是根据软件开发所需要的技能和知识&#x…

深度学习_18_模型的下载与读取

在深度学习的过程中&#xff0c;需要将训练好的模型运用到我们要使用的另一个程序中&#xff0c;这就需要模型的下载与转移操作 代码&#xff1a; import math import torch from torch import nn from d2l import torch as d2l import matplotlib.pyplot as plt# 生成随机的…

私有化部署自己的ChatGPT,免费开源的chatgpt-next-web搭建

随着AI的应用变广&#xff0c;各类AI程序已逐渐普及&#xff0c;尤其是在一些日常办公、学习等与撰写/翻译文稿密切相关的场景&#xff0c;大家都希望找到一个适合自己的稳定可靠的ChatGPT软件来使用。 ChatGPT-Next-Web就是一个很好的选择。它是一个Github上超人气的免费开源…

新零售SaaS架构:订单履约系统的概念模型设计

订单履约系统的概念模型 订单&#xff1a;客户提交购物请求后&#xff0c;生成的买卖合同&#xff0c;通常包含客户信息、下单日期、所购买的商品或服务明细、价格、数量、收货地址以及支付方式等详细信息。 子订单&#xff1a;为了更高效地进行履约&#xff0c;大订单可能会被…

安卓开发:计时器

一、新建模块 二、填写应用名称和模块名称 三、选择模块&#xff0c;Next 四、可以保持不变&#xff0c;Finish 五、相关目录文件 六、相关知识 七、&#xff1f;

正大国际:期货结算价是如何理解呢?结算价有什么作用?

如何理解期货结算价&#xff1a; 什么是商品期货当日结算价&#xff0c; 商品期货当日结算价是指某一期货合约当日交易期间成交价格按成交量的加权平均价。当日 无成交的&#xff0c;当日结算价按照交易所相关规定确定。 股指期货当日结算价是指某一期货合约当日交易期间最后一…

采购软件是如何改善采购周期?

采购是一个复杂的职能重叠网络&#xff0c;由市场分析、供应商选择、发布 RPF/RFQ、合同谈判等多个工作流程组成。此外&#xff0c;时间紧迫、满足客户期望等压力也使这项工作极具挑战性。因此&#xff0c;如果企业在采购过程中采取短视的方法&#xff0c;没有遵循适当的结构&a…

Pygame教程02:图片的加载+缩放+旋转+显示操作

------------★Pygame系列教程★------------ Pygame教程01&#xff1a;初识pygame游戏模块 Pygame教程02&#xff1a;图片的加载缩放旋转显示操作 Pygame教程03&#xff1a;文本显示字体加载transform方法 Pygame教程04&#xff1a;draw方法绘制矩形、多边形、圆、椭圆、弧…

海王星(Neptune)系列和大禹(DAYU)系列OpenHarmony智能硬件配置解决方案

海王星&#xff08;Neptune&#xff09;系列和大禹&#xff08;DAYU&#xff09;系列OpenHarmony智能硬件对OS的适配、部件拼装配置、启动配置和文件系统配置等。产品解决方案的源码路径规则为&#xff1a;vendor/{产品解决方案厂商}/{产品名称}_。 解决方案的目录树规则如下&…

React__ 二、React状态管理工具Redux的使用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言redux状态管理安装redux创建文件 并使用传参action 总结 前言 redux状态管理插件的使用 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考…

Typescript 哲学 morn on funtion

函数重载 overload 有一些编程语言&#xff08;eg&#xff1a;java&#xff09;允许不同的函数参数&#xff0c;对应不同的函数实现。但是&#xff0c;JavaScript 函数只能有一个实现&#xff0c;必须在这个实现当中&#xff0c;处理不同的参数。因此&#xff0c;函数体内部就…

【系统需求分析报告-项目案例直接套用】

软件需求分析报告 软件开发要求项目建设内容物理设计安全系统设计安全网络安全设计应用安全设计用户安全管理性能设计稳定性设计安全性设计兼容性设计易操作性设计可维护行设计 软件开发全套精华资料过去进主页领取。

10亿数据如何快速插入MySQL

最快的速度把10亿条数据导入到数据库,首先需要和面试官明确一下,10亿条数据什么形式存在哪里,每条数据多大,是否有序导入,是否不能重复,数据库是否是MySQL? 有如下约束 10亿条数据,每条数据 1 Kb 数据内容是非结构化的用户访问日志,需要解析后写入到数据库 数据存放在…

2024新版SonarQube+JenKins+Github联动代码扫描(2)-SonarQube代码扫描

文章目录 前言一、docker方式安装sonar二、启动容器三、创建数据库四、启动sonarqube五、访问sonar六、如果访问报错-通过sonar日志定位问题七、修改密码八、汉化&#xff08;看个人选择&#xff09;九、扫描十、我遇到的Sonar报错以及解决办法 总结 前言 这是2024新版SonarQu…

【OpenGL编程手册08】 摄像机

一、说明 前面的教程中我们讨论了观察矩阵以及如何使用观察矩阵移动场景&#xff08;我们向后移动了一点&#xff09;。OpenGL本身没有摄像机(Camera)的概念&#xff0c;但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机&#xff0c;产生一种我们在移动的感…

关于python函数参数传递

参数传递 在 python 中&#xff0c;类型属于对象&#xff0c;对象有不同类型的区分&#xff0c;变量是没有类型的&#xff1a; 在下面的代码示例重&#xff0c;[1,2,3] 是 List 类型&#xff0c;“qayrup” 是 String 类型&#xff0c;而变量 a 是没有类型&#xff0c;它仅仅…

PyTorch深度学习实战(38)——StyleGAN详解与实现

PyTorch深度学习实战&#xff08;38&#xff09;——StyleGAN详解与实现 0. 前言1. StyleGAN1.1 模型介绍1.2 模型策略分析 2. 实现 StyleGAN2.1 生成图像2.2 风格迁移 小结系列链接 0. 前言 StyleGAN (Style-Generative Adversarial Networks) 是生成对抗网络 (Generative Ad…

使用AI创建令人惊叹的3D模型

老子云平台《《《《《 使内容创作者能够在一分钟内毫不费力地将文本和图像转换为引人入胜的 3D 资产。 文本转 3D 我们的文本转 3D 工具使创作者&#xff08;包括那些没有 3D 经验的创作者&#xff09;能够使用文本输入在短短一分钟内生成 3D 模型。 一句话生成3D模型 老子…

Day31|贪心算法1

贪心的本质是选择每一阶段的局部最优&#xff0c;从而达到全局最优。 无固定套路&#xff0c;举不出反例&#xff0c;就可以试试贪心。 一般解题步骤&#xff1a; 1.将问题分解成若干子问题 2.找出适合的贪心策略 3.求解每一个子问题的最优解 4.将局部最优解堆叠成全局最…

Unity2023.1.19_ECS_DOTS

Unity2023.1.19_ECS_DOTS 盲学-盲目的学习&#xff1a; 懒着自己整理就看看别人整理的吧&#xff0c;整合一下逻辑通了不少&#xff1a; DOTS/data oriented technology stack-面向数据的技术栈 ECS/Entities-Component-System Unity-Entities包 Entities提供ECS架构面向数…