Unity 3D常用的数据结构

目录

数组

使用场景

元素的数量是固定的,并且需要使用下标时。

ArrayList数组

为了解决Array创建时必须指定长度,以及只能存放相同类型的缺点而推出的数据结构。

ArrayList的缺点
  • ArrayList是类型不安全的。因为把不同的类型都当作Object来做处理,很有可能会在使用ArrayList时发生类型不匹配的情况。
  • 数组存储值类型时并未发生装箱,但是ArrayList由于把所有类型都当作了Object,所以不可避免的是当插入值类型时会发生装箱操作,在索引取值时会发生拆箱操作。因此在频繁读写 ArrayList 时会产生额外的开销,导致性能下降。

List<T>数组

可以认为List<T> 类是 ArrayList 类的泛型等效类。

List<T>有以下3点好处
  1. 即确保了类型安全。因此List<T>是类型安全的。
  2. 取消了装箱和拆箱的操作,以及由于引入泛型而无需运行时类型检查。因此List<T>是高性能的。
  3. 融合了Array可以快速访问的优点,以及ArrayList长度可以灵活变化的优点。

链表

链表与数组的不同之处

数组中的内容在内存中是连续排列的,可以通过下标来访问。

链表中内容的顺序则是由各个对象的指针所决定的,这就决定了其内容的排列不一定是连续的,所以不能通过下标来访问。

链表的优势

使用链表最主要的优势 就在于向链表中插入或删除节点时,无需考虑调整结构的容量。相反的对于数组来说容量始终是固定的,且数组中的内容在内存中是连续的。因此如果需要存放更多的数据,则面临着需要调整数组容量的现实,这就会引发新建数组、数据拷贝等一系列复杂且影响效率的操作。即使是List<T>类,虽然其对开发人员隐藏了容量调整的复杂性,但实质上性能的损耗是必须考虑的。

数组和链表的应用场景

数组 适合数据的数量是有上限 ,且需要快速访问其元素 内容的情况.
链表 适合元素数量不固定 且需要经常增删结点的情况。

LinkedList<T>

C#中内置的双向链表LinkedList

在Unity 3D开发过程中,由于C#已经为开发者封装了一个对应链表的类------LinkedList<T>类。因此可以很方便地通过LinkedList<T>来实现链表的功能。而和LinkedList<T>类相配套的,C#还提供了链表的结点类------LinkedListNode<T>类以用来代表链表中的结点,LinkedList<T>对象中的每个节点都属于LinkedListNode<T>类型。由于LinkedList<T>是双向链表 ,因此每个节点向前指向Next节点向后指向Previous节点

需要说明的一点是,LinkedList<T>类的插入和移除的运算复杂度都是O(1)。而由于该列表还维护内部计数,因此获取Count属性的运算复杂度也为 O(1)。

如何创建一个链表LinkedList<T>,以及最常见的几种操作。

  • AddFirst,将一个新结点加入该链表的第一个结点的位置;
  • RemoveFirst,将第一个结点移除;
  • AddLast,将一个新节点加入该链表最后一个结点的位置;
  • 以及在某个结点前后插入新的结点的AddBefore和AddAfter方法。
  • 对链表中的结点类LinkedListNode的各种操作。
使用场景

元素需要能够在列表的两端添加时。否则使用List<T>。

队列(Queue<T>)和栈(Stack<T>)

queue队列
内部实现

在Queue<T>内部,有一个存放类型为T的对象的环形数组 ,并通过head 和tail变量来指向该数组的头和尾 。当使用Enqueue方法将新的元素入列时,会判断队列的长度是否足够。若不足,则依据增长因子来增加容量,例如当为初始的2.0时,则队列容量增长2倍。

在默认情况下,Queue<T>的初始化容量是32 ,但是也可以通过构造函数指定容量

元素的进出顺序是先进先出(FIFO)

栈(Stack)又名堆栈,它和队列一样是一种运算受限的线性表

其限制是仅允许在表的一端进行插入和删除的操作运算。这一端称为栈顶,相对的,把另一端称为栈底。

向一个栈插入新元素称为进栈、入栈或压栈。

一个栈删除元素称为出栈或退栈,它是把栈顶元素删除,使其相邻的元素成为新的栈顶元素。

元素的进出顺序是后进先出(LIFO)

内部实现

内部同样使用了数组来实现。内部结构可以通过一个垂直的数组来形象的表示。

Hashtable哈希表

哈希表(Hash Table,也叫散列表),是根据关键码/值(Key/value)而直接进行访问的数据结构。也就是说,它通过把关键码/值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫哈希函数或散列函数,存放记录的数组就叫哈希表。

如何处理哈希冲突

处理哈希冲突时,也有两种思路------避免解决

  • 冲突避免机制(Collision Avoidance)
  • 冲突解决机制(Collision Resolution)
避免哈希冲突

避免哈希冲突的一个方法就是尽可能先择合适的哈希函数。

解决哈希冲突
  1. 将要插入的元素放到另一块空间中,因为相同的哈希位置已经被占用了。
  2. 开放寻址法(Open Addressing)
开放寻址法的简单实现------线性探查(Linear Probing)
  1. 当插入新的元素时,使用哈希函数在哈希表中定位元素位置。
  2. 检查哈希表中该位置是否已经存在元素。如果该位置内容为空,则插入并返回,否则进行步骤3的操作。
  3. 如果该位置为i,则检查i+1是否为空。如果已被占用,则检查i+2。依此类推,直到找到一个内容为空的位置。

线性探查(Linear Probing)方式虽然简单,但并不是解决冲突的最好策略,因为它会导致同类哈希的聚集(Primary Clustering)。这会导致搜索哈希表时,冲突依然存在。

针对线性探查方式所存在的问题,一种改进的方式为二次探查(Quadratic Probing)

即每次检查位置空间的步长为平方倍数。也就是说,如果位置s被占用,则首先检查s+12处,然后检查s-12、s+22、s-22、s+32...以此类推,而不是像线性探查那样以s+1、s+2...方式增长。

尽管如此,二次探查同样也会导致同类哈希聚集问题(Secondary Clustering)。

二度哈希

当在哈希表中添加或获取一个元素时,会发生哈希冲突。前面简单地介绍了两种冲突解决策略,即线性探查(Linear Probing)和二次探查(Quadratic Probing)。

二度哈希使用了Θ(m2)种探查序列,而线性探查(Linear Probing)和二次探查(QuadraticProbing)使用了Θ(m)种探查序列,因此二度哈希提供了更好的避免冲突的策略。

二度哈希的工作原理

有一个包含一组哈希函数H1...Hn的集合。当需要从哈希表中添加或获取元素时,首先使用哈希函数H1。如果导致冲突,则尝试使用H2。以此类推,直到Hn。所有的哈希函数都与H1十分相似,不同的是它们选用的乘法因子(multiplicative factor)。

当使用二度哈希时,重要的是在执行了hashsize次探查后,哈希表中的每一个位置都有且只有一次被访问到。也就是说,对于给定的key,对哈希表中的同一位置不会同时使用H1和H2 。在Hashtable类中使用二度哈希公式,其始终保持(1 +((GetHash(key) >> 5) + 1) %(hashsize - 1)hashsize互为素数 (两数互为素数表示两者没有共同的质因子)

loadFactor

Hashtable类中还包含了一个私有成员变量loadFactor,loadFactor指定了哈希表中元素数量与位置(slot)数量之间的最大比例。 例如,如果loadFactor 等于0.5,则说明哈希表中只有一半的空间存放了元素值,其余一半都为空。

哈希表的构造函数允许用户指定loadFactor值,定义范围为0.1至1.0。然而不管提供的值是多少,范围都不会超过72%。即使传递的值为1.0,Hashtable类的loadFactor值还是0.72。微软官方认为loadFactor的最佳值为0.72,这平衡了速度与空间。因此,虽然默认的loadFactor为1.0,但系统内部却自动地将其改变为0.72。所以,建议使用缺省值1.0(但实际上是 0.72)。

Hashtable类的实例中添加新元素时,需要检查以保证元素与空间大小的比例不会超过最大比例。如果超过了,Hashtable类实例的空间将被扩充。空间扩充的步骤如下
  1. Hashtable类实例的位置空间几乎被翻倍。准确地说,位置空间值从当前的素数值增加到下一个最大的素数值。
  2. 因为二度哈希时,Hashtable类实例中的所有元素值将依赖于Hashtable类实例的位置空间值,所以Hashtable类实例中保存的所有值也需要重新二度哈希。

Dictionary<K,T>字典

Dictionary<K,T>使用强类型来限制Key和Item,当创建Dictionary<K,T>实例时,必须指定Key和Item的类型。

冲突解决机制

Dictionary<K,T>还采用了不同的冲突解决策略(Collision Resolution Strategy),这种技术称为链接技术(Chaining)。

链接技术(Chaining)将采用额外的数据结构来处理冲突。Dictionary<K,T>中的每个位置(slot)都映射到了一个链表。当冲突发生时,冲突的元素将被添加到桶(bucket)列表中。

Dictionary<K,T>类的缺点

它的缺点就是空间。以空间换时间 ,通过更多的内存开销来满足对速度的追求 。在创建字典时,可以传入一个容量值,但实际使用的容量并非该值。而是使用不小于该值的最小质数作为它使用的实际容量,容量的最小值是 3。当有了实际容量后,并非直接实现索引,而是通过创建额外的两个数组来实现间接索引,即int[] buckets和Entry[] entries两个数组。因此面临的情况就是,即便新建了一个空的字典,那么伴随而来的是两个长度为3的数组。所以当处理的数据不多时,还是慎重使用字典为好,在很多情况下使用数组也是可以的

使用场景

需要使用键值对(KeyValue)来快速添加和查找,并且元素没有特定的顺序时。

相关推荐
Sitarrrr几秒前
【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中
3d·unity
极梦网络无忧3 分钟前
Unity中IK动画与布偶死亡动画切换的实现
unity·游戏引擎·lucene
△曉風殘月〆6 小时前
WPF MVVM入门系列教程(二、依赖属性)
c#·wpf·mvvm
逐·風8 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i10 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
m0_6569747411 小时前
C#中的集合类及其使用
开发语言·c#
九鼎科技-Leo11 小时前
了解 .NET 运行时与 .NET 框架:基础概念与相互关系
windows·c#·.net
九鼎科技-Leo14 小时前
什么是 ASP.NET Core?与 ASP.NET MVC 有什么区别?
windows·后端·c#·asp.net·mvc·.net
.net开发14 小时前
WPF怎么通过RestSharp向后端发请求
前端·c#·.net·wpf
小乖兽技术14 小时前
C#与C++交互开发系列(二十):跨进程通信之共享内存(Shared Memory)
c++·c#·交互·ipc